Quali sono i vantaggi dell'utilizzo di nullptr?


163

Questo pezzo di codice concettualmente fa la stessa cosa per i tre puntatori (inizializzazione sicura del puntatore):

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

E quindi, quali sono i vantaggi di assegnare i puntatori nullptrrispetto all'assegnazione dei valori NULLo 0?


39
Per prima cosa, una funzione sovraccarica richiede inte void *non sceglierà la intversione rispetto alla void *versione durante l'utilizzo nullptr.
chris,

2
Bene f(nullptr)è diverso da f(NULL). Ma per quanto riguarda il codice sopra (assegnando a una variabile locale), tutti e tre i puntatori sono esattamente gli stessi. L'unico vantaggio è la leggibilità del codice.
Balki,

2
Sono favorevole a rendere questa una FAQ, @Prasoon. Grazie!
sabato

1
NB NULL non è storicamente garantito essere 0, ma è come C99, nello stesso modo in cui un byte non era necessariamente lungo 8 bit e vero e falso erano valori dipendenti dall'architettura. Questa domanda si concentra su nullptrbutthat è la differenza tra 0 eNULL
awiebe

Risposte:


180

In quel codice, non sembra esserci alcun vantaggio. Ma considera le seguenti funzioni sovraccaricate:

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

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

Quale funzione verrà chiamata? Naturalmente, l'intenzione qui è quella di chiamare f(char const *), ma in realtà f(int)si chiamerà! Questo è un grosso problema 1 , non è vero?

Quindi, la soluzione a tali problemi è utilizzare nullptr:

f(nullptr); //first function is called

Naturalmente, questo non è l'unico vantaggio di nullptr. Eccone un altro:

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

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

Dato che nel modello, il tipo di nullptrviene dedotto come nullptr_t, quindi puoi scrivere questo:

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 ++, NULLè definito come #define NULL 0, quindi sostanzialmente int, è per questo che f(int)viene chiamato.


1
Come affermato da Mehrdad, questi tipi di sovraccarichi sono piuttosto rari. Ci sono altri vantaggi rilevanti di nullptr? (No. Non sto chiedendo)
Mark Garcia l'

2
@MarkGarcia, questo potrebbe essere utile: stackoverflow.com/questions/13665349/…
chris

9
La tua nota in calce sembra arretrata. NULLè richiesto dallo standard per avere un tipo integrale ed è per questo che in genere è definito come 0o 0L. Inoltre non sono sicuro che mi piace quel nullptr_tsovraccarico, poiché intercetta solo le chiamate nullptr, non con un puntatore nullo di tipo diverso, come (void*)0. Ma posso credere che abbia alcuni usi, anche se tutto ciò che fa è salvarti nel definire un tuo tipo di segnaposto a valore singolo per indicare "nessuno".
Steve Jessop,

1
Un altro vantaggio (anche se certamente minore) può essere quello che nullptrha un valore numerico ben definito, mentre le costanti puntatore null non lo sono. Una costante puntatore null viene convertita nel puntatore null di quel tipo (qualunque esso sia). È necessario che due puntatori null dello stesso tipo si confrontino in modo identico e la conversione booleana trasformi un puntatore null in false. Nient'altro è richiesto. Pertanto, è possibile per un compilatore (sciocco, ma possibile) utilizzare ad es. 0xabcdef1234O qualche altro numero per il puntatore null. D'altra parte, nullptrè necessario convertire in zero numerico.
Damon,

1
@DeadMG: cosa non è corretto nella mia risposta? che f(nullptr)non chiamerà la funzione prevista? C'erano più di una motivazione. Molte altre cose utili possono essere scoperte dagli stessi programmatori nei prossimi anni. Quindi non puoi dire che esiste un solo vero utilizzo di nullptr.
Nawaz,

87

C ++ 11 introduce nullptr, è nota come Nullcostante del puntatore e migliora la sicurezza dei tipi e risolve situazioni ambigue a differenza della costante di puntatore null dipendente dall'implementazione esistente NULL. Essere in grado di comprendere i vantaggi di nullptr. dobbiamo prima capire cos'è NULLe quali sono i problemi ad esso associati.


Che cosa è NULLesattamente?

Pre C ++ 11 è NULLstato utilizzato per rappresentare un puntatore che non ha alcun valore o puntatore che non punta a qualcosa di valido. Contrariamente alla nozione popolare NULLnon è una parola chiave in C ++ . È un identificatore definito nelle intestazioni di libreria standard. In breve, non è possibile utilizzare NULLsenza includere alcune intestazioni di libreria standard. Considera il programma di esempio :

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

