Nullptr kullanmanın avantajları nelerdir?


163

Bu kod parçası kavramsal olarak üç işaretçi için de aynı şeyi yapar (güvenli işaretçi başlatma):

int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;

Ve böylece, işaretçileri atama avantajları nelerdir nullptronlara değerleri atamak üzerinde NULLveya 0?


39
Bir kere, bir aşırı fonksiyon alarak intve void *seçim olmayacaktır intüzerinde sürümünü void *kullanırken sürümü nullptr.
chris

2
Peki f(nullptr)bundan farklı f(NULL). Ancak yukarıdaki kod söz konusu olduğunda (yerel bir değişkene atama), her üç işaretçi de tamamen aynıdır. Tek avantaj kod okunabilirliğidir.
balki

2
Bunu bir SSS yapmaktan yanayım @Prasoon. Teşekkürler!
sbi

1
NB NULL, tarihsel olarak 0 olduğu garanti edilmez, ancak oc C99'dur, aynı şekilde bir bayt mutlaka 8 bit uzunluğunda ve doğru ve yanlış, mimariye bağlı değerlerdir. Bu soru nullptr0 veNULL
awiebe

Yanıtlar:


180

Bu kodda bir avantaj yok gibi görünüyor. Ancak aşağıdaki aşırı yüklenmiş işlevleri göz önünde bulundurun:

void f(char const *ptr);
void f(int v);

f(NULL);  //which function will be called?

Hangi işlev çağrılır? Tabii ki, burada niyet aramaktır f(char const *), ama gerçekte f(int)çağrılacaktır! Bu büyük bir sorun 1 , değil mi?

Yani, bu tür sorunların çözümü kullanmaktır nullptr:

f(nullptr); //first function is called

Tabii ki, tek avantajı bu değil nullptr. İşte bir başkası:

template<typename T, T *ptr>
struct something{};                     //primary template

template<>
struct something<nullptr_t, nullptr>{};  //partial specialization for nullptr

Şablonda olduğundan, türü nullptrolarak çıkarılır nullptr_t, böylece bunu yazabilirsiniz:

template<typename T>
void f(T *ptr);   //function to handle non-nullptr argument

void f(nullptr_t); //an overload to handle nullptr argument!!!

1. yılında C ++ NULLolarak tanımlanır #define NULL 0temelde bu yüzden, intneden olmasın, f(int)denir.


1
Mehrdad'ın söylediği gibi, bu tür aşırı yükler oldukça nadirdir. Avantajlı başka avantajları var nullptrmı? (Hayır
Mark Garcia

2
@MarkGarcia, Bu yardımcı olabilir: stackoverflow.com/questions/13665349/…
chris

9
Dipnotunuz geriye doğru görünüyor. NULLstandardın ayrılmaz bir türe sahip olması gerekir ve bu nedenle genellikle 0veya olarak tanımlanır 0L. Ayrıca bu nullptr_taşırı yükü sevdiğimden emin değilim , çünkü farklı bir tipte boş bir işaretçi ile değil, sadece çağrıları yakalar . Ama bazı kullanımları olduğuna inanabilirim, tek yaptığı, "hiçbiri" anlamına gelen tek değerli bir yer tutucu türü tanımlamaktan kurtarmak olsa bile. nullptr(void*)0
Steve Jessop

1
Başka bir avantaj (kuşkusuz küçük olsa da) nullptriyi tanımlanmış bir sayısal değere sahipken, boş işaretçi sabitleri bu olmayabilir. Bir boş işaretçi sabiti, o türdeki boş işaretleyiciye dönüştürülür (her ne olursa olsun). Aynı türdeki iki boş göstergenin aynı şekilde karşılaştırılması ve boole dönüşümünün bir boş gösterici haline getirilmesi gerekir false. Başka bir şey gerekmez. Bu nedenle, bir derleyicinin (aptalca, ancak mümkün) 0xabcdef1234boş gösterici için örneğin veya başka bir sayı kullanması mümkündür . Öte yandan, nullptrsayısal sıfıra dönüştürmek gerekir.
Damon

1
@DeadMG: Cevabımda yanlış olan nedir? O f(nullptr)amaçlanan işlevini aramadı? Birden fazla motivasyon vardı. Önümüzdeki yıllarda programcılar tarafından başka birçok faydalı şey keşfedilebilir. Demek orada olduğunu söyleyemeyiz sadece bir tane gerçek kullanım arasında nullptr.
Nawaz

87

