Che cos'è esattamente nullptr?


570

Ora abbiamo C ++ 11 con molte nuove funzionalità. Uno interessante e confuso (almeno per me) è il nuovo nullptr.

Bene, non c'è più bisogno della brutta macro NULL.

int* x = nullptr;
myclass* obj = nullptr;

Tuttavia, non capisco come nullptrfunzioni. Ad esempio, l' articolo di Wikipedia dice:

C ++ 11 corregge questo problema introducendo una nuova parola chiave da utilizzare come costante puntatore nullo distinto: nullptr. È di tipo nullptr_t , che è implicitamente convertibile e confrontabile con qualsiasi tipo di puntatore o tipo puntatore a membro. Non è implicitamente convertibile o paragonabile a tipi integrali, ad eccezione di bool.

Come è una parola chiave e un'istanza di un tipo?

Inoltre, hai un altro esempio (oltre a quello di Wikipedia) dove nullptrè superiore al buon vecchio 0?


23
fatto correlato: nullptrviene utilizzato anche per rappresentare riferimento null per handle gestiti in C ++ / CLI.
Mehrdad Afshari,

3
Quando si utilizza Visual C ++, ricordare che se si utilizza nullptr con codice C / C ++ nativo e quindi si compila con l'opzione del compilatore / clr, il compilatore non può determinare se nullptr indica un valore del puntatore null nativo o gestito. Per chiarire la tua intenzione al compilatore, usa nullptr per specificare un valore gestito o __nullptr per specificare un valore nativo. Microsoft ha implementato questo come estensione componente.
ceder

6
È nullptr_tgarantito un solo membro nullptr,? Quindi, se viene restituita una funzione nullptr_t, il compilatore sa già quale valore verrà restituito, indipendentemente dal corpo della funzione?
Aaron McDaid,

8
@AaronMcDaid std::nullptr_tpuò essere istanziato, ma tutte le istanze saranno identiche nullptrperché il tipo è definito come typedef decltype(nullptr) nullptr_t. Credo che il motivo principale per cui esiste il tipo sia che le funzioni possano essere sovraccaricate specificamente per essere catturate nullptr, se necessario. Vedi qui per un esempio.
Justin Time - Ripristina Monica il

5
0 non è mai stato un puntatore null, un puntatore null è un puntatore che può essere ottenuto eseguendo il cast di zero letterale al tipo di puntatore e non punta a nessun oggetto esistente per definizione.
Swift - Friday Pie

Risposte:


403

Come è una parola chiave e un'istanza di un tipo?

