Каковы преимущества использования nullptr?

163

Этот фрагмент кода концептуально делает то же самое для трех указателей (безопасная инициализация указателя):

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

Итак, каковы преимущества назначения указателей nullptrперед присвоением им значений NULLили 0?

Марк Гарсия
источник
39
Во-первых, перегруженная функция берет intи void *не будет выбирать intверсию над void *версией при использовании nullptr.
Крис
2
Ну f(nullptr)отличается от f(NULL). Но что касается вышеприведенного кода (присваивания локальной переменной), все три указателя абсолютно одинаковы. Единственное преимущество - читаемость кода.
Балки
2
Я за то, чтобы сделать это FAQ, @Prasoon. Спасибо!
СБИ
1
NB NULL исторически не гарантируется равным 0, но равен oc C99, во многом так же, как байт не обязательно должен быть длиной 8 битов, а истина и ложь были зависимыми от архитектуры значениями. Этот вопрос фокусируется на том nullptr, что разница между 0 иNULL
awiebe

Ответы:

180

В этом коде, кажется, нет преимущества. Но рассмотрим следующие перегруженные функции:

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

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

Какая функция будет вызвана? Конечно, намерение здесь заключается в том, чтобы позвонить f(char const *), но на самом деле f(int)будет называться! Это большая проблема 1 , не так ли?

Итак, решение таких проблем заключается в использовании nullptr:

f(nullptr); //first function is called

Конечно, это не единственное преимущество nullptr. Вот еще один:

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

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

Поскольку в шаблоне тип nullptrвыводится как nullptr_t, так что вы можете написать это:

template<typename T>
void f(T *ptr);   //function to handle non-nullptr argument

void f(nullptr_t); //an overload to handle nullptr argument!!!

1. В C ++ NULLопределяется как #define NULL 0, так оно и есть в основном int, поэтому и f(int)вызывается.

Наваз
источник
1
Как сказал Мерадад, такие перегрузки встречаются довольно редко. Есть ли другие важные преимущества nullptr? (Нет. Я не требую)
Марк Гарсия
2
@MarkGarcia, это может быть полезно: stackoverflow.com/questions/13665349/…
chris
9
Ваша сноска кажется задом наперед. NULLСтандарт требует наличия целочисленного типа, поэтому он обычно определяется как 0или 0L. Также я не уверен, что мне нравится эта nullptr_tперегрузка, так как она перехватывает только вызовы nullptr, а не нулевой указатель другого типа, например (void*)0. Но я могу поверить, что у него есть несколько применений, даже если все, что он делает, это сохраняет вас, определяя свой собственный однозначный тип заполнителя, чтобы он означал «нет».
Стив Джессоп
1
Другое преимущество (хотя и незначительное) может иметь то, что nullptrимеет четко определенное числовое значение, тогда как константы нулевого указателя - нет. Константа нулевого указателя преобразуется в нулевой указатель этого типа (что бы это ни было). Требуется, чтобы два нулевых указателя одного типа сравнивались одинаково, а логическое преобразование превращает нулевой указатель в false. Больше ничего не требуется. Следовательно, компилятор может (глупо, но возможно) использовать, например, 0xabcdef1234или другое число для нулевого указателя. С другой стороны, nullptrтребуется преобразовать в числовой ноль.
Деймон
1
@DeadMG: что не так в моем ответе? что f(nullptr)не будет вызывать намеченную функцию? Было больше чем одна мотивация. Многие другие полезные вещи могут быть обнаружены самими программистами в ближайшие годы. Таким образом , вы не можете сказать , что есть только один истинный использование в nullptr.
Наваз
87

C ++ 11 представляет nullptr, она известна как Nullконстанта указателя и повышает безопасность типов и разрешает неоднозначные ситуации в отличие от существующей константы нулевого указателя, зависящей от реализации NULL. Чтобы быть в состоянии понять преимущества nullptr. Сначала нам нужно понять, что есть NULLи с чем связаны проблемы.


Что NULLименно?

