Каковы преимущества использования 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 важнее, чем эти проблемы с перегрузкой: в глубоко вложенных конструкциях шаблонов трудно не потерять типы, и предоставление явных подписей-это довольно усилие. Поэтому для всего, что вы используете, чем точнее ориентированы на целевое назначение, тем лучше, это уменьшит потребность в явных подписях и позволит компилятору создавать более проницательные сообщения об ошибках, когда что-то идет не так.