Что именно является nullptr?

570

Теперь у нас есть C ++ 11 со многими новыми функциями. Интересным и запутанным (по крайней мере для меня) является новое nullptr.

Ну, не надо больше для мерзкого макроса NULL.

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

Тем не менее, я не понимаю, как nullptrработает. Например, статья в Википедии гласит:

C ++ 11 исправляет это, вводя новое ключевое слово, служащее отличительной константой нулевого указателя: nullptr. Он имеет тип nullptr_t , который неявно конвертируем и сопоставим с любым типом указателя или указателем на член. Он не является неявно конвертируемым или сопоставимым с целочисленными типами, за исключением bool.

Как это ключевое слово и экземпляр типа?

Кроме того, у вас есть еще один пример (помимо Википедии), где nullptrон превосходит старый добрый 0?

арак
источник
23
Связанный факт: nullptrтакже используется для представления нулевой ссылки для управляемых дескрипторов в C ++ / CLI.
Мердад Афшари
3
При использовании Visual C ++ помните, что если вы используете nullptr с собственным кодом C / C ++, а затем компилируете с параметром компилятора / clr, компилятор не может определить, указывает ли nullptr собственное или управляемое значение нулевого указателя. Чтобы сделать ваше намерение понятным для компилятора, используйте nullptr для указания управляемого значения или __nullptr для указания собственного значения. Microsoft реализовала это как расширение компонента.
cseder
6
Является ли nullptr_tгарантировано иметь только один элемент, nullptr? Итак, если функция вернулась nullptr_t, то компилятор уже знает, какое значение будет возвращено, независимо от тела функции?
Аарон МакДейд
8
std::nullptr_tМожно создать экземпляр @AaronMcDaid , но все экземпляры будут идентичны, nullptrпоскольку тип определяется как typedef decltype(nullptr) nullptr_t. Я считаю, что основная причина существования типа заключается в том, что функции могут быть перегружены специально для перехвата nullptr, если это необходимо. Смотрите здесь для примера.
Джастин Тайм - Восстановить Монику
5
0 никогда не был нулевым указателем, нулевой указатель - это указатель, который можно получить, приведя нулевой литерал к типу указателя, и он не указывает ни на какой существующий объект по определению.
Свифт - пятничный пирог

Ответы:

403

Как это ключевое слово и экземпляр типа?

Это не удивительно. Оба trueи falseявляются ключевыми словами и в качестве литералов они имеют тип ( bool). nullptrявляется литералом-указателем типа std::nullptr_t, и это значение (вы не можете получить его адрес, используя &).

  • 4.10О преобразовании указателя говорит, что значение типа std::nullptr_tявляется константой нулевого указателя и что целая константа нулевого указателя может быть преобразована в std::nullptr_t. Противоположное направление не допускается. Это позволяет перегрузить функцию как для указателей, так и для целых чисел, и передать ее nullptrдля выбора версии указателя. Передача NULLили 0будет сбивать с толку выбрал intверсию.

  • Приведение nullptr_tк целочисленному типу требует reinterpret_castи имеет ту же семантику, что и приведение (void*)0к целочисленному типу (определена реализация отображения). Не reinterpret_castможет преобразовать nullptr_tв любой тип указателя. Положитесь на неявное преобразование, если это возможно, или используйте static_cast.

  • Стандарт требует, чтобы это sizeof(nullptr_t)было sizeof(void*).

