条款8 优先使用nullptr而不是0或者NULL
0
字面上是一个int
类型,而不是指针,这是显而易见的。C++
扫描到一个0
,但是发现在上下文中仅有一个指针用到了它,编译器将勉强将0
解释为空指针,但是这仅仅是一个应变之策。C++
最初始的原则是:0
是int
而非指针。
经验上讲,同样的情况对NULL
也是存在的。对NULL
而言,仍有一些细节上的不确定性,因为赋予NULL
一个除了int
(即long
)以外的整数类型是被允许的。这不常见,但是这真的是没有问题的,因为此处的焦点不是NULL
的确切类型,而是0
和NULL
都不属于指针类型。
在C++98
中,这意味着重载指针和整数类型的函数行为会令人吃惊。传递0
或者NULL
作为参数给重载函数永远不会调用指针重载的那个函数:
f(NULL)
行为的不确定性的确反映了在实现NULL
的类型上存在的自由发挥空间。如果NULL
被定为0L
(即0
作为一个long
整形),函数的调用是有歧义的,因为long
转换为int
,long
转换为bool
,0L
转换为void*
都被认为是同样可行的。关于这个函数调用有意思的事情是在源代码的字面意思(使用NULL
调用f
,NULL
应该是个空指针)和它的真实意义(一个整数在调用f
,NULL
不是空指针)存在着冲突。这种违背直觉的行为正是C++98
程序员不被允许重载指针和整数类型的原因。这个原则对于C++11
依然有效,因为尽管有关条款的力荐,仍然还有一些开发者继续使用0
和NULL
,虽然nullptr
是一个更好的选择。
nullptr
的优势是它不再是一个整数类型。诚实的讲,它也不是一个指针类型,但是你可以把它想象成一个可以指向任意类型的指针。nullptr
的类型实际上是std::nullptr_t
,std::nullptr_t
定义为nullptr
的类型,这是一个完美的循环定义。std::nullptr_t
可以隐式的转换为所有的原始指针类型,这使得nullptr
表现的像可以指向任意类型的指针。
使用nullptr
作为参数去调用重载函数f
将会调用f(void*)
重载体,因为nullptr
不能被视为整数类型的:
使用nullptr
而不是0
或者NULL
,可以避免重载解析上的令人吃惊的行为,但是它的优势不仅限于此。它可以提高代码的清晰度,尤其是牵扯到auto
类型变量的时候。举个栗子:
如果你不能轻松的看出findRecord
返回的是什么,要知道result
是一个指针还是整数类型并不是很简单的。毕竟,0
(被用来测试result
的)既可以当做指针也可以当做整数类型。另一方面,如果是下面这个栗子:
明显就没有歧义了:result
一定是个指针类型。
当模板进入我们的考虑的范围,nullptr
的光芒就显得更加耀眼了。假想你有一些函数,只有当对应的互斥量被锁定的时候,这些函数才可以被调用。每个函数的参数是不同类型的指针:
想传递空指针给这些函数的调用看上去像这样:
在前两个函数调用中没有使用nullptr
是令人沮丧的,但是上面的代码是可以工作的,这才是最重要的。然而,代码中的重复模式——锁定互斥量,调用函数,解锁互斥量————才是更令人沮丧和反感的。避免这种重复风格的代码正是模板的设计初衷,因此,让我们使用模板化上面的模式:
如果这个函数的返回类型(auto ... -> decltype(func(ptr))
)让你挠头不已,你应该去看看条款3。在C++14
中,你可以看到,返回值可以通过简单的decltype(auto)
推导得出:
给定lockAndCall
模板,调用者可以写像下面的代码:
他们可以这样写,但是就如注释中指明的,三种情况里面的两种是无法编译通过的。在第一个调用中,当把0
作为参数传给lockAndCall
,模板通过类型推导得知它的类型。0
的类型总是int
,这就是对lockAndCall
的调用实例化的时候的类型。不幸的是,这意味着在lockAndCall
中调用func
,被传入的是int
,这个f1
期望接受的参数是std::shared_ptr<Widget>
是不兼容的。传入到lockAndCall
的0
尝试来表示一个空指针,但是真正传入的是一个普通的int
类型。尝试将int
作为std::shared_ptr<Widget>
传给f1
会导致一个类型冲突错误。使用0
调用lockAndCall
会失败,因为在模板中,一个int
类型传给一个要求参数是std::shared_ptr<Widget>
的函数。
对调用NULL
的情况分析基本上和以上是一样的。
相反,使用nullptr
是没有问题的。当nullptr
传递给lockAndCall
,ptr
的类型被推导为std::nullptr_t
。当ptr
被传递给f3
,有一个由std::nullptr_t
到Widget*
的隐式转换,因为std::nullptr_t
可以隐式转换为任何类型的指针。
真正的原因是,对于0
和NULL
,模板类型推导出了错误的类型(它们的真正类型,而不是作为空指针而体现出的退化的内涵),这是在需要用到空指针时使用nullptr
而非0
或者NULL
最引人注目的原因。使用nullptr
,模板不会造成额外的困扰。另外结合nullptr
在重载中不会导致像0
和NULL
那样的诡异行为的事实,胜负已定。当你需要用到空指针时,使用nullptr
而不是0
或者NULL
。
归纳
相较于
0
和NULL
,优先使用nullptr
避免整数类型和指针类型之间的重载
Last updated