用nullptr代替0和NULL
你看看这样行不行,0字面上的意思是个int,而不是指针。如果C++代码中把0用作指针,那么C++会勉强地把0翻译为一个空指针,和一开始说的不太一样。C++的主要政策是,0是一个int值,而不是指针。
从实践上来说,NULL也是如此。NULL在细节上有一些不确定性,因为在实现上允许把NULL附给一个不同于int的整数类型(例如long)。这不常见,但在这里却无所谓,因为我们这里讨论的问题不是NULL的确切类型,而是0和NULL都不是指针类型。
在C++98中,基于上面的问题,使用带有指针类型或整数类型重载函数的会出乎我们意料。传递0或者NULL到重载函数中,指针作为参数的重载函数不会被调用:
void f(int); // f的3个重载函数
void f(bool);
void f(void*);
f(0); // 调用f(int)
f(NULL); // 可能不会通过编译,一般境况下调用f(int)
想要知道f(NULL)的行为就要求我们知道NULL的实现。如果NULL被定义为0L(即0是long类型),那么这次调用是引起歧义的,因为long转换成int,long转换成bool,long转换成void*在编译器看来是一样好的。这个函数调用从代码上看(我要用NULL空指针调用f),和从实际上看(我用某个整数类型调用f)是相互矛盾的。这个违反直觉的行为使得C++98程序员的指导方针是避免重载函数带有指针类型和整数类型。这个指导方针在C++11中依然有效,因为尽管有这条款的建议,但是一些开发者依然使用0和NULL,即使nullptr更好。
nullptr的好处是它不是整数类型,但实话说,它也不是指针类型,但是你可以把它看作一个可以指向所有类型的指针。nullptr的实际类型是std::nullptr_t,std::nullptr_t可以隐式转换为所有类型的原生指针,这就使得nullptr像一个可以任何类型的指针。
用nullptr调用上面f的重载函数,编译器会选择参数为void*的重载函数,因为nullptr不能被视为整数类型:
f(nullptr)
// 调用 f(void*)
用nullptr可以避免重载选择的问题,但它不只有这一个优点。它还可以让代码更清晰易懂,尤其是当你使用了auto变量。例如,你遇到了以下代码:
auto result = findRecord( /*arguments*/);
if (result == 0) {
...
}
如果你碰巧不知道findRecord函数返回什么(或者很难查明),那么你可能不知道result是指针还是整数类型,但你别忘了,0在两种情况下都是可以继续运行的。另一方面,如果我们用以下代码:
auto result = findRecord(/*arguments*/);
if (result == nullptr) {
...
}
这就没有二义性了:result一定是指针类型。
nullptr在涉及模板的时候十分有用。假如你有些函数只能在互斥锁被锁的时候才可以调用,而每个函数又有不同的指针类型:
int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f3(Widget *pw);
我们像这样传递空指针给函数:
std::mutex f1m, f2m, f3m; // f1,f2, f3的互斥锁
using MuGuard = std::lock_guard<std::mutex>;
...
{
MuGuard g(f1m);
auto result = f1(0);
}
...
{
MuGuard g(f2m);
auto result = f2(NULL);
}
...
{
MuGuard g(f3m);
auto result = f3(nullptr);
}
前两个函数不使用nullptr让人感到悲伤,但函数是可以运行的,代码还是有些价值。但是这调用函数的模式——上锁,调用函数,解锁——更让人悲伤,这让人很烦恼,这种重复的代码可以设计一个模板来避免:
template <typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType &mutex, PtrType ptr)
`-> decltype(func(ptr))`
{
MuxGuard g(mutex);
return func(ptr);
}
如果函数的返回类型(auto.. -> decltype(func(ptr)))让你头痛,那么你可以阅读条款3缓解头痛,那里解释了原理。如果在C++14,我们可以使用decltype(auto):
template <typename FuncType, typename MuxType, typename PtrType>
decltype(auto)
lockAndCall(FuncType func, MuxType &mutex, PtrType ptr)
{
MuxGuard g(mutex);
return func(ptr);
}
有了lockAndCall这个模板(任意一个版本),调用者可以写出这样的代
码:
auto result1 = lockAndCall(f1, f1m, 0); // 错误
...
auto result2 = lockAndCall(f2, f2m, NULL); // 错误
...
auto result3 = lockAndCall(f3, f3m, nullptr); // 正确
就像注释里所说,前面两个函数无法通过编译。第一个函数调用的问题是,当传0给lockAndCall时,模板类型推断会把ptr推断成int类型,因此模板把int类型参数传给func函数,int类型和f1期待的std::shared_ptr<Widget>
类型不兼容,就会出错。我们传递0想表示的是空指针,但是实际变成了传递一个普通的int,试图将int传递给接收std::shared_ptr<Widget>
的f1是会报错的。这错误主要是因为在模板中,int的值传递给接收std::shared_ptr<Widget>
的函数。
第二个函数在本质上错误相同。NULL被模板推断为整型数类型,那么参数为std::unique_ptr<Widget>
的f2函数接收到的整数类型的值,所以报错。
作为对比,使用nullptr的那个函数调用没有出现问题。当nullptr传递给lockAndCall时,ptr的类型被推断为std::nullptr_t,当ptr传递给f3时,发生了std::nullptr_t到Widget *的隐式转换,因为std::nullptr_t可以隐式转换为所有类型的指针。
事实上,最迫使你使用nullptr代替0和NULL的原因是,模板类型推断会为0和NULL推断出“错误”的类型当你想要一个空指针时。有了nullptr,模板就不会引起什么问题了。再想想nullptr不受重载函数选择策略影响,而0和NULL却容易受到影响。所以,当你要说明空指针时,用nullptr,而不是0或者NULL。
总结
需要记住的两点:
- 使用nullptr代替0和NULL。
- 避免整数类型与指针类型之间的函数重载。