Czym dokładnie jest nullptr?

570

Mamy teraz C ++ 11 z wieloma nowymi funkcjami. Ciekawym i mylącym (przynajmniej dla mnie) jest nowy nullptr.

Cóż, nie trzeba już nieprzyjemnego makra NULL.

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

Nadal nie rozumiem, jak nullptrdziała. Na przykład artykuł w Wikipedii mówi:

C ++ 11 naprawia to, wprowadzając nowe słowo kluczowe, które ma służyć jako wyróżniona stała wskaźnika zerowego: nullptr. Jest typu nullptr_t , który jest domyślnie konwertowalny i porównywalny z dowolnym typem wskaźnika lub typem wskaźnika do elementu. Nie jest domyślnie konwertowalny ani porównywalny z typami integralnymi, z wyjątkiem bool.

Jak to jest słowo kluczowe i instancja typu?

Czy masz też inny przykład (oprócz Wikipedii), gdzie nullptrjest lepszy od starego, dobrego 0?

ARAK
źródło
23
powiązany fakt: nullptrsłuży również do reprezentowania pustego odwołania dla zarządzanych uchwytów w C ++ / CLI.
Mehrdad Afshari,
3
Korzystając z Visual C ++, pamiętaj, że jeśli używasz nullptr z natywnym kodem C / C ++, a następnie kompilujesz z opcją kompilatora / clr, kompilator nie może ustalić, czy nullptr wskazuje natywną czy zarządzaną wartość wskaźnika zerowego. Aby wyjaśnić kompilatorowi swoją intencję, użyj nullptr, aby określić wartość zarządzaną, lub __nullptr, aby określić wartość natywną. Microsoft zaimplementował to jako rozszerzenie komponentu.
cseder
6
Czy nullptr_tzagwarantowane jest posiadanie tylko jednego członka nullptr? Tak więc, jeśli funkcja została zwrócona nullptr_t, to kompilator już wie, która wartość zostanie zwrócona, niezależnie od treści funkcji?
Aaron McDaid
8
std::nullptr_tMożna utworzyć instancję @AaronMcDaid , ale wszystkie instancje będą identyczne, nullptrponieważ typ jest zdefiniowany jako typedef decltype(nullptr) nullptr_t. Uważam, że głównym powodem tego typu jest to, że funkcje mogą być przeciążone specjalnie w celu przechwycenia nullptr, jeśli to konieczne. Zobacz tutaj przykład.
Justin Time - Przywróć Monikę
5
0 nigdy nie był wskaźnikiem zerowym, wskaźnik zerowy jest wskaźnikiem, który można uzyskać przez rzutowanie literału zerowego na typ wskaźnika i nie wskazuje on żadnego istniejącego obiektu z definicji.
Swift - piątek Pie

Odpowiedzi:

403

Jak to jest słowo kluczowe i instancja typu?

To nie jest zaskakujące. Zarówno truei falsesą wyszukiwane i jak literały mają typ ( bool). nullptrjest literalnym wskaźnikiem typu std::nullptr_ti jest wartością (nie można użyć jego adresu &).

  • 4.10o konwersji wskaźnika mówi, że wartość typu std::nullptr_tjest stałą zerowego wskaźnika i że można przekształcić integralną stałą zerowego wskaźnika std::nullptr_t. Odwrotny kierunek jest niedozwolony. Pozwala to na przeładowanie funkcji zarówno wskaźników, jak i liczb całkowitych oraz przekazywanie w nullptrcelu wybrania wersji wskaźnika. Pomijanie NULLlub 0mylący wybór intwersji.

  • Rzutowanie nullptr_tna typ integralny wymaga reinterpret_casti ma taką samą semantykę jak rzutowanie (void*)0na typ integralny (zdefiniowano implementację odwzorowania). Nie reinterpret_castmożna przekonwertować nullptr_tna dowolny typ wskaźnika. W miarę możliwości polegaj na domniemanej konwersji lub użyj static_cast.

  • Standard wymaga, sizeof(nullptr_t)BE sizeof(void*).