Предварительно C ++ 11 NULLиспользовался для представления указателя, который не имеет значения или указателя, который не указывает на что-либо допустимое. Вопреки распространенному мнению, NULLэто не ключевое слово в C ++ . Это идентификатор, определенный в заголовках стандартной библиотеки. Короче говоря, вы не можете использовать NULLбез включения некоторых стандартных библиотечных заголовков. Рассмотрим пример программы :

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

Вывод:

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

Стандарт C ++ определяет NULL как определенный макрос реализации, определенный в определенных заголовочных файлах стандартной библиотеки. Происхождение NULL происходит от C, а C ++ унаследовал его от C. Стандарт C определил NULL как 0или (void *)0. Но в C ++ есть небольшая разница.

C ++ не может принять эту спецификацию как есть. В отличие от C, C ++ является языком со строгой типизацией (C не требует явного приведения void*к любому типу, в то время как C ++ требует явного приведения). Это делает определение NULL, заданное стандартом C, бесполезным во многих выражениях C ++. Например:

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

Если NULL был определен как (void *)0, ни одно из приведенных выше выражений не будет работать.

  • Случай 1: не будет компилироваться, потому что требуется автоматическое приведение от void *к std::string.
  • Случай 2: не будет компилироваться, потому что void *требуется приведение от к указателю на функцию-член.

Таким образом, в отличие от C, C ++ Standard обязывает определять NULL как числовой литерал 0или 0L.


Так зачем же нужна еще одна константа нулевого указателя, когда она у нас NULLуже есть ?

Хотя комитет по стандартам C ++ разработал определение NULL, которое будет работать для C ++, у этого определения была своя собственная доля проблем. NULL работал достаточно хорошо почти для всех сценариев, но не для всех. Это дало неожиданные и ошибочные результаты для некоторых редких сценариев. Например :

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

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

Вывод:

In Int version

Ясно, что намерение, похоже, состоит в том, чтобы вызвать версию, которая принимает char*в качестве аргумента, но так как вывод показывает, что вызывается функция, которая принимает intверсию. Это потому, что NULL является числовым литералом.

Кроме того, поскольку определяется реализацией, равен ли NULL 0 или 0L, может возникнуть путаница в разрешении перегрузки функции.

Пример программы:

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

Анализируя приведенный фрагмент:

  • Случай 1: звонки, doSomething(char *)как и ожидалось.
  • Случай 2: звонки, doSomething(int)но, возможно, char*версия была желательна, потому что 0IS также нулевой указатель.
  • Случай 3: Если NULLон определен как 0, вызовы, doSomething(int)когда, возможно, doSomething(char *)были предназначены, возможно, приводит к логической ошибке во время выполнения. Если NULLон определен как 0L, вызов является неоднозначным и приводит к ошибке компиляции.

Таким образом, в зависимости от реализации один и тот же код может давать различные результаты, что явно нежелательно. Естественно, комитет по стандартам C ++ хотел исправить это, и это является основной мотивацией для nullptr.


Так что же nullptrи как избежать проблем NULL?

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

Свойство 1: оно имеет свой собственный тип std::nullptr_t, а
свойство 2: оно неявно конвертируемо и сопоставимо с любым типом указателя или указателем на член-тип, но
свойство 3: оно не является неявно конвертируемым или сопоставимым с целочисленными типами, за исключением bool.

Рассмотрим следующий пример:

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

В вышеуказанной программе

  • Случай 1: ОК - Свойство 2
  • Случай 2: не в порядке - свойство 3
  • Случай 3: ОК - Собственность 3
  • Случай 4: Нет путаницы - char *версия Calls , собственность 2 и 3

Таким образом, введение nullptr позволяет избежать всех проблем старого доброго NULL.

Как и где вы должны использовать nullptr?

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


Стандартные ссылки:

Стандарт C ++ 11: C.3.2.4 Макрос NULL
Стандарт C ++ 11: 18.2 Типы
C ++ 11 Стандарт: 4.10 Преобразование указателей
Стандарт C99: 6.3.2.3 Указатели