Produzione:

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

Lo standard C ++ definisce NULL come una macro definita dall'implementazione definita in alcuni file di intestazione della libreria standard. L'origine di NULL proviene da C e C ++ ereditata da C. Lo standard C ha definito NULL come 0o (void *)0. Ma in C ++ c'è una sottile differenza.

C ++ non ha potuto accettare questa specifica così com'è. A differenza di C, C ++ è un linguaggio fortemente tipizzato (C non richiede il cast esplicito da void*alcun tipo, mentre C ++ impone un cast esplicito). Ciò rende inutile la definizione di NULL specificata dallo standard C in molte espressioni C ++. Per esempio:

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

Se NULL fosse definito come (void *)0, nessuna delle espressioni precedenti funzionerebbe.

  • Caso 1: non verrà compilato perché è necessario un cast automatico da void *a std::string.
  • Caso 2: non verrà compilato perché void *è necessario eseguire il cast dal puntatore alla funzione membro.

Quindi, diversamente da C, C ++ Standard ha il mandato di definire NULL come letterale numerico 0o 0L.


Allora, qual è la necessità di un'altra costante puntatore null quando abbiamo NULLgià?

Sebbene il comitato per gli standard C ++ abbia escogitato una definizione NULL che funzionerà per C ++, questa definizione ha avuto una sua giusta dose di problemi. NULL ha funzionato abbastanza bene per quasi tutti gli scenari, ma non per tutti. Ha dato risultati sorprendenti ed errati per alcuni scenari rari. Per esempio :

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

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

Produzione:

In Int version

Chiaramente, l'intenzione sembra essere quella di chiamare la versione che accetta char*come argomento, ma quando l'output mostra la funzione che accetta una intversione viene chiamata. Questo perché NULL è un valore letterale numerico.

Inoltre, poiché è definito dall'implementazione se NULL è 0 o 0L, può esserci molta confusione nella risoluzione del sovraccarico della funzione.

Programma di esempio:

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

Analizzare lo snippet sopra:

  • Caso 1: chiamate doSomething(char *)come previsto.
  • Caso 2: chiamate doSomething(int)ma forse la char*versione era desiderata perché 0è anche un puntatore nullo.
  • Caso 3: se NULLdefinito come 0, chiama doSomething(int)quando forse doSomething(char *)era previsto, con conseguente errore logico in fase di esecuzione. Se NULLdefinito come 0L, la chiamata è ambigua e genera un errore di compilazione.

Quindi, a seconda dell'implementazione, lo stesso codice può dare vari risultati, il che è chiaramente indesiderato. Naturalmente, il comitato per gli standard C ++ ha voluto correggere questo e questa è la motivazione principale per nullptr.


Quindi cos'è nullptre come evita i problemi di NULL?

C ++ 11 introduce una nuova parola chiave nullptrda utilizzare come costante puntatore null. A differenza di NULL, il suo comportamento non è definito dall'implementazione. Non è una macro ma ha il suo tipo. nullptr ha il tipo std::nullptr_t. C ++ 11 definisce in modo appropriato le proprietà per nullptr per evitare gli svantaggi di NULL. Per riassumere le sue proprietà:

Proprietà 1: ha il suo tipo std::nullptr_te
proprietà 2: è implicitamente convertibile e confrontabile con qualsiasi tipo di puntatore o tipo puntatore a membro, ma la
proprietà 3: non è implicitamente convertibile o paragonabile a tipi integrali, ad eccezione di bool.

Considera il seguente esempio:

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

Nel programma sopra,

  • Caso 1: OK - Proprietà 2
  • Caso 2: Non OK - Proprietà 3
  • Caso 3: OK - Proprietà 3
  • Caso 4: nessuna confusione: chiama la char *versione, proprietà 2 e 3

Quindi l'introduzione di nullptr evita tutti i problemi del buon vecchio NULL.

Come e dove dovresti usare nullptr?

La regola empirica per C ++ 11 è semplicemente iniziare a usare nullptrogni volta che altrimenti avresti usato NULL in passato.


Riferimenti standard:

Standard C ++ 11: C.3.2.4 Macro NULL
Standard C ++ 11: 18.2 Tipi
Standard C ++ 11: 4.10 Conversioni puntatore
Standard C99: 6.3.2.3 Puntatori