Йоханнес Шауб - Литб
источник
О, после просмотра мне кажется, что условный оператор не может преобразовать 0 в nullptr в таких случаях, как cond ? nullptr : 0;. Удалено из моего ответа.
Йоханнес Шауб -
88
Обратите внимание, что NULLэто даже не гарантируется 0. Может быть 0L, в этом случае вызов void f(int); void f(char *);будет неоднозначным.nullptrвсегда будет поддерживать версию указателя и никогда не вызывать ее int. Также обратите внимание , что nullptr это конвертируется в bool(проект говорит , что в 4.12).
Йоханнес Шауб -
@litb: так что касается f (int) и f (void *) - будет ли f (0) все еще неоднозначным?
Стив Фолли
27
@ Стив, нет, это вызовет int версию. Но f(0L)это неоднозначно, потому long -> intчто long -> void*так же, как и одинаково дорого. Так что, если 0Lв вашем компиляторе есть NULL , то вызов f(NULL)будет двусмысленным, учитывая эти две функции. Конечно, не так nullptr.
Йоханнес Шауб -
2
@SvenS Он не должен быть определен как (void*)0в C ++. Но его можно определить как любую произвольную константу нулевого указателя, которую любая целочисленная константа со значением 0 и nullptrвыполнит. Итак, определенно нет будет, но может . (Вы забыли пинговать меня между прочим ..)
Дедупликатор
60

От nullptr: типобезопасный и четкий нулевой указатель :

Новое ключевое слово C ++ 09 nullptr обозначает константу rvalue, которая служит универсальным литералом нулевого указателя, заменяя ошибочный и слабо типизированный литерал 0 и печально известный макрос NULL. Таким образом, nullptr положил конец более чем 30 годам смущения, двусмысленности и ошибок. В следующих разделах представлено средство nullptr и показано, как оно может исправить недуги NULL и 0.

Другие ссылки:

Nik
источник
17
C ++ 09? Разве это не упоминалось как C ++ 0x до августа 2011 года?
Майкл Дорст
2
@anthropomorphic Ну, это его цель. C ++ 0x использовался, пока он еще находился в процессе разработки, потому что не было известно, будет ли он закончен в 2008 или 2009 году. Обратите внимание, что на самом деле он стал C ++ 0B, ​​что означает C ++ 11. См. Stroustrup.com/C++11FAQ.html
mxmlnkn
45

Почему nullptr в C ++ 11? Что это? Почему NULL недостаточно?

Эксперт по С ++ Алекс Аллен прекрасно говорит здесь (мой акцент выделен жирным шрифтом):

... представьте, что у вас есть следующие два объявления функций:

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

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

Хотя, похоже, будет вызвана вторая функция - в конце концов, вы передаете то, что кажется указателем - это действительно первая функция, которая будет вызвана! Проблема в том, что, поскольку NULL равен 0, а 0 - целое число, вместо него будет вызвана первая версия func. Это такая вещь, которая, да, не случается все время, но когда это происходит, это чрезвычайно расстраивает и сбивает с толку. Если вы не знаете подробностей происходящего, это может выглядеть как ошибка компилятора. Функция языка, которая выглядит как ошибка компилятора, ну, не то, что вам нужно.

Введите nullptr. В C ++ 11 nullptr - это новое ключевое слово, которое можно (и нужно!) Использовать для представления указателей NULL; другими словами, где бы вы ни писали ранее NULL, вы должны использовать вместо него nullptr. Это не более понятно для вас, программист , (все знают, что означает NULL), но это более явно для компилятора , который больше не будет видеть везде, где используются 0, чтобы иметь особое значение при использовании в качестве указателя.

Аллен заканчивает свою статью:

Независимо от всего этого - практическое правило для C ++ 11 - просто начинать использовать nullptrвсякий раз, когда вы в противном случае использовали бы его NULLв прошлом.

(Мои слова):

Наконец, не забывайте, что nullptrэто объект - класс. Он может быть использован в любом месте NULLиспользовалась раньше, но если вам нужно его тип по какой - то причине, это тип можно экстрагировать decltype(nullptr)или непосредственно , как описано std::nullptr_t, что это просто typedefиз decltype(nullptr).

Ссылки:

  1. Cprogramming.com: лучшие типы в C ++ 11 - nullptr, перечислимые классы (строго типизированные перечисления) и cstdint
  2. https://en.cppreference.com/w/cpp/language/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t
Габриэль Стейплс
источник
2
Я должен сказать, что ваш ответ недооценен, это было очень легко понять на вашем примере.
ПСС
37

