Каковы преимущества использования 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 Указатели


Я уже практикую ваш последний совет, так как знаю 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)

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


2

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.