C ++ 11 nullptr, Nullişaretçi sabiti olarak bilinir ve mevcut güvenliği geliştirir ve mevcut uygulamaya bağlı boş işaretçi sabitinin aksine belirsiz durumları çözerNULL . Avantajlarını kavrayabilme nullptr. öncelikle NULLbununla ilişkili sorunların ne olduğunu ve ne olduğunu anlamamız gerekir.


Nedir NULLtam olarak?

Ön C ++ 11 NULL, değeri olmayan bir işaretçiyi veya geçerli hiçbir şeye işaret etmeyen işaretçiyi temsil etmek için kullanıldı. Popüler kavramın aksine NULLC ++ 'da bir anahtar kelime değildir . Standart kütüphane başlıklarında tanımlanan bir tanımlayıcıdır. Kısacası NULLbazı standart kütüphane başlıklarını eklemeden kullanamazsınız . Örnek programı düşünün :

int main()
{ 
    int *ptr = NULL;
    return 0;
}

Çıktı:

prog.cpp: In function 'int main()':
prog.cpp:3:16: error: 'NULL' was not declared in this scope

C ++ standardı, NULL'u belirli standart kitaplık başlık dosyalarında tanımlanan bir uygulama tanımlı makro olarak tanımlar. C ve C ++ olarak C. C standardı tanımlanan NULL dan miras gelen NULL kökeni 0veya (void *)0. Ancak C ++ 'da ince bir fark vardır.

C ++ bu belirtimi olduğu gibi kabul edemedi. C ++ 'dan farklı olarak, C ++ kuvvetle yazılan bir dildir (C void*herhangi bir türe açık bir döküm gerektirmezken C ++ açık bir döküm gerektirir). Bu, C standardı tarafından belirtilen NULL tanımını birçok C ++ ifadesinde işe yaramaz hale getirir. Örneğin:

std::string * str = NULL;         //Case 1
void (A::*ptrFunc) () = &A::doSomething;
if (ptrFunc == NULL) {}           //Case 2

NULL olarak tanımlandıysa (void *)0, yukarıdaki ifadelerin hiçbiri işe yaramaz.

  • Durum 1: Bir otomatik döküm gelen gereklidir çünkü derlemek olmaz void *için std::string.
  • Durum 2: Derleyiciden void *üye işlevine yayınlama gerektiğinden derlenmeyecek .

Yani C aksine C ++ Standart sayısal hazır olarak NULL tanımlamak için zorunlu 0ya 0L.


Öyleyse, NULLzaten varken başka bir boş işaretçi sabitine ihtiyaç nedir?

C ++ Standartları komitesi C ++ için işe yarayacak bir NULL tanımlansa da, bu tanımın kendi sorunlarından adil bir payı vardı. NULL, neredeyse tüm senaryolar için yeterince iyi çalıştı, ancak hepsi için değil. Bazı nadir senaryolar için şaşırtıcı ve hatalı sonuçlar verdi. Örneğin :

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    doSomething(NULL);
    return 0;
}

Çıktı:

In Int version

Açıkçası, amaç char*argüman olarak sürülen sürümü çağırmak gibi görünmektedir , ancak çıktıda bir intsürümü alan fonksiyon çağrılmaktadır. Çünkü NULL sayısal bir değişmez değerdir.

Ayrıca, NULL değerinin 0 mı yoksa 0L mi olduğu uygulama tarafından tanımlandığından, işlev aşırı yük çözünürlüğünde çok fazla karışıklık olabilir.

Örnek Program:

#include <cstddef>

void doSomething(int);
void doSomething(char *);

int main()
{
  doSomething(static_cast <char *>(0));    // Case 1
  doSomething(0);                          // Case 2
  doSomething(NULL)                        // Case 3
}

Yukarıdaki pasajı analiz etme:

  • Durum 1:doSomething(char *) Beklendiği gibi çağrılar .
  • Durum 2: çağrılar doSomething(int)ama belki char*sürüm istendi çünkü 0IS de bir boş gösterici.
  • Durum 3: Eğer NULLolarak tanımlanırsa 0, doSomething(int)belki doSomething(char *)de amaçlandığı zaman çağırır , belki de çalışma zamanında mantık hatasına neden olur. Eğer NULLolarak tanımlanır 0L, çağrı belirsiz ve derleme hatayla sonuçlanır.

Dolayısıyla, uygulamaya bağlı olarak, aynı kod açıkça istenmeyen çeşitli sonuçlar verebilir. Doğal olarak, C ++ standartlar komitesi bunu düzeltmek istedi ve bu nullptr için temel motivasyon.


Peki sorun nedir nullptrve nasıl önlenir NULL?