Alok Save
источник
Я уже практикую ваш последний совет, так как знаю nullptr, хотя я не знал, какое это имеет значение для моего кода. Спасибо за отличный ответ и особенно за усилия. Я пролил много света на эту тему.
Марк Гарсия
msgstr "в некоторых стандартных заголовочных файлах библиотеки." -> почему бы просто не написать "cstddef" с самого начала?
mxmlnkn
Почему мы должны позволить nullptr быть конвертируемым в тип bool? Не могли бы вы рассказать подробнее?
Роберт Ван
... использовался для представления указателя, который не имеет значения ... Переменные всегда имеют значение. Это может быть шум или 0xccccc...., но переменная без значения является внутренним противоречием.
3Dave
«Случай 3: ОК - Свойство 3» (строка bool flag = nullptr;). Нет, не ОК, я получаю следующую ошибку во время компиляции с g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Георг,
23

Настоящая мотивация здесь - идеальная пересылка .

Рассматривать:

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

Проще говоря, 0 - это специальное значение , но значения не могут распространяться через системные типы. Функции пересылки очень важны, и 0 не может справиться с ними. Таким образом, было абсолютно необходимо ввести nullptr, где тип является тем, что является особенным, и тип действительно может распространяться. Фактически, команда MSVC должна была представить nullptrдосрочно после того, как они реализовали rvalue ссылки, а затем обнаружили эту ловушку для себя.

Есть еще несколько угловых случаев, где nullptrможно облегчить жизнь, но это не основной случай, поскольку актеры могут решить эти проблемы. Рассматривать

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

Вызывает две отдельные перегрузки. Кроме того, рассмотрим

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

Это неоднозначно. Но, с nullptr, вы можете предоставить

void f(std::nullptr_t)
int main() { f(nullptr); }
щенок
источник
7
Веселая. Половина ответа совпадает с двумя другими ответами, которые, по вашему мнению, являются «совершенно неправильными» ответами !!!
Наваз
Проблема переадресации также может быть решена с помощью приведения. forward((int*)0)работает. Я что-то упускаю?
jcsahnwaldt восстановить Монику
5

Основы nullptr

std::nullptr_tявляется типом литерала нулевого указателя, nullptr. Это prvalue / rvalue типа std::nullptr_t. Существуют неявные преобразования из nullptr в нулевое значение указателя любого типа указателя.

Литерал 0 - это целое число, а не указатель. Если C ++ обнаруживает, что смотрит на 0 в контексте, где может использоваться только указатель, он неохотно интерпретирует 0 как нулевой указатель, но это запасная позиция. Основная политика C ++ заключается в том, что 0 - это int, а не указатель.

Преимущество 1 - устранение неоднозначности при перегрузке указателя и целочисленных типов

В C ++ 98 основной причиной этого было то, что перегрузка указателей и целочисленных типов может привести к неожиданностям. Передача 0 или NULL таким перегрузкам никогда не вызывает перегрузку указателя:

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

Интересным в этом вызове является противоречие между кажущимся значением исходного кода («я называю fun с помощью NULL-нулевого указателя») и его фактическим значением («я вызываю fun с каким-то целым числом, а не нулевым»). указатель").

Преимущество nullptr в том, что он не имеет целочисленного типа. Вызов перегруженной функции fun с nullptr вызывает перегрузку void * (т. Е. Перегрузку указателя), потому что nullptr не может рассматриваться как что-то целое:

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

Использование nullptr вместо 0 или NULL позволяет избежать неожиданностей при перегрузке.

Еще одно преимущество по nullptrсравнению NULL(0)с использованием авто для типа возврата

Например, предположим, что вы столкнулись с этим в кодовой базе:

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

Если вы случайно не знаете (или не можете легко выяснить), что возвращает findRecord, может быть неясно, является ли тип результата указателем или целым типом. В конце концов, 0 (какой результат проверяется) может пойти в любом случае. Если вы видите следующее, с другой стороны,

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

нет никакой двусмысленности: результат должен быть указателем типа.

Преимущество 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;
}

Выше программа компилируется и выполняется успешно, но lockAndCallF1, lockAndCallF2 и lockAndCallF3 имеют избыточный код. Жаль писать такой код, если мы можем написать шаблон для всех этих lockAndCallF1, lockAndCallF2 & lockAndCallF3. Так что это можно обобщить с помощью шаблона. Я написал функцию шаблона lockAndCallвместо множественного определения lockAndCallF1, lockAndCallF2 & lockAndCallF3для избыточного кода.

