wake-up-neo.com

Was sind die Vorteile von nullptr?

Dieser Code konzeptionell bewirkt dasselbe für die drei Zeiger (sichere Zeigerinitialisierung):

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

Was sind also die Vorteile der Zuweisung von Zeigern nullptr gegenüber der Zuweisung der Werte NULL oder 0?

156
Mark Garcia

In diesem Code scheint es keinen Vorteil zu geben. Beachten Sie jedoch die folgenden überladenen Funktionen:

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

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

Welche Funktion wird aufgerufen? Natürlich soll hier f(char const *) aufgerufen werden, aber in Wirklichkeit wird f(int) aufgerufen! Das ist ein großes Problem1nicht wahr

Die Lösung für solche Probleme ist also nullptr:

f(nullptr); //first function is called

Das ist natürlich nicht der einzige Vorteil von nullptr. Hier ist eine andere:

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

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

Da in der Vorlage der Typ von nullptr als nullptr_t Abgeleitet wird, können Sie Folgendes schreiben:

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

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

1. In C++ ist NULL als #define NULL 0 Definiert, es ist also im Grunde int, weshalb f(int) aufgerufen wird.

173
Nawaz

C++ 11 führt nullptr ein, es wird als die Zeigerkonstante Null bezeichnet, und It verbessert die Typensicherheit und löst mehrdeutige Situationen im Gegensatz zur vorhandenen implementierungsabhängigen Nullzeiger-Konstante NULL. Die Vorteile von nullptr verstehen. Zuerst müssen wir verstehen, was NULL ist und welche Probleme damit verbunden sind.


Was genau ist NULL?

Vor C++ 11 wurde NULL verwendet, um einen Zeiger darzustellen, der keinen Wert hat, oder einen Zeiger, der auf nichts Gültiges verweist. Im Gegensatz zur gängigen Vorstellung ist NULL in C++ kein Schlüsselwort . Es ist eine Kennung, die in Standard-Bibliotheksüberschriften definiert ist. Kurz gesagt, Sie können NULL nicht verwenden, ohne einige Standard-Bibliotheks-Header einzuschließen. Betrachten Sie das Beispielprogramm:

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

Ausgabe:

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

Der C++ - Standard definiert NULL als ein implementierungsdefiniertes Makro, das in bestimmten Standard-Bibliotheks-Header-Dateien definiert ist. Der Ursprung von NULL ist von C und C++ hat es von C geerbt. Der C-Standard definiert NULL als 0 Oder (void *)0. In C++ gibt es jedoch einen subtilen Unterschied.

C++ konnte diese Spezifikation so wie sie ist nicht akzeptieren. Im Gegensatz zu C ist C++ eine stark typisierte Sprache (C erfordert keine explizite Umwandlung von void* In einen beliebigen Typ, während C++ eine explizite Umwandlung erfordert). Dies macht die in C-Standard angegebene Definition von NULL in vielen C++ - Ausdrücken unbrauchbar. Beispielsweise:

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

Wenn NULL als (void *)0 Definiert wäre, würde keiner der obigen Ausdrücke funktionieren.

  • Fall 1: Wird nicht kompiliert, da eine automatische Umwandlung von void * Nach std::string Erforderlich ist.
  • Fall 2: Wird nicht kompiliert, da die Umwandlung von void * Zum Zeiger auf die Elementfunktion erforderlich ist.

Im Gegensatz zu C muss C++ Standard NULL als numerisches Literal 0 Oder 0L Definieren.


Also, was ist die Notwendigkeit für eine andere Nullzeiger-Konstante, wenn wir bereits NULL haben?

Obwohl das C++ - Standardkomitee eine NULL-Definition ausgearbeitet hat, die für C++ geeignet ist, hatte diese Definition ihre eigenen Probleme. NULL funktionierte für fast alle Szenarien, aber nicht für alle. Es gab überraschende und fehlerhafte Ergebnisse für bestimmte seltene Szenarien. Zum Beispiel:

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

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

Ausgabe:

In Int version

Offensichtlich scheint die Absicht zu sein, die Version aufzurufen, die char* Als Argument verwendet, aber wie die Ausgabe zeigt, wird die Funktion aufgerufen, die eine int -Version benötigt. Dies liegt daran, dass NULL ein numerisches Literal ist.

Da darüber hinaus durch die Implementierung festgelegt ist, ob NULL 0 oder 0L ist, kann die Funktionsüberladungsauflösung verwirrend sein.

Beispielprogramm:

#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
}

Analysieren des obigen Snippets:

  • Fall 1: ruft doSomething(char *) wie erwartet auf.
  • Fall 2: ruft doSomething(int) auf, aber möglicherweise wurde die Version char* Gewünscht, weil 0 IS auch ein Nullzeiger.
  • Fall 3: Wenn NULL als 0 Definiert ist, ruft doSomething(int) auf, wenn vielleicht doSomething(char *) war vorgesehen, was möglicherweise zu einem Logikfehler zur Laufzeit führte. Wenn NULL als 0L Definiert ist, ist der Aufruf mehrdeutig und führt zu einem Kompilierungsfehler.

