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

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

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

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

Ответ 1

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

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. В С++ NULL определяется как #define NULL 0, поэтому он в основном int, поэтому вызывается f(int).

Ответ 2

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


Что такое Null точно?

Pre С++ 11 Null использовался для представления указателя, который не имеет значения или указателя, который не указывает на что-либо действительное. Вопреки популярному понятию Null не является ключевым словом в С++. Это идентификатор, определенный в стандартных заголовках библиотек. Короче говоря, вы не можете использовать 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

Стандарт С++ определяет NULL как макрос, определенный реализацией, определенный в некоторых стандартных файлах заголовков библиотеки. Происхождение NULL от C и С++ унаследовало его от C. Стандарт C определял NULL как 0 или (void *) 0. Но в С++ существует тонкая разница.
С++ не мог принять эту спецификацию так, как она есть. В отличие от C, С++ - это строго типизированный язык. (C не требует явного приведения в то время как от void* к любому типу, в то время как С++ требует явного приведения). Это делает определение NULL, заданное стандартом 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, С++ Standard, для определения NULL в качестве числового литерала 0 или 0L.


Итак, в чем же нуждается еще одна константа нулевого указателя, когда мы уже имеем Null?

Хотя комитет по стандартам С++ разработал определение NULL, которое будет работать на С++, это определение имело свою собственную долю проблем. 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*, потому что 0 также является нулевым указателем.
  • Случай 3: Если Null определяется как 0,
    вызывает doSomething(int), когда возможно doSomething(char *), что может привести к логической ошибке во время выполнения.
    Пока, если Null определяется как 0L,
    Вызов неоднозначен и приводит к ошибке компиляции.

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


Итак, что такое nullptr и как это устраняет проблемы Null?

С++ 11 вводит новое ключевое слово nullptr в качестве нулевой константы указателя. В отличие от NULL его поведение не определяется реализацией. Это не макрос, но он имеет свой собственный тип. nullptr имеет тип std::nullptr_t. С++ 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: OK - Свойство 2
  • Случай 2: Не работает - свойство 3
  • Случай 3: OK - Свойство 3
  • Случай 4: Без путаницы - Вызов char * версия, Свойство 2 и 3

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

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

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


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

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

Ответ 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); }

Ответ 4

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

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

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

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

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

foo(nullptr);

Дополнительная ссылка с веб-страницы Bjarne.
Нежелательно, но на С++ 11 обратите внимание:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)

Ответ 5

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

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

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

Ответ 6

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

Ответ 7

Основные свойства nullptr

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

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

Преимущество 1 - Удалить неоднозначность при перегрузке по указателю и интегральным типам

В С++ 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*)

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

Преимущество

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

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

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

Еще одно преимущество nullptr over 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 -an int или int-like type передается 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.