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

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

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

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

7 ответов


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

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) называется.


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


что это NULL точно?

Pre 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: Не будет компилироваться, потому что cast from void * для указателя на функцию-член нужно.

в отличие от C, стандарт C++ должен определять NULL как числовой литерал 0 или 0L.


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

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

#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* версия была желательна, потому что 0 Также является нулевым указателем.
  • Случай 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 * версия, свойство 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 Указатели


реальная мотивация здесь идеальный переадресации.

считаем:

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

основы nullptr

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

литерал 0 является int, а не указателем. Если 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 для типа возврата

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

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. Поэтому его можно обобщить с помощью template. Я написал функцию шаблона 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 внутри экземпляра этого вызова локандколл. К сожалению, это означает, что в вызове func внутри lockAndCall передается int, и это несовместимо с std::shared_ptr<int> параметр that 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, который ожидает получить std::unique_ptr<int>.

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

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


нет никакого прямого преимущества наличия 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)

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

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

не компилируется: Cannot convert int to MyClass*.


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