C ++ 11 nullptr, boş işaretçi sabiti olarak hizmet etmek için yeni bir anahtar kelime sunar. NULL'den farklı olarak, davranışı uygulama tanımlı değildir. Bir makro değil, kendine özgü bir türü var. nullptr türü vardır std::nullptr_t. C ++ 11, NULL dezavantajlarını önlemek için nullptr özelliklerini uygun şekilde tanımlar. Özelliklerini özetlemek için:

Mülkiyet 1: kendi türü vardır std::nullptr_tve
Mülk 2: örtük olarak dönüştürülebilir ve herhangi bir işaretçi türü veya işaretçi-üye türü ile karşılaştırılabilir, ancak
Özellik 3: dolaylı olarak dönüştürülemez veya hariç ayrılmaz türlerle karşılaştırılabilir değildir bool.

Aşağıdaki örneği düşünün:

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    char *pc = nullptr;      // Case 1
    int i = nullptr;         // Case 2
    bool flag = nullptr;     // Case 3

    doSomething(nullptr);    // Case 4
    return 0;
}

Yukarıdaki programda,

  • Durum 1: Tamam - Mülk 2
  • Durum 2: Değil - Mülk 3
  • Durum 3: Tamam - Mülk 3
  • Durum 4: Karışıklık yok - Çağrı char *sürümü, Özellik 2 ve 3

Böylece nullptr'in tanıtımı eski NULL'un tüm problemlerinden kaçınır.

Nasıl ve nerede kullanmalısın nullptr ?

C ++ 11 için başparmak kuralı nullptr, geçmişte NULL kullanacaksanız kullanmaya başlamanızdır .


Standart Referanslar:

C ++ 11 Standart: C.3.2.4 Makro NULL
C ++ 11 Standart: 18.2 Türler
C ++ 11 Standart: 4.10 İşaretçi dönüşümleri
C99 Standart: 6.3.2.3 İşaretçiler


Bildiğimden beri son tavsiyenizi zaten uyguluyorum nullptr, yine de kodum için gerçekten ne fark ettiğini bilmiyordum. Mükemmel cevap için ve özellikle çaba için teşekkürler. Konuya çok fazla ışık getirdi.
Mark Garcia

"bazı standart kütüphane başlık dosyalarında." -> neden sadece "cstddef" yazmıyorsunuz?
mxmlnkn

Nullptr'in bool tipine dönüştürülmesine neden izin vermeliyiz? Daha fazla ayrıntı verebilir misiniz?
Robert Wang

... değeri olmayan bir işaretçiyi temsil etmek için kullanıldı ... Değişkenlerin her zaman bir değeri vardır. Gürültü olabilir veya 0xccccc....değersiz bir değişken doğal bir çelişkidir.
3

"Durum 3: Tamam - Özellik 3" (satır bool flag = nullptr;). Hayır, tamam değil, g ++ 6 ile derleme zamanında aşağıdaki hatayı alıyorum:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg

23

Buradaki gerçek motivasyon mükemmel yönlendirme .

Düşünmek:

void f(int* p);
template<typename T> void forward(T&& t) {
    f(std::forward<T>(t));
}
int main() {
    forward(0); // FAIL
}

Basitçe ifade etmek gerekirse, 0 özel bir değerdir , ancak değerler yalnızca sistem türlerinde yayılamaz. Yönlendirme işlevleri önemlidir ve 0 bunlarla baş edemez. Böylece, tanıtmak için kesinlikle gerekli olduğunu nullptr, nerede tipi özel ve tip gerçekten yayılabilir budur. Aslında, MSVC ekibinullptr rvalue referanslarını uyguladıktan ve daha sonra kendileri için bu tuzakları keşfettikten sonra programın önüne geçmek .

nullptrHayatı kolaylaştırabilecek birkaç köşe vakası daha var - ancak bu bir döküm, bu sorunları çözebileceği için temel bir durum değil. Düşünmek

void f(int);
void f(int*);
int main() { f(0); f(nullptr); }

İki ayrı aşırı yük çağırır. Ayrıca,

void f(int*);
void f(long*);
int main() { f(0); }

Bu belirsiz. Ancak, nullptr ile,

void f(std::nullptr_t)
int main() { f(nullptr); }

7
Komik. Cevabın yarısı, size göre "oldukça yanlış" cevaplar olan diğer iki cevapla aynı !
Nawaz

Yönlendirme problemi bir kadro ile de çözülebilir. forward((int*)0)İşler. Bir şey mi kaçırıyorum?
jcsahnwaldt Reinstate Monica

5

Nullptr'in temelleri

