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

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

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

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

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

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

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

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

11 ответов

Решение

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

Это не удивительно. И то и другое 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*),

Почему 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, чтобы иметь особое значение при использовании в качестве указателя."

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

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

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

Если у вас есть функция, которая может получать указатели на несколько типов, вызывая ее с помощью 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)  {  }

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

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

Кроме того, у вас есть еще один пример (кроме Википедии), где 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

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

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

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

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

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

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

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

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

pointer-literal:
nullptr

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

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

Допустим, у вас есть функция (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*)
}

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

Согласно cppreference, nullptr ключевое слово, которое:

обозначает литерал указателя. Это значение типаstd::nullptr_t. Существуют неявные преобразования из nullptr в значение нулевого указателялюбого типа указателя и любого указателя на тип члена. Подобные преобразования существуют для любой константы нулевого указателя, которая включает значения типаstd::nullptr_t а также макрос NULL.

nullptrбудет неявно преобразовывать в любой тип указателя, но не в целое число. NULLоднако это макрос и константа нулевого указателя, определяемая реализацией. Часто это определяется так:

#define NULL 0

т.е. целое число.

Отсюда:

int i = NULL;     //OK
int i = nullptr;  //error
int* p = NULL;    //OK
int* p = nullptr; //OK

nullptr можно избежать двусмысленности, если у вас есть две перегрузки функций, например:

void func(int x);   //1)
void func(int* x);  //2)

func(NULL) вызывает 1) потому что NULL целое число.func(nullptr) вызывает 2) потому что nullptr не является целым числом и неявно преобразуется в любой тип указателя.

Преимущества использования nulptr:

  • избегать двусмысленности между перегрузками функций
  • позволяет вам делать специализацию шаблона
  • более безопасный, интуитивно понятный и выразительный код, например if (ptr == nullptr) вместо if (ptr == 0)

Вот заголовок 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 в обычные указатели в качестве значений часовых, очевидно, уменьшит фактор "никогда не забывай, это может быть бомба".

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

Другие вопросы по тегам