Je nach Implementierung kann derselbe Code verschiedene Ergebnisse liefern, was eindeutig unerwünscht ist. Natürlich wollte das C++ - Standardkomitee dies korrigieren, und das ist die Hauptmotivation für nullptr.


Was ist also nullptr und wie vermeidet es die Probleme von NULL?

C++ 11 führt ein neues Schlüsselwort nullptr ein, das als Nullzeiger-Konstante dient. Im Gegensatz zu NULL ist sein Verhalten nicht durch die Implementierung definiert. Es ist kein Makro, aber es hat einen eigenen Typ. nullptr hat den Typ std::nullptr_t. Um die Nachteile von NULL zu vermeiden, definiert C++ 11 die Eigenschaften für nullptr entsprechend. Um seine Eigenschaften zusammenzufassen:

Eigenschaft 1: hat einen eigenen Typ std::nullptr_t Und
Eigenschaft 2: Sie ist implizit konvertierbar und mit jedem Zeigertyp oder Zeiger-auf-Element-Typ vergleichbar
Eigenschaft 3: Mit Ausnahme von bool ist sie nicht implizit konvertierbar oder mit Integraltypen vergleichbar.

Betrachten Sie das folgende Beispiel:

#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;
}

Im obigen Programm

  • Fall 1: OK - Eigenschaft 2
  • Fall 2: Nicht Ok - Eigenschaft 3
  • Fall 3: OK - Eigenschaft 3
  • Fall 4: Keine Verwechslung - Ruft die Version char * Auf, Eigenschaft 2 & 3

Die Einführung von nullptr vermeidet somit alle Probleme des guten alten NULL.

Wie und wo sollten Sie nullptr verwenden?

Die Faustregel für C++ 11 lautet, dass Sie nullptr immer dann verwenden, wenn Sie in der Vergangenheit sonst NULL verwendet hätten.


Standardverweise:

C++ 11 Standard: C.3.2.4 Makro NULL
C++ 11 Standard: 18.2 Typen
C++ 11 Standard: 4.10 Zeigerumwandlungen
C99 Standard: 6.3.2.3 Zeiger

84
Alok Save

Die eigentliche Motivation ist hier perfekte Weiterleitung.

Erwägen:

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

Einfach ausgedrückt ist 0 ein spezielles Wert, aber Werte können sich nicht durch die Nur-System-Typen verbreiten. Weiterleitungsfunktionen sind unerlässlich, und 0 kann sie nicht verarbeiten. Daher war es absolut notwendig, nullptr einzuführen, wobei type das Besondere ist und der Typ sich tatsächlich verbreiten kann. Tatsächlich musste das MSVC-Team nullptr vorzeitig einführen, nachdem es rWertreferenzen implementiert hatte, und entdeckte dann diese Falle für sich.

Es gibt ein paar andere Eckfälle, in denen nullptr das Leben erleichtern kann - aber es ist kein Kernfall, da eine Besetzung diese Probleme lösen kann. Erwägen

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

Ruft zwei separate Überladungen auf. Darüber hinaus berücksichtigen

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

Das ist nicht eindeutig. Mit nullptr können Sie jedoch Folgendes bereitstellen

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

Grundlagen von nullptr

std::nullptr_t Ist der Typ des Nullzeiger-Literales nullptr. Es ist ein Wert vom Typ std::nullptr_t. Es gibt implizite Konvertierungen von nullptr in einen Nullzeigerwert eines beliebigen Zeigertyps.

Das Literal 0 ist ein Int, kein Zeiger. Wenn C++ 0 in einem Kontext betrachtet, in dem nur ein Zeiger verwendet werden kann, interpretiert es 0 widerwillig als Nullzeiger, aber das ist eine Fallback-Position. Die primäre Richtlinie von C++ ist, dass 0 ein Int und kein Zeiger ist.

Vorteil 1 - Mehrdeutigkeit beim Überladen von Zeiger- und Integraltypen beseitigen

In C++ 98 bedeutete dies in erster Linie, dass eine Überladung von Zeiger- und Integraltypen zu Überraschungen führen konnte. Die Übergabe von 0 oder NULL an solche Überladungen wird niemals als Zeigerüberladung bezeichnet:

   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*)

Das Interessante an diesem Aufruf ist der Widerspruch zwischen der offensichtlichen Bedeutung des Quellcodes („Ich rufe Spaß mit NULL auf - dem Nullzeiger“) und seiner tatsächlichen Bedeutung („Ich rufe Spaß mit einer Art Ganzzahl auf - nicht der Null Zeiger").

nullptr hat den Vorteil, dass es keinen integralen Typ gibt. Das Aufrufen der überladenen Funktion fun mit nullptr ruft die void * -Überladung (d. H. Die Zeigerüberladung) auf, da nullptr nicht als etwas Integrales angesehen werden kann:

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

Die Verwendung von nullptr anstelle von 0 oder NULL vermeidet somit Überraschungen bei der Überladungsauflösung.

Ein weiterer Vorteil von nullptr gegenüber NULL(0) bei Verwendung von auto als Rückgabetyp

Angenommen, Sie stoßen in einer Codebasis auf Folgendes:

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

Wenn Sie nicht wissen (oder nicht leicht herausfinden können), was findRecord zurückgibt, ist möglicherweise nicht klar, ob es sich bei dem Ergebnis um einen Zeigertyp oder einen ganzzahligen Typ handelt. Immerhin kann 0 (gegen welches Ergebnis getestet wird) in beide Richtungen gehen. Wenn Sie andererseits Folgendes sehen,

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

es gibt keine Mehrdeutigkeit: Das Ergebnis muss ein Zeigertyp sein.

Vorteil

#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;
}

