Item 8:Prefer nullptr to 0 and NULL.
有一个协定:字面值0是一个int,不是一个指针。如果c++在只有指针可以被用到的地方发现了了0,它将会将0解释为一个null指针,但是这是一个备选计划。c++的主要政策是0是一个int,不是一个指针。
实际上,对于NULL来说是一样的。NULL的情况中细节方面上有一些不确定因素,因为NULL的实现取决于一个integral类型(非int)。这通常不是一个相同的情况,但是它不会印象什么,因为现在的问题不是NULL的真正类型究竟是什么,而是0和NULL都不具有指针类型。
在c++98中,主要的问题发生在,调用指针类型和integral类型参数的重载函数会造成令人惊讶的结果。给这样的重载函数传递0或者NULL永远不会调用指针类型版本的重载函数:
void f(int); //f的三个重载函数
void f(bool);
void f(void *);
f(0); //调用f(int),不是f(void *)
f(NULL); //可能不会通过编译,但一般来说调用
//f(int).永远不会调用f(void *)
关于f(NULL)的行为的不确定性是关于NULL类型实现的反应。如果NULL被定义为0L(例如0作为long),调用是模棱两可的,因为从long到int,从long到bool,从long到void*的转换被认为是一样好的。关于这个函数调用有趣的事情
在于一个矛盾,这个矛盾是指源代码中表现出来的意思(“我要用NULL作为参数调用f--一个null指针”)和它实际的意思(“我要用类似于integer的类型的参数调用f----不是一个null指针”)。这个违反直觉的行为导致了c++98程序员去避免指针类型和integral类型分别作为参数的重载函数的设计。这个指导方针在c++11中依旧保持合理,因为尽管有这个Item的建议,很有可能一些程序员
会一直使用0和NULL,即使nullptr是更好的选择。
nullptr的优点是它不具有integral类型。事实上来说,它也不具有指针类型,但是你可以认为它是所有类型的指针。nullptr真实的类型是std::nullptr_t,并且,in a wonderfully circular definition,std::nullptr_t被定义为nullptr的类型。类型std::nullptr_t隐式的转化为所有类型的原生指针,这就是让nullptr表现的像一个所有类型的指针的原因。
用nullptr调用f的重载函数会调用void*重载类型(或者指针参数的重载类型)
因为nullptr不能被视为任何的integral类型:
f(nullptr); //调用f(void*) 重载函数
使用nullptr而不是0或者NULL会避免重载决议令人惊讶的行为,但是这不是它唯一的优点。它会让代码更清楚,尤其是当auto变量被使用的时候。例如,假设你在代码中看到:
auto result = findRecord(/* arguments */);
if(result == 0) {
...
}
如果你恰好不知道(或者不能轻易的发现)findRecord函数返回了什么,那么result究竟是指针类型还是integral类型是不那么清楚的。毕竟,0(result比较的对象)能够被看成两种类型中的任何一种。另一方面,如果你看到下面的代码:
auto result = findRecord(/* arguments */);
if(result == nullptr)
{
...
}
这里没有困惑的地方:result一定是一个指针类型。
当模板参与进来的时候nullptr更为出色。假设你有一些函数,这些函数只有当合适的mutex锁住的时候才能调用。每个函数都接受不同类型的指针:
int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f3(Widget* pw);
传入null指针用来调用代码看起来像这样:
std::mutex f1m,f2m,f3m;
using MuxGuard = std::lock_guard<std::mutex>;
...
{
MuxGuard g(f1m);
auto result = f1(0);
}
{
MuxGuard g(f2m);
auto result = f2(NULL);
}
{
MuxGuard g(f3m);
auto result = f3(nullptr);
}
前面两个调用没有使用nullptr让人悲伤,但是代码是有用的,这在某些方面是有价值的。然而,函数调用中的重复的模式--锁住mutex,调用函数,解锁mutex--更让人悲伤。这令人厌烦。这种重复的源代码是模板设计可以避免的,所以我们模板化这种模式:
template<typename FuncType,
typename MuxType,
typename PtrType>
auto lockAndCall(FuncType func,
MuxType& mutex,
PtrType ptr) -> decltype(func(ptr))
{
using MuxGuard = std::lock_guard<MuxType>;
MuxGuard g(mutex);
return func(ptr);
}
如果这个函数的返回类型(auto...->decltype(func(ptr)))让你抓耳挠腮,去看Item3,item3解释了发生了什么。在c++14中你可以看到,返回类型可以被简单的decltype(auto)推断出来:
template<typename FuncType,
typename MuxType,
typename PtrType>
decltype(auto) lockAndCall(FuncType func,
MuxType& mutex,
PtrType ptr)
{
using MuxGuard = std::lock_guard<MuxType>;
MuxGuard g(mutex);
return func(ptr);
}
有了lockAndCall模板,调用者可以像这样写代码:
auto result1 = lockAndCall(f1,f1m,0); //error!
auto result2 = lockAndCall(f2,f2m,NULL); //error!
...
auto result3 = lockAndCall(f3,f3m,nullptr); //fine
是的,它们可以这么写,但是就像评论指示的那样,前两个调用不会通过编译。问题在于第一个调用,当0被传入时,模板类型推断推断它的类型。0的类型总是int,所以ptr的类型也是int。不幸的是,这意味着lockAndCall中func的调用中,被传入了一个int值,这不和f1期望的std::shared_ptr<Widget>参数一致。传入lockAndCall函数的0企图去代表一个null指针,但是真正传入的是一个int,想要将int传入f1作为std::shared_ptr<Widget>是一种类型错误。用0调用lockAndCall函数的失败,是因为模板中,一个int被传入需要std::shared_ptr<Widget>参数的函数。
涉及NULL的调用的分析几乎是一样的。当NULL被传入lockAndCall,一个integral类型被推断为参数ptr,当ptr--一个int或者int-like类型--被传入f2中时会造成类型错误,因为f2期望着一个std::shared_ptr<Widget>。
相反的,设计nullptr的调用没有问题。当nullptr被传入lockAndCall中,ptr的类型被推断为std::nullptr_t。当ptr被传入f3,有从std::nullptr_t到Widget*的隐式转换,因为std::nullptr_t隐式的转换为所有的指针类型。
模板类型推断会将0和NULL推断出“错误的类型”(例如,它们真实的类型而不是备选的代表着null指针的类型)这一事实是使用nullptr而不是0和NULL最合理的原因(当你想要一个null指针的时候)。使用nullptr,模板不会有什么特殊改变。结合nullptr不会因为重载决议(0和NULL会受到影响)受到影响这一事实,the case is ironclad。当你涉及一个null指针的时候,使用nullptr而不是0和NULL。
Things to Remember
1.相比于0和NULL,使用nullptr。
2.避免重载函数中有integral和指针类型的参数。