Johannes Schaub - litb
źródło
Och, po spojrzeniu wydaje mi się, że operator warunkowy nie może przekonwertować wartości 0 na nullptr w takich przypadkach cond ? nullptr : 0;. Usunięto z mojej odpowiedzi.
Johannes Schaub - litb
88
Pamiętaj, że NULLnie ma takiej gwarancji 0. Może być 0L, w którym to przypadku wezwanie void f(int); void f(char *);będzie dwuznaczne. nullptrzawsze faworyzuje wersję wskaźnika i nigdy nie wywołuje tej int. Zauważ też, że nullptr można go zamienić na bool(projekt mówi, że o 4.12).
Johannes Schaub - litb
@litb: jeśli chodzi o f (int) if (void *) - czy f (0) nadal będzie dwuznaczny?
Steve Folly,
27
@ Steve, nie, to wywoła intwersję. Ale f(0L)jest niejednoznaczny, ponieważ long -> inti long -> void*oba są równie kosztowne. Jeśli więc 0Lw kompilatorze znajduje się NULL , wywołanie f(NULL)będzie dwuznaczne, biorąc pod uwagę te dwie funkcje. Nie tak z nullptroczywiście.
Johannes Schaub - litb
2
@SvenS Nie można go zdefiniować jak (void*)0w C ++. Można go jednak zdefiniować jako dowolną stałą zerową wskaźnika, którą nullptrspełnia dowolna stała całkowa o wartości 0 . Więc zdecydowanie nie będzie, ale może . (Zapomniałeś mi pingować btw ..)
Deduplicator
60

Od nullptr: Bezpieczny i wyraźny wskaźnik zerowy typu :

Nowe słowo kluczowe C ++ 09 nullptr oznacza stałą wartości, która służy jako uniwersalny literał wskaźnika zerowego, zastępując błędny i słabo wpisany literał 0 i niesławne makro NULL. W ten sposób nullptr kończy ponad 30 lat zawstydzenia, dwuznaczności i błędów. Poniższe sekcje przedstawiają funkcję nullptr i pokazują, jak można wyleczyć dolegliwości NULL i 0.

Inne referencje:

nik
źródło
17
C ++ 09? Czy przed sierpniem 2011 r. Nie był nazywany C ++ 0x?
Michael Dorst,
2
@anthropomorphic Cóż, to jest jego cel. Użyto C ++ 0x, gdy było jeszcze w toku, ponieważ nie było wiadomo, czy będzie on ukończony w 2008 czy 2009 roku. Zauważ, że faktycznie stał się C ++ 0B, ​​co oznacza C ++ 11. Zobacz stroustrup.com/C++11FAQ.html
mxmlnkn 14.04.2016
44

Dlaczego nullptr w C ++ 11? Co to jest? Dlaczego NULL nie wystarcza?

Ekspert C ++, Alex Allain, mówi doskonale tutaj (moje podkreślenie zostało pogrubione):

... wyobraź sobie, że masz dwie następujące deklaracje funkcji:

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

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

Chociaż wygląda na to, że zostanie wywołana druga funkcja - w końcu przekazujesz coś, co wydaje się wskaźnikiem - to tak naprawdę pierwsza funkcja, która zostanie wywołana! Problem polega na tym, że ponieważ NULL wynosi 0, a 0 jest liczbą całkowitą, zamiast tego zostanie wywołana pierwsza wersja func. Tego rodzaju rzeczy nie zdarzają się cały czas, ale kiedy tak się dzieje, są wyjątkowo frustrujące i mylące. Jeśli nie znasz szczegółów tego, co się dzieje, może to wyglądać jak błąd kompilatora. Funkcja języka, która wygląda jak błąd kompilatora, nie jest czymś, czego chcesz.

Wpisz nullptr. W C ++ 11 nullptr jest nowym słowem kluczowym, które może (i powinno!) Być używane do reprezentowania wskaźników NULL; innymi słowy, wszędzie tam, gdzie pisałeś NULL, powinieneś użyć nullptr. Dla programisty nie jest to dla ciebie bardziej zrozumiałe (wszyscy wiedzą, co oznacza NULL), ale jest bardziej wyraźne dla kompilatora , który nie będzie już widział, że zera wszędzie mają specjalne znaczenie, gdy są używane jako wskaźnik.

Allain kończy swój artykuł:

Niezależnie od tego wszystkiego - podstawową zasadą dla C ++ 11 jest po prostu rozpoczęcie korzystania z niego, nullptrilekroć używałbyś go NULLw przeszłości.

(Moje słowa):

Na koniec nie zapominaj, że nullptrto obiekt - klasa. Można go używać w dowolnym miejscu, w którym NULLbył używany wcześniej, ale jeśli z jakiegoś powodu potrzebujesz jego typu, można go wyodrębnić decltype(nullptr)lub opisać bezpośrednio jako std::nullptr_t, co jest po prostu jednym typedefz decltype(nullptr).

Bibliografia:

  1. Cprogramming.com: Lepsze typy w C ++ 11 - nullptr, klasy enum (wyliczenia silnie typowane) i cstdint
  2. https://en.cppreference.com/w/cpp/language/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t
Gabriel Staples
źródło
2
Muszę powiedzieć, że twoja odpowiedź jest niedoceniana, dzięki twojemu przykładowi bardzo łatwo ją zrozumieć.
ms.
37