Das obige Programm wurde erfolgreich kompiliert und ausgeführt, aber lockAndCallF1, lockAndCallF2 und lockAndCallF3 haben redundanten Code. Es ist schade, Code wie diesen zu schreiben, wenn wir Vorlage für all diese lockAndCallF1, lockAndCallF2 & lockAndCallF3 Schreiben können. So kann es mit Vorlage verallgemeinert werden. Ich habe die Template-Funktion lockAndCall anstelle der Mehrfachdefinition lockAndCallF1, lockAndCallF2 & lockAndCallF3 Für redundanten Code geschrieben.

Code wird wie folgt neu faktorisiert:

#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;
}

Detailanalyse, warum die Kompilierung für lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) fehlgeschlagen ist, nicht für lockAndCall(f3, f3m, nullptr)

Warum ist die Kompilierung von lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr) fehlgeschlagen?

Das Problem besteht darin, dass bei der Übergabe von 0 an lockAndCall der Abzug des Vorlagentyps aktiviert wird, um dessen Typ zu ermitteln. Der Typ 0 ist int. Dies ist der Typ des Parameters ptr in der Instanziierung dieses Aufrufs von lockAndCall. Leider bedeutet dies, dass beim Aufruf von func in lockAndCall ein int übergeben wird und dies nicht mit dem std::shared_ptr<int> - Parameter kompatibel ist, den f1 Erwartet. Die 0, die beim Aufruf von lockAndCall übergeben wurde, sollte einen Nullzeiger darstellen, aber was tatsächlich übergeben wurde, war int. Der Versuch, dieses int als std::shared_ptr<int> An f1 zu übergeben, ist ein Typfehler. Der Aufruf von lockAndCall mit 0 schlägt fehl, da in der Vorlage ein int an eine Funktion übergeben wird, die einen std::shared_ptr<int> Benötigt.

Die Analyse für den Aufruf von NULL ist im Wesentlichen dieselbe. Wenn NULL an lockAndCall übergeben wird, wird ein ganzzahliger Typ für den Parameter ptr abgeleitet, und ein Typfehler tritt auf, wenn ptr - ein int- oder int-ähnlicher Typ - übergeben wird auf f2, der einen std::unique_ptr<int> erwartet.

Im Gegensatz dazu hat der Aufruf von nullptr keine Probleme. Wenn nullptr an lockAndCall übergeben wird, wird der Typ für ptr als std::nullptr_t Abgeleitet. Wenn ptr an f3 Übergeben wird, erfolgt eine implizite Konvertierung von std::nullptr_t In int*, Da std::nullptr_t Implizit in alle Zeigertypen konvertiert wird.

Es wird empfohlen, immer dann, wenn Sie auf einen Nullzeiger verweisen möchten, nullptr zu verwenden, nicht 0 oder NULL.

5
Ajay yadav

Wie andere bereits gesagt haben, liegt der Hauptvorteil in Überlastungen. Und obwohl explizite int vs. Zeigerüberladungen selten vorkommen können, sollten Sie Standardbibliotheksfunktionen wie std::fill (was mich in C++ 03 mehrmals gebissen hat):

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

Kompiliert nicht: Cannot convert int to MyClass*.

4
Angew

Es gibt keinen direkten Vorteil, nullptr so zu haben, wie Sie es in den Beispielen gezeigt haben.
Stellen Sie sich jedoch eine Situation vor, in der Sie zwei Funktionen mit demselben Namen haben. 1 nimmt int und eine andere ein int*

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

Wenn Sie foo(int*) aufrufen möchten, indem Sie eine NULL übergeben, lautet der Weg:

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

nullptr macht es einfacher und intuitiver :

foo(nullptr);

Zusätzlicher Link von Bjarnes Webseite.
Irrelevant, aber in C++ 11 Randnotiz:

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

IMO ist wichtiger als diese Überlastungsprobleme: In tief verschachtelten Template-Konstrukten ist es schwierig, die Typen nicht aus den Augen zu verlieren, und explizite Signaturen sind ein ziemliches Unterfangen. Je genauer Sie auf den beabsichtigten Zweck fokussieren, desto besser wird der Bedarf an expliziten Signaturen reduziert, und der Compiler kann aufschlussreichere Fehlermeldungen ausgeben, wenn ein Fehler auftritt.

2
leftaroundabout