Если у вас есть функция, которая может получать указатели на несколько типов, вызывать ее с помощью NULLнеоднозначно. То, как это обходится сейчас, очень хакерское, принимая int и предполагая, что это так 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); }
};

В результате C++11вы могли бы перегружаться, nullptr_tтак что ptr<T> p(42);это будет ошибка во время компиляции, а не во время выполнения assert.

ptr(std::nullptr_t) : p_(nullptr)  {  }
Моти
источник
Что если NULLопределяется как 0L?
LF
9

nullptrнельзя присвоить целочисленному типу, такому как intтип указателя; либо встроенный тип указателя, такой какint *ptr или интеллектуальный указатель, такой какstd::shared_ptr<T>

Я полагаю, что это важное различие, потому что NULLвсе еще может быть назначено как целочисленному типу, так и указателю, так как NULLэто макрос, расширенный до 0которого может служить как начальным значением для intуказателя, так и указателем.

user633658
источник
Обратите внимание, что этот ответ неверен. NULLне гарантируется расширение до 0.
LF
6

Кроме того, у вас есть другой пример (помимо Википедии), где nullptrон превосходит старый добрый 0?

Да. Это также (упрощенный) реальный пример из нашего производственного кода. Он выделялся только потому, что gcc смог выдать предупреждение при кросс-компиляции на платформу с другой шириной регистра (но точно не уверен, почему только при кросс-компиляции с x86_64 до x86 предупреждает warning: converting to non-pointer type 'int' from NULL):

Рассмотрим этот код (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;
}

Это дает такой вывод:

(a == pb): 1
(a == 0): 0
(a == NULL): 0
(a == null): 1
Габриэль Шрайбер
источник
Я не вижу, как это улучшается при использовании nullptr (и C ++ 11). Если вы установите для pb значение nullptr, первое сравнение будет по-прежнему истинным (при сравнении яблок с грушами ..). Второй случай еще хуже: если вы сравните a с nullptr, он преобразует a в B *, а затем снова оценивается как true (до того, как его преобразовали в bool, а expr оценили в false). Вся эта штука напоминает мне JavaScript, и мне интересно, получим ли мы === в C ++ в будущем :(
Nils
5

Ну, в других языках есть зарезервированные слова, которые являются экземплярами типов. Python, например:

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

Это на самом деле довольно близкое сравнение, потому что Noneобычно используется для чего-то, что не было инициализировано, но в то же время сравнения, такие как None == 0ложные.

С другой стороны, в простом C, NULL == 0вернул бы true IIRC, потому что NULLэто просто макрос, возвращающий 0, который всегда является неверным адресом (AFAIK).

Марк Рушаков
источник
4
NULLэто макрос, который расширяется до нуля, константный ноль, приведенный к указателю, создает нулевой указатель. Нулевой указатель не обязательно должен быть нулем (но часто это так), ноль не всегда является недопустимым адресом, и непостоянный ноль, приведенный к указателю, не должен быть нулевым, а нулевой указатель приведен к целое число не должно быть нулем. Надеюсь, я все понял, ничего не забыв. Ссылка: c-faq.com/null/null2.html
Сэмюэль Эдвин Уорд
3

Это ключевое слово, потому что стандарт будет указывать его как таковой. ;-) Согласно последнему публичному проекту (n2914)

2.14.7 Литералы указателя [lex.nullptr]

pointer-literal:
nullptr

Указатель литерал является ключевым словом nullptr. Это значение типа std::nullptr_t.

Это полезно, поскольку неявно преобразуется в целочисленное значение.

КТС
источник
2

Допустим, у вас есть функция (f), которая перегружена и принимает как int, так и char *. До C ++ 11, если вы хотели вызвать его с нулевым указателем и использовали NULL (т. Е. Значение 0), вы бы вызвали перегруженный для int:

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

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