Sto già praticando il tuo ultimo consiglio da quando l'ho conosciuto nullptr, anche se non sapevo che differenza fa davvero con il mio codice. Grazie per l'ottima risposta e soprattutto per l'impegno. Mi ha dato molta luce sull'argomento.
Mark Garcia,

"in alcuni file di intestazione della libreria standard." -> perché non scrivere "cstddef" dall'inizio?
mxmlnkn,

Perché dovremmo consentire a nullptr di essere convertibile in tipo bool? Potresti per favore elaborare di più?
Robert Wang,

... è stato usato per rappresentare un puntatore che non ha valore ... Le variabili hanno sempre un valore. Può essere rumore o 0xccccc...., ma, una variabile senza valore è una contraddizione intrinseca.
3Dave l'

"Caso 3: OK - Proprietà 3" (riga bool flag = nullptr;). No, non va bene, ricevo il seguente errore in fase di compilazione con g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg

23

La vera motivazione qui è l' inoltro perfetto .

Tener conto di:

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

In poche parole, 0 è un valore speciale , ma i valori non possono propagarsi attraverso i soli tipi di sistema. Le funzioni di inoltro sono essenziali e 0 non può gestirle. Pertanto, era assolutamente necessario introdurre nullptr, dove il tipo è ciò che è speciale e il tipo può effettivamente propagarsi. In effetti, il team di MSVC ha dovuto presentarsi in nullptranticipo rispetto al programma dopo aver implementato i riferimenti di valore e poi scoperto questa trappola per sé.

Ci sono alcuni altri casi angolari in cui nullptrpuò semplificare la vita, ma non è un caso fondamentale, in quanto un cast può risolvere questi problemi. Tener conto di

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

Chiama due sovraccarichi separati. Inoltre, considera

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

Questo è ambiguo. Ma, con nullptr, puoi fornire

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

7
Divertente. La metà della risposta è uguale alle altre due risposte che secondo te sono risposte "abbastanza errate" !!!
Nawaz,

Il problema di inoltro può anche essere risolto con un cast. forward((int*)0)lavori. Mi sto perdendo qualcosa?
jcsahnwaldt Reinstate Monica,

5

Nozioni di base di nullptr

std::nullptr_tè il tipo del puntatore null letterale, nullptr. È un valore / valore di tipo std::nullptr_t. Esistono conversioni implicite da nullptr a valore di puntatore null di qualsiasi tipo di puntatore.

Lo 0 letterale è un int, non un puntatore. Se C ++ si trova a guardare 0 in un contesto in cui è possibile utilizzare solo un puntatore, interpreterà a malincuore 0 come puntatore nullo, ma questa è una posizione di fallback. La politica principale di C ++ è che 0 è un int, non un puntatore.

Vantaggio 1: rimuovere l'ambiguità in caso di sovraccarico su puntatore e tipi integrali

In C ++ 98, la principale conseguenza di ciò era che il sovraccarico su puntatori e tipi integrali poteva portare a sorprese. Passare 0 o NULL a tali sovraccarichi non ha mai chiamato un sovraccarico del puntatore:

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

La cosa interessante di quella chiamata è la contraddizione tra il significato apparente del codice sorgente ("Sto chiamando divertimento con NULL-il puntatore null") e il suo significato reale ("Sto chiamando divertimento con un qualche tipo di numero intero - non il null puntatore”).

Il vantaggio di nullptr è che non ha un tipo integrale. Chiamare la funzione sovraccarico divertente con nullptr chiama il void * overload (ovvero il sovraccarico del puntatore), poiché nullptr non può essere visto come qualcosa di integrale:

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

L'uso di nullptr anziché 0 o NULL evita quindi sorprese nella risoluzione del sovraccarico.

Un altro vantaggio di nullptrover NULL(0)quando si utilizza auto per il tipo restituito

Ad esempio, supponiamo di riscontrarlo in una base di codice:

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

Se non ti capita di sapere (o non riesci a scoprire facilmente) cosa restituisce findRecord, potrebbe non essere chiaro se il risultato sia un tipo di puntatore o un tipo integrale. Dopotutto, 0 (con quale risultato viene testato) potrebbe andare in entrambi i modi. Se vedi quanto segue, d'altra parte,

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

non c'è ambiguità: il risultato deve essere un tipo di puntatore.

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

Il programma sopra compilato ed eseguito correttamente ma lockAndCallF1, lockAndCallF2 e lockAndCallF3 hanno un codice ridondante. È un peccato scrivere codice in questo modo se possiamo scrivere un modello per tutti questi lockAndCallF1, lockAndCallF2 & lockAndCallF3. Quindi può essere generalizzato con template. Ho scritto la funzione modello lockAndCallanziché la definizione multipla lockAndCallF1, lockAndCallF2 & lockAndCallF3per il codice ridondante.

Il codice viene ricodificato come di seguito:

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

Analisi di dettaglio per cui la compilazione non è riuscita per lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)non perlockAndCall(f3, f3m, nullptr)