Код пересчитан, как показано ниже:

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

Подробный анализ , почему компиляция не удалось по lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)неlockAndCall(f3, f3m, nullptr)

Почему сборка lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)не удалась?

Проблема в том, что когда 0 передается в lockAndCall, вычитается тип шаблона, чтобы выяснить его тип. Тип 0 - это int, так что это тип параметра ptr внутри экземпляра этого вызова lockAndCall. К сожалению, это означает, что при вызове func внутри lockAndCall передается int, что несовместимо с ожидаемым std::shared_ptr<int>параметром f1. 0, переданный в вызове, lockAndCallпредназначался для представления нулевого указателя, но в действительности было передано int. Попытка передать это int в f1 как std::shared_ptr<int>ошибка типа. Вызов lockAndCallс 0 завершается неудачно, потому что внутри шаблона int передается функции, которая требует std::shared_ptr<int>.

Анализ для вызова, NULLпо сути, такой же. Когда NULLпередается lockAndCall, целочисленный тип выводится для параметра ptr, и ошибка типа возникает, когда ptrпередается - тип int или int-like f2, который ожидает получить a std::unique_ptr<int>.

В отличие от вызова с участием nullptrне имеет проблем. Когда nullptrпередается lockAndCall, тип для ptrвыводится как std::nullptr_t. Когда ptrпередается f3, есть неявное преобразование из std::nullptr_tв int*, потому что std::nullptr_tнеявно преобразуется во все типы указателей.

Рекомендуется, если вы хотите сослаться на пустой указатель, используйте nullptr, а не 0 или NULL.

Аджай Ядав
источник
4

Нет прямого преимущества nullptrв том, как вы показали примеры.
Но рассмотрим ситуацию, когда у вас есть 2 функции с одинаковым именем; 1 дубль intи еще одинint*

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

Если вы хотите позвонить foo(int*), передав NULL, то путь:

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

nullptrделает его более простым и интуитивно понятным :

foo(nullptr);

Дополнительная ссылка с веб-страницы Бьярне.
Не имеет значения, но на стороне C ++ 11 примечание:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)
iammilind
источник
3
Для справки decltype(nullptr)есть std::nullptr_t.
Крис
2
@MarkGarcia, насколько я знаю, это полноценный тип.
Крис
5
@MarkGarcia, это интересный вопрос. cppreference имеет: typedef decltype(nullptr) nullptr_t;. Я думаю, я могу посмотреть в стандарте. Ах, нашел его: Примечание: std :: nullptr_t - это отдельный тип, который не является ни типом указателя, ни указателем на тип члена; скорее, значение этого типа является константой нулевого указателя и может быть преобразовано в значение нулевого указателя или значение указателя нулевого элемента.
Крис
2
@DeadMG: было больше чем одна мотивация. Многие другие полезные вещи могут быть обнаружены самими программистами в ближайшие годы. Таким образом , вы не можете сказать , что есть только один истинный использование в nullptr.
Наваз
2
@DeadMG: Но вы сказали, что этот ответ «совершенно неправильный» просто потому, что он не говорит об «истинной мотивации», о которой вы говорили в своем ответе. Мало того, что этот ответ (и мой тоже) получил от вас отрицательный ответ.
Наваз
4

Как уже говорили другие, его основное преимущество заключается в перегрузках. И хотя явные intперегрузки по сравнению с указателями могут быть редкими, рассмотрим стандартные библиотечные функции, такие как std::fill(которые укусили меня более одного раза в C ++ 03):

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

Не компилировать: Cannot convert int to MyClass*.

Angew больше не гордится SO
источник
2

IMO более важен, чем эти проблемы перегрузки: в глубоко вложенных шаблонных конструкциях трудно не потерять отслеживание типов, и предоставление явных подписей является довольно трудным делом. Таким образом, для всего, что вы используете, чем точнее сфокусировано на предполагаемой цели, тем лучше, это уменьшит необходимость явных подписей и позволит компилятору генерировать более проницательные сообщения об ошибках, когда что-то идет не так.

leftaroundabout
источник