std::nullptr_tnull işaretçi değişmezinin türü, nullptr. Bu bir tür ön değer / değerdir std::nullptr_t. Herhangi bir işaretçi türünün nullptr değerinden null işaretçi değerine örtülü dönüştürmeler vardır.

Değişmez 0, bir işaretçi değil, int'tir. C ++ kendisini yalnızca bir işaretçinin kullanılabileceği bir bağlamda 0'a bakarken bulursa, 0'ı küstahça boş gösterici olarak yorumlar, ancak bu bir geri dönüş konumudur. C ++ 'ın birincil ilkesi 0 bir işaretçi değil, bir int olmasıdır.

Avantaj 1 - İşaretçi ve integral türlerine aşırı yüklenirken belirsizliği ortadan kaldırın

C ++ 98'de bunun birincil anlamı, işaretçi ve integral türlerine aşırı yüklenmenin sürprizlere yol açabileceğiydi. Bu tür aşırı yüklere 0 veya NULL iletilmesi, hiçbir zaman işaretçi aşırı yükü olarak adlandırılmaz:

   void fun(int); // two overloads of fun
    void fun(void*);
    fun(0); // calls f(int), not fun(void*)
    fun(NULL); // might not compile, but typically calls fun(int). Never calls fun(void*)

Bu çağrı ile ilgili ilginç olan şey, kaynak kodun görünen anlamı (“NULL-boş gösterici ile eğlenceye çağırıyorum”) ve gerçek anlamı (“bir tür tamsayı ile eğlenceye çağırıyorum - boş değil) Işaretçi").

nullptr'un avantajı, ayrılmaz bir türü olmamasıdır. Nullptr ile aşırı yüklenmiş işlevi eğlence olarak adlandırmak, void * aşırı yükünü (yani, işaretçi aşırı yüklenmesi) çağırır, çünkü nullptr tümleşik bir şey olarak görüntülenemez:

fun(nullptr); // calls fun(void*) overload 

0 veya NULL yerine nullptr kullanmak aşırı çözünürlük sürprizlerini önler.

Bir diğer avantajı nullptrover NULL(0)dönüş türü için otomatik kullanırken

Örneğin, bunun bir kod tabanıyla karşılaştığınızı varsayalım:

auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}

FindRecord'un neyi döndürdüğünü bilmiyorsanız (veya kolayca bulamıyorsanız), sonucun bir işaretçi türü veya integral türü olup olmadığı net olmayabilir. Sonuçta, 0 (hangi sonuca karşı test edilir) her iki şekilde de gidebilir. Diğer taraftan aşağıdakileri görürseniz,

auto result = findRecord( /* arguments */ );
if (result == nullptr) {
...
}

belirsizlik yoktur: sonuç bir işaretçi türü olmalıdır.

Avantaj 3

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

void lockAndCallF1()
{
        MuxtexGuard g(f1m); // lock mutex for f1
        auto result = f1(static_cast<int>(0)); // pass 0 as null ptr to f1
        cout<< result<<endl;
}

void lockAndCallF2()
{
        MuxtexGuard g(f2m); // lock mutex for f2
        auto result = f2(static_cast<int>(NULL)); // pass NULL as null ptr to f2
        cout<< result<<endl;
}
void lockAndCallF3()
{
        MuxtexGuard g(f3m); // lock mutex for f2
        auto result = f3(nullptr);// pass nullptr as null ptr to f3 
        cout<< result<<endl;
} // unlock mutex
int main()
{
        lockAndCallF1();
        lockAndCallF2();
        lockAndCallF3();
        return 0;
}

Yukarıda program derleme ve başarıyla yürütülür ancak lockAndCallF1, lockAndCallF2 & lockAndCallF3 yedek kodu vardır. Tüm bunlar için şablon yazabilirsek, böyle bir kod yazmak üzücü lockAndCallF1, lockAndCallF2 & lockAndCallF3. Böylece şablon ile genelleştirilebilir. lockAndCallÇoklu tanım yerine şablon fonksiyonu yazdımlockAndCallF1, lockAndCallF2 & lockAndCallF3Yedek kod için .

Kod aşağıdaki gibi yeniden faktörlendirilir:

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
//decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr)
{
        MuxtexGuard g(mutex);
        return func(ptr);
}
int main()
{
        auto result1 = lockAndCall(f1, f1m, 0); //compilation failed 
        //do something
        auto result2 = lockAndCall(f2, f2m, NULL); //compilation failed
        //do something
        auto result3 = lockAndCall(f3, f3m, nullptr);
        //do something
        return 0;
}

Derleme için neden başarısız Detay analizi lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)için değillockAndCall(f3, f3m, nullptr)