Это, вероятно, не то, что вы хотели. C ++ 11 решает это с помощью nullptr; Теперь вы можете написать следующее:

void g()
{
  f(nullptr); //calls f(char*)
}
Амит Г.
источник
1

Позвольте мне сначала дать вам реализацию простой 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Тонкий пример идиомы Resolver Return Type для автоматического вывода нулевого указателя правильного типа в зависимости от типа экземпляра, которому он назначается.

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • Как вы можете выше, когда nullptrназначается целочисленный указатель, создается intэкземпляр типа для функции преобразования шаблонов. И то же самое относится и к указателям на метод.
  • Таким образом, используя функциональность шаблона, мы фактически создаем соответствующий тип нулевого указателя при каждом новом назначении типа.
  • Так nullptrкак это целочисленный литерал со значением ноль, вы не можете использовать его адрес, который мы сделали, удалив & оператор.

Зачем нам нужно nullptrв первую очередь?

  • Вы видите, что у традиционного NULLесть некоторая проблема с этим как ниже:

1️⃣ Неявное преобразование

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

2️⃣ Неоднозначность вызова функций

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

func(NULL);     // Which one to call?
  • Компиляция выдает следующую ошибку:
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️⃣ Перегрузка конструктора

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

String s1( NULL );
String s2( 5 );
  • В таких случаях вам нужно явное приведение (т String s((char*)0)). Е. 
Вишал Човатия
источник
0

Раньше 0 было единственным целочисленным значением, которое могло использоваться в качестве инициализатора без приведения для указателей: вы не можете инициализировать указатели с другими целочисленными значениями без приведения. Вы можете рассматривать 0 как одноэлементное выражение, синтаксически похожее на целочисленный литерал. Может инициировать любой указатель или целое число. Но удивительно, вы обнаружите, что он не имеет четкого типа: это int. Так почему же 0 может инициализировать указатели, а 1 - нет? Практический ответ состоял в том, что нам нужны средства определения нулевого значения указателя, а прямое неявное преобразование intв указатель подвержено ошибкам. Таким образом 0 стал настоящим чудовищным чудаком из доисторической эпохи. nullptrбыло предложено быть реальным одноэлементным представлением constexpr нулевого значения для инициализации указателей. Его нельзя использовать для непосредственной инициализации целых чисел, а устранение неоднозначностей, связанных с определением, можно определить как библиотеку с использованием синтаксиса std, но семантически выглядело как отсутствующий основной компонент. в настоящее время устарела в пользу , если какая-то библиотека не решит определить его как .NULL в терминах 0.nullptrNULLnullptrnullptr

Red.Wave
источник
-1

Вот заголовок 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

(многое можно найти быстро grep -r /usr/include/*`)

Одна вещь, которая выскакивает, это *перегрузка оператора (возвращение 0 намного удобнее, чем segfaulting ...). Другое дело, что он не выглядит совместимым с хранением адреса вообще . Который, по сравнению с тем, как он обрабатывает void * и передает результаты NULL в обычные указатели в качестве значений часовых, очевидно, уменьшит фактор «никогда не забывай, это может быть бомба».

лк
источник
-2

NULL не обязательно должен быть 0. Пока вы используете всегда NULL и никогда не 0, NULL может принимать любое значение. Предполагается, что вы программируете микроконтроллер von Neuman с плоской памятью, вектор прерываний которого равен 0. Если NULL равен 0 и что-то записывает в нулевой указатель, микроконтроллер выходит из строя. Если NULL, скажем, 1024, а в 1024 есть зарезервированная переменная, запись не приведет к сбою, и вы можете обнаружить назначения NULL Pointer изнутри программы. Это бессмысленно для ПК, но для космических зондов, военной или медицинской техники важно не разбиться.

Аксель Швейс
источник
2
Ну, фактическое значение нулевого указателя в памяти может не быть нулевым, но стандарт C (и C ++) обязывает компиляторы преобразовывать целочисленный литерал 0 в нулевой указатель.
bzim