Perché compilazione lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)fallita?

Il problema è che quando 0 viene passato a lockAndCall, inizia la deduzione del tipo di modello per capirne il tipo. Il tipo di 0 è int, quindi questo è il tipo di parametro ptr all'interno dell'istanza di questa chiamata a lockAndCall. Sfortunatamente, questo significa che nella chiamata a func all'interno di lockAndCall, viene passato un int, che non è compatibile con il std::shared_ptr<int>parametro che si f1aspetta. Lo 0 passato nella chiamata a lockAndCalldoveva rappresentare un puntatore nullo, ma ciò che effettivamente veniva passato era int. Cercare di passare questo int a f1 come a std::shared_ptr<int>è un errore di tipo. La chiamata a lockAndCallcon 0 non riesce perché all'interno del modello, un int viene passato a una funzione che richiede un std::shared_ptr<int>.

L'analisi per la chiamata che coinvolge NULLè essenzialmente la stessa. Quando NULLviene passato lockAndCall, viene dedotto un tipo integrale per il parametro ptr e si verifica un errore di tipo quando si passa a un tipo ptrint o simile a int f2che si aspetta di ottenere un std::unique_ptr<int>.

Al contrario, la chiamata in questione nullptrnon ha problemi. Quando nullptrviene passato lockAndCall, ptrviene dedotto il tipo per std::nullptr_t. Quando ptrviene passato a f3, c'è una conversione implicita da std::nullptr_ta int*, perché si std::nullptr_tconverte implicitamente in tutti i tipi di puntatore.

Si consiglia, ogni volta che si desidera fare riferimento a un puntatore null, utilizzare nullptr, non 0 o NULL.


4

Non vi è alcun vantaggio diretto di avere nullptrnel modo in cui hai mostrato gli esempi.
Ma considera una situazione in cui hai 2 funzioni con lo stesso nome; 1 prende inte un altro anint*

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

Se vuoi chiamare foo(int*)passando un NULL, il modo è:

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

nullptrrende più semplice e intuitivo :

foo(nullptr);

Link aggiuntivo dalla pagina Web di Bjarne.
Irrilevante ma su C ++ 11 nota a margine:

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

3
Per riferimento, decltype(nullptr)è std::nullptr_t.
chris,

2
@MarkGarcia, Per quanto ne so, è un tipo in piena regola.
chris,

5
@MarkGarcia, è una domanda interessante. cppreference ha: typedef decltype(nullptr) nullptr_t;. Immagino di poter guardare nello standard. Ah, l'ho trovato: Nota: std :: nullptr_t è un tipo distinto che non è né un tipo di puntatore né un puntatore al tipo di membro; piuttosto, un valore di questo tipo è una costante puntatore null e può essere convertito in un valore puntatore null o valore puntatore membro null.
chris,

2
@DeadMG: c'erano più di una motivazione. Molte altre cose utili possono essere scoperte dagli stessi programmatori nei prossimi anni. Quindi non puoi dire che esiste un solo vero utilizzo di nullptr.
Nawaz,

2
@DeadMG: Ma hai detto che questa risposta è "abbastanza errata" semplicemente perché non parla della "vera motivazione" di cui hai parlato nella tua risposta. Non solo questa risposta (e anche la mia) ha ricevuto un tuo voto negativo.
Nawaz,

4

Proprio come altri hanno già detto, il suo vantaggio principale risiede nei sovraccarichi. E mentre i intsovraccarichi espliciti vs. puntatori possono essere rari, considera le funzioni di libreria standard come std::fill(che mi ha morso più di una volta in C ++ 03):

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

Non compilazione: Cannot convert int to MyClass*.


2

L'IMO è più importante di quei problemi di sovraccarico: in costrutti di template profondamente annidati, è difficile non perdere traccia dei tipi e dare firme esplicite è abbastanza uno sforzo. Quindi, per tutto ciò che usi, più precisamente focalizzato sullo scopo previsto, meglio ridurrà la necessità di firme esplicite e consentirà al compilatore di produrre messaggi di errore più approfonditi quando qualcosa va storto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.