Neden derleme lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)başarısız oldu?

Sorun, lockAndCall öğesine 0 iletildiğinde, şablon türü kesinti türünü bulmak için devreye girmesidir. 0 türü int şeklindedir, bu nedenle lockAndCall çağrısının örneğinin içindeki ptr parametresinin türü budur. Ne yazık ki, bu lockAndCall içinde func için çağrıda, bir int geçiriliyor ve bu beklenen std::shared_ptr<int>parametre ile uyumlu olmadığı anlamına gelir f1. Çağrısında geçirilen 0 lockAndCall, boş bir işaretçiyi temsil etmeyi amaçladı, ancak gerçekte geçen şey int idi. Bu int'i f1'e a olarak aktarmaya çalışmak std::shared_ptr<int>bir tür hatadır. lockAndCallŞablonun içinde bir int gerektiren bir işleve int iletildiği için 0 ile çağrısı başarısız olur std::shared_ptr<int>.

Çağrı içeren analiz NULLesasen aynıdır. 'E NULLiletildiğinde lockAndCall, ptr parametresi için bir integral türü çıkarılır ve a'yı almayı bekleyen ptr—an int veya int benzeri bir tür-- iletildiğinde bir tür hatası oluşur .f2std::unique_ptr<int>

Aksine, çağrı içeren nullptrherhangi bir sorun yoktur. Ne zaman nullptriletilir lockAndCall, için türü ptrçıkarılır std::nullptr_t. Ne zaman ptrgeçirilir f3, bir örtük dönüşüm gerçekleşmesi std::nullptr_tiçin int*, çünkü std::nullptr_tdolaylı dönüştürür tüm işaretçi türlerine.

Bir boş göstericiye başvurmak istediğinizde, 0 veya nullptr kullanın NULL.


4

nullptrÖrnekleri gösterme şeklinize sahip olmanın doğrudan bir avantajı yoktur .
Ancak aynı ada sahip 2 işlevinizin olduğu bir durumu düşünün; 1 alır intve başka birint*

void foo(int);
void foo(int*);

foo(int*)Bir NULL ileterek aramak istiyorsanız , yol:

foo((int*)0); // note: foo(NULL) means foo(0)

nullptrdaha kolay ve sezgisel hale getirir :

foo(nullptr);

Bjarne'nin web sayfasından ek bağlantı .
Alakasız ancak C ++ 11 yan notunda:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)

3
Referans olarak, decltype(nullptr)bir std::nullptr_t.
chris

2
@ MarkGarcia, Bildiğim kadarıyla tam gelişmiş bir tip.
chris

5
@ MarkGarcia, İlginç bir soru. cppreference vardır: typedef decltype(nullptr) nullptr_t;. Sanırım standartlara bakabilirim. Ah, buldum: Not: std :: nullptr_t, bir işaretçi türü ya da üye türüne işaretçi olmayan farklı bir türdür; bunun yerine, bu tür bir ön değer boş bir işaretçi sabitidir ve boş bir işaretçi değerine veya boş üye işaretçi değerine dönüştürülebilir.
chris

2
@DeadMG: Birden fazla motivasyon vardı. Önümüzdeki yıllarda programcılar tarafından başka birçok faydalı şey keşfedilebilir. Demek orada olduğunu söyleyemeyiz sadece bir tane gerçek kullanım arasında nullptr.
Nawaz

2
@DeadMG: Ama bu cevabın "oldukça yanlış" olduğunu söylediniz, çünkü cevabınızda bahsettiğiniz "gerçek motivasyon" hakkında konuşmuyor . Sadece bu cevabın (benim de) senden aşağı oy alması değil.
Nawaz

4

Diğerlerinin söylediği gibi, birincil avantajı aşırı yüklerde yatmaktadır. Açık intve işaretçi aşırı yüklemeleri nadir olsa da, std::fill(örneğin, C ++ 03'te bir kereden fazla ısırdı) gibi standart kitaplık işlevlerini göz önünde bulundurun :

MyClass *arr[4];
std::fill_n(arr, 4, NULL);

Derlemek yok: Cannot convert int to MyClass*.


2

IMO, aşırı yük sorunlarından daha önemlidir: derin iç içe şablon yapılarında, türlerin izini kaybetmemek zordur ve açık imzalar vermek oldukça uğraştır. Bu nedenle, kullandığınız her şey için, amaçlanan amaca ne kadar hassas odaklanırsanız, açık imzalara olan gereksinim o kadar iyi olur ve derleyicinin bir şeyler ters gittiğinde daha anlaşılır hata iletileri üretmesine izin verir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.