Questo non è sorprendente. Entrambi truee falsesono parole chiave e come letterali hanno un tipo ( bool). nullptrè un puntatore letterale di tipo std::nullptr_ted è un valore (non puoi prenderne l'indirizzo usando &).

  • 4.10sulla conversione del puntatore dice che un valore di tipo std::nullptr_tè una costante puntatore null e che una costante puntatore null integrale può essere convertita in std::nullptr_t. La direzione opposta non è consentita. Ciò consente di sovraccaricare una funzione sia per i puntatori che per i numeri interi e di passare nullptrper selezionare la versione del puntatore. Passando NULLo 0selezionerebbe confusamente la intversione.

  • Un cast di nullptr_tun tipo integrale ha bisogno di a reinterpret_cast, e ha la stessa semantica di un cast di (void*)0un tipo integrale (implementazione della mappatura definita). A reinterpret_castnon può essere convertito nullptr_tin nessun tipo di puntatore. Affidati alla conversione implicita se possibile o utilizzali static_cast.

  • Lo standard richiede che sizeof(nullptr_t)sia sizeof(void*).


Oh, dopo aver guardato, mi sembra che l'operatore condizionale non possa convertire 0 in nullptr in casi come cond ? nullptr : 0;. Rimosso dalla mia risposta.
Johannes Schaub - litb

88
Si noti che NULLnon è nemmeno garantito che lo sia 0. Può essere 0L, nel qual caso una chiamata void f(int); void f(char *);sarà ambigua. nullptrfavorirà sempre la versione del puntatore e non chiamerà mai intquella. Si noti inoltre che nullptr è convertibile in bool(la bozza dice che a 4.12).
Johannes Schaub - litb

@litb: quindi per quanto riguarda f (int) ef (void *) - f (0) sarà ancora ambiguo?
Steve Folly,

27
@Steve, no che chiamerà la intversione. Ma f(0L)è ambigua, perché long -> intproprio come long -> void*è entrambi ugualmente costosa. Quindi se NULL è 0Lsul tuo compilatore, allora una chiamata f(NULL)sarà ambigua dato quelle due funzioni. Non così nullptrovviamente.
Johannes Schaub - litb

2
@SvenS Non deve essere definito come (void*)0in C ++. Ma può essere definito come qualsiasi costante puntatore null arbitrario, che qualsiasi costante integrale con valore 0 e nullptrsoddisfa. Quindi, sicuramente non lo farà, ma è possibile . (Hai dimenticato di chiamarmi ping tra ..)
Deduplicatore,

60

Da nullptr: un puntatore nullo sicuro per i tipi e chiaro :

La nuova parola chiave nullptr C ++ 09 designa una costante di valore che funge da puntatore null universale letterale, sostituendo il buggy e lo 0 letterale con caratteri deboli e il famigerato macro NULL. nullptr pone così fine a oltre 30 anni di imbarazzo, ambiguità e bug. Le seguenti sezioni presentano la funzione nullptr e mostrano come può porre rimedio ai disturbi di NULL e 0.

Altre referenze:


17
C ++ 09? Non era indicato come C ++ 0x prima di agosto 2011?
Michael Dorst,

2
@anthropomorphic Bene, questo è il suo scopo. Il C ++ 0x è stato utilizzato mentre era ancora in corso di elaborazione, poiché non era noto se sarebbe stato terminato nel 2008 o nel 2009. Si noti che in realtà è diventato C ++ 0B che significa C ++ 11. Vedi stroustrup.com/C++11FAQ.html
mxmlnkn

45

Perché nullptr in C ++ 11? Che cos'è? Perché NULL non è sufficiente?

L'esperto di C ++ Alex Allain lo dice perfettamente qui (la mia enfasi è aggiunta in grassetto):

... immagina di avere le seguenti due dichiarazioni di funzione:

void func(int n); 
void func(char *s);

func( NULL ); // guess which function gets called?

Anche se sembra che la seconda funzione verrà chiamata - dopotutto, stai passando in quello che sembra essere un puntatore - è davvero la prima funzione che verrà chiamata! Il problema è che poiché NULL è 0 e 0 è un numero intero, verrà invece chiamata la prima versione di func. Questo è il genere di cose che, sì, non succede sempre, ma quando succede, è estremamente frustrante e confuso. Se non conosci i dettagli di ciò che sta succedendo, potrebbe sembrare un bug del compilatore. Una funzione del linguaggio che assomiglia a un bug del compilatore non è qualcosa che desideri.

Inserisci nullptr. In C ++ 11, nullptr è una nuova parola chiave che può (e dovrebbe!) Essere utilizzata per rappresentare i puntatori NULL; in altre parole, ovunque tu abbia scritto NULL prima, dovresti usare nullptr. Non è più chiaro per te, programmatore (tutti sanno cosa significa NULL), ma è più esplicito per il compilatore , che non vedrà più gli 0 ovunque usati per avere un significato speciale quando usato come puntatore.

Allain termina il suo articolo con:

Indipendentemente da tutto ciò, la regola empirica per C ++ 11 è semplicemente quella di iniziare a utilizzare nullptrogni volta che avresti altrimenti usato NULLin passato.

(Le mie parole):

Infine, non dimenticare che nullptrè un oggetto: una classe. Può essere usato ovunque NULLprima, ma se hai bisogno del suo tipo per qualche motivo, il suo tipo può essere estratto decltype(nullptr)o descritto direttamente come std::nullptr_t, che è semplicemente uno typedefdi decltype(nullptr).

Riferimenti:

  1. Cprogramming.com: tipi migliori in C ++ 11 - nullptr, classi enum (enumerazioni fortemente tipizzate) e cstdint
  2. https://en.cppreference.com/w/cpp/language/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t

2
Devo dire che la tua risposta è sottovalutata, è stato molto facile da capire attraverso il tuo esempio.
MSS

37

Quando hai una funzione che può ricevere puntatori a più di un tipo, chiamarla con NULLè ambigua. Il modo in cui questo viene risolto ora è molto confuso accettando un int e assumendolo NULL.

template <class T>
class ptr {
    T* p_;
    public:
        ptr(T* p) : p_(p) {}

        template <class U>
        ptr(U* u) : p_(dynamic_cast<T*>(u)) { }

        // Without this ptr<T> p(NULL) would be ambiguous
        ptr(int null) : p_(NULL)  { assert(null == NULL); }
};

In C++11te sarebbe in grado di sovraccaricare in nullptr_tmodo che ptr<T> p(42);sarebbe un errore di compilazione piuttosto che un tempo di esecuzione assert.

ptr(std::nullptr_t) : p_(nullptr)  {  }

Cosa succede se NULLviene definito come 0L?
LF

9

nullptrnon può essere assegnato a un tipo integrale come un intma solo un tipo di puntatore; un tipo di puntatore incorporato come int *ptro un puntatore intelligente comestd::shared_ptr<T>

Credo che questa sia una distinzione importante perché NULLpuò ancora essere assegnata sia a un tipo integrale che a un puntatore, così come NULLuna macro espansa a 0cui può servire sia come valore iniziale sia per un intpuntatore.


Nota che questa risposta è sbagliata. NULLnon è garantito per essere espanso a 0.
LF

6

Inoltre, hai un altro esempio (oltre a quello di Wikipedia) dove nullptrè superiore al buon vecchio 0?

Sì. È anche un esempio (semplificato) del mondo reale che si è verificato nel nostro codice di produzione. Si è distinto solo perché gcc è stato in grado di emettere un avviso durante la compilazione incrociata su una piattaforma con larghezza di registro diversa (ancora non sono sicuro del perché solo quando si esegue la compilazione incrociata da x86_64 a x86, avvertewarning: converting to non-pointer type 'int' from NULL ):

Considera questo codice (C ++ 03):

#include <iostream>

struct B {};

struct A
{
    operator B*() {return 0;}
    operator bool() {return true;}
};

int main()
{
    A a;
    B* pb = 0;
    typedef void* null_ptr_t;
    null_ptr_t null = 0;

    std::cout << "(a == pb): " << (a == pb) << std::endl;
    std::cout << "(a == 0): " << (a == 0) << std::endl; // no warning
    std::cout << "(a == NULL): " << (a == NULL) << std::endl; // warns sometimes
    std::cout << "(a == null): " << (a == null) << std::endl;
}

Produce questo output:

(a == pb): 1
(a == 0): 0
(a == NULL): 0
(a == null): 1

Non riesco a vedere come questo migliora quando si utilizza nullptr (e C ++ 11). Se si imposta pb su nullptr, il primo confronto risulta ancora vero (confrontando le mele con le pere ...). Il secondo caso è ancora peggio: se si confronta a con nullptr, convertirà a in B * e quindi verrà nuovamente valutato come vero (prima che fosse lanciato in bool e l'espr valutato in falso). Il tutto mi ricorda JavaScript e mi chiedo se avremo === in C ++ in futuro :(
Nils

5

Bene, altre lingue hanno parole riservate che sono istanze di tipi. Python, ad esempio:

>>> None = 5
  File "<stdin>", line 1
SyntaxError: assignment to None
>>> type(None)
<type 'NoneType'>

Questo è in realtà un confronto abbastanza ravvicinato perché in Nonegenere viene utilizzato per qualcosa che non è stato inizializzato, ma allo stesso tempo confronti comeNone == 0 falsi.

D'altra parte, in semplice C, NULL == 0restituirebbe IIRC vero perché NULLè solo una macro che restituisce 0, che è sempre un indirizzo non valido (AFAIK).


4
NULLè una macro che si espande a zero, uno zero costante trasmesso a un puntatore produce un puntatore nullo. Un puntatore null non deve essere zero (ma spesso lo è), zero non è sempre un indirizzo non valido e uno zero non costante su un puntatore non deve essere nullo e un puntatore null su un intero non deve essere zero. Spero di aver capito bene senza dimenticare nulla. Un riferimento: c-faq.com/null/null2.html
Samuel Edwin Ward

3

È una parola chiave perché lo standard la specificherà come tale. ;-) Secondo l'ultima bozza pubblica (n2914)

2.14.7 Pointer letterals [lex.nullptr]

pointer-literal:
nullptr

Il puntatore letterale è la parola chiave nullptr. È un valore di tipo std::nullptr_t.

È utile perché non si converte implicitamente in un valore integrale.


2

Diciamo che hai una funzione (f) che è sovraccarica per prendere sia int che char *. Prima di C ++ 11, se si voleva chiamarlo con un puntatore null e si utilizzava NULL (ovvero il valore 0), si chiamava quello sovraccaricato per int:

void f(int);
void f(char*);

void g() 
{
  f(0); // Calls f(int).
  f(NULL); // Equals to f(0). Calls f(int).
}

Questo probabilmente non è quello che volevi. C ++ 11 risolve questo problema con nullptr; Ora puoi scrivere quanto segue:

void g()
{
  f(nullptr); //calls f(char*)
}

1

Vorrei prima darti un'implementazione di non sofisticato nullptr_t

struct nullptr_t 
{
    void operator&() const = delete;  // Can't take address of nullptr

    template<class T>
    inline operator T*() const { return 0; }

    template<class C, class T>
    inline operator T C::*() const { return 0; }
};

nullptr_t nullptr;

nullptrè un sottile esempio del linguaggio Return Type Resolver per dedurre automaticamente un puntatore nullo del tipo corretto a seconda del tipo di istanza a cui sta assegnando.

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • Come puoi sopra, quando nullptrviene assegnato a un puntatore intero, aint viene creata un'istanza di tipo della funzione di conversione templatizzata. E lo stesso vale per i puntatori al metodo.
  • In questo modo sfruttando la funzionalità del modello, stiamo effettivamente creando il tipo appropriato di puntatore null ogni volta che lo facciamo, una nuova assegnazione di tipo.
  • Dato che nullptrè un valore intero letterale con valore zero, non è possibile utilizzare il suo indirizzo che abbiamo realizzato eliminando e operatore.

Perché abbiamo bisogno nullptrin primo luogo?

  • Vedi tradizionale NULLha qualche problema con esso come di seguito:

1️⃣ Conversione implicita

char *str = NULL; // Implicit conversion from void * to char *
int i = NULL;     // OK, but `i` is not pointer type

2️⃣ Funzione che chiama ambiguità

void func(int) {}
void func(int*){}
void func(bool){}

func(NULL);     // Which one to call?
  • La compilazione produce il seguente errore:
error: call to 'func' is ambiguous
    func(NULL);
    ^~~~
note: candidate function void func(bool){}
                              ^
note: candidate function void func(int*){}
                              ^
note: candidate function void func(int){}
                              ^
1 error generated.
compiler exit status 1

3 overload Sovraccarico del costruttore

struct String
{
    String(uint32_t)    {   /* size of string */    }
    String(const char*) {       /* string */        }
};

String s1( NULL );
String s2( 5 );
  • In tali casi, è necessario un cast esplicito (ad es  String s((char*)0)).

0

0 era l'unico valore intero che poteva essere utilizzato come inizializzatore senza cast per i puntatori: non è possibile inizializzare i puntatori con altri valori interi senza un cast. Puoi considerare 0 come un singleton consexpr sintatticamente simile a un intero letterale. Può avviare qualsiasi puntatore o intero. Ma sorprendentemente, scoprirai che non ha un tipo distinto: è un int. Allora come mai 0 può inizializzare i puntatori e 1 no? Una risposta pratica è che abbiamo bisogno di un mezzo per definire il valore null del puntatore e la conversione implicita diretta di intun puntatore è soggetta a errori. Così 0 divenne una vera strana bestia fuori dall'era preistorica. nullptrè stato proposto di essere una vera rappresentazione singleton constexpr di valore null per inizializzare i puntatori. Non può essere utilizzato per inizializzare direttamente numeri interi ed elimina le ambiguità implicate nella definizione cheNULL in termini di 0.nullptrpotrebbe essere definito come una libreria usando la sintassi std ma semanticamente sembrava essere un componente core mancante. NULLè ora deprecato a favore nullptr, a meno che alcune librerie non decidano di definirlo come nullptr.


-1

Ecco l'intestazione LLVM.

// -*- C++ -*-
//===--------------------------- __nullptr --------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef _LIBCPP_NULLPTR
#define _LIBCPP_NULLPTR

#include <__config>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif

#ifdef _LIBCPP_HAS_NO_NULLPTR

_LIBCPP_BEGIN_NAMESPACE_STD

struct _LIBCPP_TEMPLATE_VIS nullptr_t
{
    void* __lx;

    struct __nat {int __for_bool_;};

    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t() : __lx(0) {}
    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t(int __nat::*) : __lx(0) {}

    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR operator int __nat::*() const {return 0;}

    template <class _Tp>
        _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
        operator _Tp* () const {return 0;}

    template <class _Tp, class _Up>
        _LIBCPP_INLINE_VISIBILITY
        operator _Tp _Up::* () const {return 0;}

    friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator==(nullptr_t, nullptr_t) {return true;}
    friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator!=(nullptr_t, nullptr_t) {return false;}
};

inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t __get_nullptr_t() {return nullptr_t(0);}

#define nullptr _VSTD::__get_nullptr_t()

_LIBCPP_END_NAMESPACE_STD

#else  // _LIBCPP_HAS_NO_NULLPTR

namespace std
{
    typedef decltype(nullptr) nullptr_t;
}

#endif  // _LIBCPP_HAS_NO_NULLPTR

#endif  // _LIBCPP_NULLPTR

(molto può essere scoperto con una rapida grep -r /usr/include/*`)

Una cosa che salta fuori è il *sovraccarico dell'operatore (la restituzione di 0 è molto più amichevole del segfaulting ...). Un'altra cosa è che non sembra compatibile con la memorizzazione di un indirizzo a tutti . Il che, rispetto al modo in cui va scagliando il vuoto * e passando i risultati NULL ai normali puntatori come valori sentinella, ridurrebbe ovviamente il fattore "non dimenticare, potrebbe essere una bomba".


-2

NULL non deve essere 0. Fintanto che usi sempre NULL e mai 0, NULL può avere qualsiasi valore. Supponendo di programmare un microcontrollore von Neuman con memoria piatta, che ha i suoi veicoli di interruzione a 0. Se NULL è 0 e qualcosa scrive su un puntatore NULL, il microcontrollore si arresta in modo anomalo. Se NULL è diciamo 1024 e in 1024 c'è una variabile riservata, la scrittura non la arresterà in modo anomalo e puoi rilevare le assegnazioni del puntatore NULL dall'interno del programma. Questo è inutile su PC, ma per sonde spaziali, attrezzature militari o mediche è importante non schiantarsi.


2
Bene, il valore effettivo del puntatore null in memoria potrebbe non essere zero, ma lo standard C (e C ++) obbliga i compilatori a convertire l'integrale 0 letterale in puntatore null.
bzim,
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.