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

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

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

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

153

Решение

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

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

168

Другие решения

C ++ 11 вводит nullptrэто известно как Null Константа указателя и It повышает безопасность типов а также разрешает неоднозначные ситуации в отличие от существующей зависящей от реализации константы нулевого указателя 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: Не будет компилироваться, потому что приведен из 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* версия была желанной, потому что 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. Указатели

81

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

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

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

Основы 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, который ожидает получить std::unique_ptr<int>,

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

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

5

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

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

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

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

4

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

2
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector