优先选择nullptr而不是0和NULL

时间:2021-09-12 09:56:31

我们知道:0是一个int,而不是一个指针。如果C++在一个只有指针才能够使用的上下文中发现它只有一个0,那么它会勉强将0解释成空指针,但那时一种倒退行为。C++的主要方针是0就是一个int,而不是指针。

实际上来说,对于NULL也是一样。关于NULL还有一些不确定因素,因为其实现允许给NULL一个整型而不必是int(比如说long)。这并不常见,但是也无关紧要,因为这里的问题并不是NULL的确切类型是什么,而是0和NULL都不是指针类型。

C++98中,主要的启示就是对指针和整型分别进行重载可能会导致意想不到的结果。传递0或者NULL给这些重载将永远不会调用指针版本的重载函数:

void f(int); // three overloads of f
void f(bool);
void f(void*); f(); // calls f(int), not f(void*)
f(NULL); // might not compile, but typically calls
// f(int). Never calls f(void*)

关于f(NULL)行为的不确定性是NULL的实现类型不确定性的一个反映。如果NULL被定义成,比如说,0L(也就是说是long),那么该调用就很模棱两可,因为long到int的转换,long到bool的转换以及0L到void*的转换被认为一样好。关于该调用的一个有趣的事就是该源代码看起来的含义(”我用NULL(空指针)调用f”)和其实际的含义(”我用NULL(某种整数)调用f”)之间的冲突。这种违法直觉的行为促成了c++98程序员的一个指导方针就是避免重载指针类型和整型。该指导方针在C++11中依然有效,因为尽管我在这里建议大家用nullptr,但是很多开发者很可能还是继续使用0和NULL。

nullptr的优势在于它并不是一个整型。实话实说,它其实也不是一个指针类型,但是你可以认为它是所有类型的指针。nullptr的真实类型是std::nullptr_t,而std::nullptr_t又被定义成nullptr的类型,形成一个极好的环形定义。std::nullptr_t可以隐形转换任何原始指针类型,而正是这才让nullptr用起来像是所有类型的指针。

使用nullptr来调用重载函数f会调用void*版本的重载函数,因为nullptr不能被当做成一个整型来看:

f(nullptr);         //calls f(void*) overload

所以使用nullptr而不是0或者NULL防止调用不合理的重载函数,但是这并不是其仅有的优势。它还可以提高代码的清晰度,尤其当你使用auto变量时。例如,假设你遇到下面这种代码:

auto result = findRecord(/* arguments */);

if(result == ){
...
}
优先选择nullptr而不是0和NULL

如果你恰好不知道(或者不容易弄清楚)findRecord的返回类型,那么这个result是一个指针类型还是一个整型就可能不太清楚了。毕竟,0对于上面哪一种情况都适用。如果你看到下面的代码:

auto result = findRecord(/* arguments */);

if(result == nullptr){
...
}

就没有任何歧义了:result就是一个指针类型

而当涉及到模板时,nullptr更是大显光彩。假设你有一些函数,只有当合适的mutex被锁定时才应该被调用。每个函数都接受一个不同类型的指针:

int 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

想要传递空指针的函数调用写起来可能像下面这样:

std::mutex f1m, f2m, f3m;   //mutexes for f1,f2,f3

using MuxGuard = std::lock_guard<std::mutex>; //C++11 typedef; See Item 9
... {
MuxGuard g(f1m); //lock mutex for f1
auto result = f1(); //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
} //unlock mutex {
MuxGuard g(f3m); //lock mutex for f3
auto result = f3(nullptr);//pass nullptr as null ptr to f3
} //unlock mutex

该代码中前两个调用没有使用nullptr真是一个悲哀,但是这份代码可以正常工作,有一定价值。然而,调用代码中的重复模式—锁mutex,调用函数,解锁—却更加让人悲伤。这很令人烦。这种类型的代码重复是模板被设计用来避免的事物之一,所以咱们对这种模式使用模板:

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)))还不熟悉,那么请你参考Item 3,那里解释的很清楚。如果你使用C++14,返回类型还可以被精简成decltype(auto):

template<typename FuncType,
typename MuxType,
typename PtrType>
auto lockAndCall(FuncType func, //C++14
MuxType& mutex,
PtrType ptr)
{
MuxGuard g(mutex);
return func(ptr);
}

基于lockAndCall模板(哪一个版本都行),调用代码可以写成如下:

auto result1 = lockAndCall(f1,f1m,);  //error!
...
auto result2 = lockAndCall(f2,f2m,NULL); //error!
...
auto result3 = lockAndCall(f3,f3m,nullptr); //fine

他们可以这样写,但是正如评论所说的,前两个并不会通过编译。第一个调用的问题在于0被传递给lockAndCall,模板类型推断计算出它的类型。0的类型,过去是,现在也一直都是int,所以在针对这个调用的lockAndCall实例中,其参数ptr的类型就是int。不幸的是,这意味着lockAndCall内部的func调用中,一个int被传递了,而这和f1期待的std::shared_ptr<Widget>参数类型不匹配。传递给lockAndCall的0本来打算是表示空指针的,但是传递进去的实际类型是一个普通的int。尝试给f1传递int当做其std::shared<Widget>参数会导致类型错误。使用0来调用lockAndCall会失败是因为在模板内部,一个int类型被传递给了一个需要std::shared_ptr<Widget>类型的函数里。

对于涉及到NULL的调用的分析和上面基本上一模一样。当NULL被传递给lockAndCall时,对参数ptr推断出的类型是一个整型,而当一个int或者类似于int的类型被传递个期待一个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
  • 避免对整型和指针类型进行重载