前言
首先需要明确一点:0是一个int,而非指针。C++在一个应该出现指针但只出现了0的环境下会把这个0看作为一个空指针,但这并不代表0与空指针等价。NULL也是一样,虽然它可能是int或者long。总之,0或NULL都不是指针。
0与NULL可能导致的二义性
在C++98中,对整数型与指针类型进行重载可能会发生意外(即重载区别仅在一个函数的形参为类似int型,一个形参为pointer)。将0或NULL传递给这样的重载函数永远不会调用其指针重载版本:1
2
3
4
5void f(int); // three overloads of f
void f(bool);
void f(void*);
f(0); // calls f(int), not f(void*)
f(NULL); // might not compile, but typically calls f(int). Never calls f(void*)
显然,如果将NULL定义为0L,那么本次函数调用具有二义性,原因在于从long到int、从long到bool以及从0L到void*具备同样的转换优先级。关于该调用,有趣之处在于,你以为你在执行f(the null pointer),实际上它意味着f(some kinds of intergral)。
这种违反直觉的行为直接导致C++98规定开发者应当避免以指针和整型类型作为重载参数。这项原则在C++11中仍然适用,因为尽管目前提倡以nullptr代替0与NULL,但仍有许多开发者孜孜不倦地使用后者。
Nullptr的优点
避免调用二义性
Nullptr的优势在于它不是一个整形,实际上它也不是一个指针类型,不过你可以认为它是一个可以指向任何类型的指针类型。nullptr的实际类型是std::nullptr_t,并且std::nullptr_t被定义为nullptr的类型(一个奇怪的循环)。std::nullptr_t类型可以隐式转换为所有原始指针类型,因此nullptr看起来像是一个可以指向所有类型的指针。
调用f(nullptr)时将直接触发指针重载版本,原因在于nullptr不可能转为任何整形:1
f(nullptr); // calls f(void*) overload
提高代码清晰度
在涉及到auto声明的变量时,使用nullptr可以提高代码清晰度,有实例如下:1
2
3
4auto result = findRecord( /* arguments */ );
if (result == 0) {
...
}
如果我们不清楚findRecord的返回类型,我们可能在看代码时可能会困惑result究竟是一个指针还是一个整形,但下述代码则十分明晰:1
2
3
4auto result = findRecord( /* arguments */ );
if (result == nullptr) {// result must be a pointer
…
}
template中的nullptr
nullptr在template中能够发挥更大的作用。假设当前有一些函数,它们只有在某些互斥量被锁定时才会被调用,每一个函数的形参都是一个指针类型:1
2
3int f1(std::shared_ptr<Widget> spw); // call these only when
double f2(std::unique_ptr<Widget> upw); // the appropriate
bool f3(Widget* pw); // mutex is locked
现将以空指针作为实参完成函数调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxGuard = std::lock_guard<std::mutex>; // C++11 typedef; see Item 9
…
{
MuxGuard g(f1m); // lock mutex for f1
auto result = f1(0); // pass 0 as null ptr to f1
} // unlock mutex
…
{
MuxGuard g(f2m); // lock mutex for f2
auto result = f2(NULL); // pass NULL as null ptr to f2
}
…
{
MuxGuard g(f3m); // lock mutex for f3
auto result = f3(nullptr); // pass nullptr as null ptr to f3
}
这明显造成了代码重复,因此我们决定对此过程执行模板化操作:1
2
3
4
5
6
7
8
9
10
11template<typename FuncType,typename MuxType,typename PtrType>
auto lockAndCall(FuncType func,MuxType& mutex,PtrType ptr) -> decltype(func(ptr)){// C++11
MuxGuard g(mutex);
return func(ptr);
}
or
template<typename FuncType,typename MuxType,typename PtrType>
decltype(auto) lockAndCall(FuncType func, MuxType& mutex,PtrType ptr){// C++14
MuxGuard g(mutex);
return func(ptr);
}
你可能会这样调用模板函数:1
2
3auto result1 = lockAndCall(f1, f1m, 0); // error!
auto result2 = lockAndCall(f2, f2m, NULL); // error!
auto result3 = lockAndCall(f3, f3m, nullptr); // fine
当0传入模板函数时,PtrType被推衍为int类型,此时func会发现int无法转为一个指针类型,进而编译失败。使用NULL也是类似的道理,PtrType被推衍为一个整数类型,无法转为func的参数类型。
总之,template类型推衍无法识别出0或NULL所具备的空指针身份,因此我们应当积极使用nullptr代替它们。
总结
- 应当尽可能以nullptr取代0与NULL。
- 应当避免对整数型与指针类型之间的重载。