Gdy masz funkcję, która może odbierać wskaźniki do więcej niż jednego typu, wywoływanie jej za pomocą NULLjest niejednoznaczne. Sposób, w jaki teraz to działa, jest bardzo hackerski, przyjmując int i zakładając, że jest 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); }
};

W C++11byłbyś w stanie przeciążenia nullptr_t, tak że ptr<T> p(42);byłoby błędem kompilacji zamiast run-time assert.

ptr(std::nullptr_t) : p_(nullptr)  {  }
Motti
źródło
Co jeśli NULLzostanie zdefiniowane jako 0L?
LF
9

nullptrnie może być przypisany do typu integralnego, takiego jak inttyp wskaźnika, ale tylko; albo wbudowany typ wskaźnika, jak int *ptrlub inteligentny wskaźnik, taki jakstd::shared_ptr<T>

Uważam, że jest to ważne rozróżnienie, ponieważ NULLnadal można je przypisać zarówno do typu integralnego, jak i do wskaźnika, podobnie jak NULLmakro rozwinięte, do 0którego może służyć zarówno jako wartość początkowa, intjak i wskaźnik.

użytkownik633658
źródło
Pamiętaj, że ta odpowiedź jest nieprawidłowa. NULLnie gwarantuje się rozszerzenia do 0.
LF
6

Czy masz też inny przykład (oprócz Wikipedii), w którym nullptrprzewyższa dobre stare 0?

Tak. Jest to także (uproszczony) przykład ze świata rzeczywistego, który pojawił się w naszym kodzie produkcyjnym. Wyróżniał się tylko dlatego, że gcc był w stanie wydać ostrzeżenie podczas kompilacji krzyżowej na platformę o różnej szerokości rejestru (wciąż nie jestem pewien, dlaczego tylko przy kompilacji krzyżowej z x86_64 na x86, ostrzega warning: converting to non-pointer type 'int' from NULL):

Rozważ ten kod (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;
}

Daje to wynik:

(a == pb): 1
(a == 0): 0
(a == NULL): 0
(a == null): 1
Gabriel Schreiber
źródło
Nie widzę, jak to się poprawia, używając nullptr (i C ++ 11). Jeśli ustawisz pb na nullptr, pierwsze porównanie będzie nadal prawdziwe (podczas porównywania jabłek z gruszkami ...). Drugi przypadek jest jeszcze gorszy: jeśli porównasz a do nullptr, przekształci on na B *, a następnie ponownie oceni na prawdę (przed rzutowaniem na bool i wyrażeniem na false). Cała ta rzecz przypomina mi JavaScript i zastanawiam się, czy w przyszłości otrzymamy === w C ++ :(
Nils,
5

Cóż, inne języki mają zastrzeżone słowa, które są instancjami typów. Python, na przykład:

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

Jest to właściwie dość dokładne porównanie, ponieważ Nonezwykle jest używane do czegoś, co nie zostało zainicjalizowane, ale jednocześnie porównania, takie jak None == 0fałszywe.

Z drugiej strony, w zwykłym C, NULL == 0zwróciłoby prawdę IIRC, ponieważ NULLjest to tylko makro zwracające 0, które zawsze jest nieprawidłowym adresem (AFAIK).

Mark Rushakoff
źródło
4
NULLto makro, które rozwija się do zera, stałe zerowanie rzutowane na wskaźnik daje wskaźnik zerowy. Wskaźnik zerowy nie musi być zerowy (ale często jest), zero nie zawsze jest nieprawidłowym adresem, a niestałe zero rzutowane na wskaźnik nie musi być zerowe, a wskaźnik zerowy rzutowany na liczba całkowita nie musi wynosić zero. Mam nadzieję, że wszystko w porządku, nie zapominając o niczym. Odniesienie: c-faq.com/null/null2.html
Samuel Edwin Ward
3

Jest to słowo kluczowe, ponieważ standard określa je jako takie. ;-) Zgodnie z najnowszym publicznym projektem (n2914)

2.14.7 Literały wskaźnika [lex.nullptr]

pointer-literal:
nullptr

Literał wskaźnika jest słowem kluczowym nullptr. Jest to wartość typu std::nullptr_t.

Jest to użyteczne, ponieważ nie jest domyślnie konwertowane na wartość całkowitą.

KTC
źródło
2

Powiedzmy, że masz funkcję (f), która jest przeciążona, aby przyjąć zarówno int, jak i char *. W wersjach wcześniejszych niż C ++ 11, jeśli chcesz wywołać go wskaźnikiem zerowym i użyjesz wartości NULL (tj. Wartości 0), wtedy wywołałbyś tę przeciążoną dla int:

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

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

Prawdopodobnie nie tego chciałeś. C ++ 11 rozwiązuje to za pomocą nullptr; Teraz możesz napisać:

void g()
{
  f(nullptr); //calls f(char*)
}
Amit G.
źródło
1

Pozwól, że najpierw przedstawię ci implementację nieskomplikowanych 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;

nullptrjest subtelnym przykładem idiomu typu zwracanego typu, aby automatycznie wydedukować pusty wskaźnik poprawnego typu w zależności od typu instancji, do której przypisuje.

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • Jak można powyżej, gdy nullptrjest przypisywany wskaźnikowi liczby całkowitej, inttworzona jest instancja typu funkcji konwersji opartej na szablonie. To samo dotyczy wskaźników metod.
  • W ten sposób, wykorzystując funkcjonalność szablonu, za każdym razem tworzymy odpowiedni typ wskaźnika zerowego, nowe przypisanie typu.
  • Ponieważ nullptrliterał jest liczbą całkowitą o wartości zero, nie można użyć jego adresu, który osiągnęliśmy, usuwając i operator.

Dlaczego nullptrprzede wszystkim potrzebujemy ?

  • Widzisz, że tradycyjny NULLma z tym jakiś problem, jak poniżej:

1️⃣ Domniemana konwersja

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

2️⃣ Dwuznaczność wywoływania funkcji

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

func(NULL);     // Which one to call?
  • Kompilacja powoduje następujący błąd:
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️⃣ Przeciążenie konstruktora

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

String s1( NULL );
String s2( 5 );
  • W takich przypadkach potrzebujesz jawnej obsady (tj  String s((char*)0)).
Vishal Chovatiya
źródło
0

0 było jedyną liczbą całkowitą, która mogła być używana jako inicjalizator bez rzutowania dla wskaźników: nie można inicjować wskaźników z innymi wartościami całkowitymi bez rzutowania. Możesz uznać 0 za singleton consexpr składniowo podobny do literału liczby całkowitej. Może zainicjować dowolny wskaźnik lub liczbę całkowitą. Ale, co zaskakujące, przekonasz się, że nie ma wyraźnego typu: jest int. Dlaczego więc 0 może inicjować wskaźniki, a 1 nie? Praktyczną odpowiedzią było to, że potrzebujemy sposobu definiowania wartości zerowej wskaźnika, a bezpośrednia niejawna konwersja intna wskaźnik jest podatna na błędy. W ten sposób 0 stał się prawdziwą dziwaczną bestią z czasów prehistorycznych. nullptrzostał zaproponowany jako rzeczywista reprezentacja wartości zerowej constexpr dla inicjalizacji wskaźników. Nie można go użyć do bezpośredniej inicjalizacji liczb całkowitych i wyeliminować dwuznaczności związane z definiowaniem.NULL w kategoriach 0.nullptrmożna go zdefiniować jako bibliotekę przy użyciu standardowej składni, ale semantycznie wygląda na brakujący składnik podstawowy. NULLjest teraz przestarzałe na korzyść nullptr, chyba że jakaś biblioteka zdecyduje się go zdefiniować jako nullptr.

Red.Wave
źródło
-1

Oto nagłówek 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

(wiele można szybko odkryć grep -r /usr/include/*`)

Jedną rzeczą, która wyskakuje, jest *przeciążenie operatora (zwracanie 0 jest o wiele bardziej przyjazne niż segfault ...). Inną rzeczą jest to, że nie wygląda zgodny z przechowywaniem adresu w ogóle . Co, w porównaniu do tego, w jaki sposób sling void * i przekazywanie wyników NULL do normalnych wskaźników jako wartości wartowników, oczywiście zmniejszyłoby czynnik „nigdy nie zapominaj, może to być bomba”.

Łk
źródło
-2

Wartość NULL nie musi być równa 0. Jeśli używasz zawsze wartości NULL, a nigdy 0, NULL może mieć dowolną wartość. Zakładając, że programujesz mikrokontroler von Neumana z płaską pamięcią, który ma swoje przerywacze na poziomie 0. Jeśli NULL wynosi 0 i coś pisze pod wskaźnikiem NULL, mikrokontroler ulega awarii. Jeśli NULL to powiedzmy 1024, a przy 1024 jest zmienna zarezerwowana, zapis nie spowoduje jej awarii i możesz wykryć przypisania wskaźnika NULL z wnętrza programu. Jest to bezcelowe na komputerach PC, ale w przypadku sond kosmicznych, sprzętu wojskowego lub medycznego ważne jest, aby nie upaść.

Axel Schweiß
źródło
2
Cóż, rzeczywista wartość wskaźnika zerowego w pamięci może nie wynosić zero, ale standard C (i C ++) nakazuje kompilatorom konwersję całki zerowej na literę zerową.
bzim