EffectiveModernCppChinese/3.MovingToModernCpp/item8.md
2016-11-23 18:32:46 +08:00

149 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Item 8: Prefer nullptr to 0 and NULL.
条款八:优先考虑nullptr而非0和NULL
你看这样对不对字面值0是一个int不是指针。
如果C++发现在当前上下文只能使用指针它会很不情愿的把0解释为指针但是那是最后的退路。
一般来说C++的解析策略是把0看做int而不是指针。
实际上NULL也是这样的。但在NULL的实现细节有些不确定因素
因为实现被允许给NULL一个除了**int**之外的整型类型(比如**long**)。
这不常见但也算不上问题所在。这里的问题不是NULL没有一个确定的类型而是0和NULL都不是指针类型。
在C++98中对指针类型和整型进行重载意味着可能导致奇怪的事情。
如果给下面的重载函数传递0或NULL它们绝不会调用指针版本的重载函数
````cpp
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**的转换或**0L**到**void\* **的转换都会被考虑。
有趣的是源代码表现出的意思我指的是使用NULL调用f和实际想表达的意思我指的是用整型数据调用f是相矛盾的。
这种违反直觉的行为导致C++98程序员都将避开同时重载指针和整型作为编程准则_[0]_。
在C++11中这个编程准则也有效因为尽管我这个条款建议使用**nullptr**,可能很多程序员还是会继续使用**0**或**NULL**,哪怕**nullptr**是更好的选择。
**nullptr**的优点是它不是整型。
老实说它也不是一个指针类型,但是你可以把它认为是通用类型的指针。
**nullptr**的真正类型是**std::nullptr_t**,在一个完美的循环定义以后,**std::nullptr_t**又被定义为**nullptr**。
**std::nullptr_t**可以转换为指向任何内置类型的指针,这也是为什么我把它叫做通用类型的指针。
使用**nullptr**调用f将会调用**void\***版本的重载函数,因为**nullptr**不能被视作任何整型:
````cpp
f(nullptr); //调用重载函数f的f(void*)版本
````
使用**nullptr***代替**0**和**NULL**可以避开了那些令人奇怪的函数重载决议,这不是它的唯一优势。
它也可以使代码表意明确,尤其是当和**auto**一起使用时。
举个例子,假如你在一个代码库中遇到了这样的代码:
````cpp
auto result = findRecord( /* arguments */ );
if (result == 0) {
}
````
如果你不知道findRecord返回了什么或者不能轻易的找出那么你就不太清楚到底result是一个指针类型还是一个整型。
毕竟,**0**也可以像我们之前讨论的那样被解析。
但是换一种假设如果你看到这样的代码:
````cpp
auto result = findRecord( /* arguments */ );
if (result == nullptr) {
}
````
这就没有任何歧义:**result**的结果一定是指针类型。
当模板出现时**nullptr**就更有用了。
假如你有一些函数只能被合适的已锁互斥量调用。
每个函数都有一个不同类型的指针:
````cpp
int f1(std::shared_ptr<Widget> spw); // 只能被合适的
double f2(std::unique_ptr<Widget> upw); // 已锁互斥量调
bool f3(Widget* pw); // 用
````
如果这样传递空指针:
````cpp
std::mutex f1m, f2m, f3m; // 互斥量f1mf2mf3m各种用于f1f2f3函数
using MuxGuard = // C++11的typedef参见Item9
std::lock_guard<std::mutex>;
{
MuxGuard g(f1m); // 为f1m上锁
auto result = f1(0); // 向f1传递控制空指针
} // 解锁
{
MuxGuard g(f2m); // 为f2m上锁
auto result = f2(NULL); // 向f2传递控制空指针
} // 解锁
{
MuxGuard g(f3m); // 为f3m上锁
auto result = f3(nullptr); // 向f3传递控制空指针
} // 解锁
````
令人遗憾前两个调用没有使用**nullptr**,但是代码可以正常运行,这也许对一些东西有用。
但是重复的调用代码——为互斥量上锁,调用函数,解锁互斥量——更令人遗憾。它让人很烦。
模板就是被设计于减少重复代码,所以让我们模板化这个调用流程:
````cpp
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)) **感到困惑不解Item3可以帮助你。
在C++14中代码的返回类型还可以被简化为**decltype(auto)**
````cpp
template<typename FuncType,
typename MuxType,
typename PtrType>
decltype(auto) lockAndCall(FuncType func,
MuxType& mutex,
PtrType ptr) {
MuxGuard g(mutex);
return func(ptr);
}
````
可以写这样的代码调用**lockAndCall**模板(两个都算):
````cpp
auto result1 = lockAndCall(f1, f1m, 0); // 错误!
auto result2 = lockAndCall(f2, f2m, NULL); // 错误!
auto result3 = lockAndCall(f3, f3m, nullptr); // 没问题
````
代码虽然可以这样写,但是就像注释中说的,前两个情况不能通过编译。
在第一个调用中存在的问题是当0被传递给**lockAndCall**模板,模板类型推导会尝试去推导实参类型,
**0**的类型总是**int**,所以**int**版本的实例化中的func会被int类型的实参调用。
这与**f1**期待的参数**std::shared_ptr<Widget>**不符。
传递**0**本来想表示空指针,结果**f1**得到的是和它相差十万八千里的**int**。
把**int**类型看做**std::shared_ptr<Widget>**类型自然是一个类型错误。
在模板**lockAndCall**中使用**0**之所以失败是因为得到的是**int**但实际上模板期待的是一个
**std::shared_ptr<Widget>**
第二个使用**NULL**调用的分析也是一样的。当**NULL**被传递给**lockAndCall**形参ptr被推导为整型_[1]_
然后当ptr——一个int或者类似int的类型——传递给f2的时候就会出现类型错误。当ptr被传递给f3的时候
隐式转换使**std::nullptr_t**转换为**Widget\* **,因为**std::nullptr_t**可以隐式转换为任何指针类型。
模板类型推导将**0**和**NULL**推导为一个错误的类型,这就导致它们的替代品**nullptr**很吸引人。
使用**nullptr**,模板不会有什么特殊的转换。
另外,使用**nullptr**不会让你受到同重载决议特殊对待**0**和**NULL**一样的待遇。
当你想用一个空指针,使用**nullptr**,不用**0**或者**NULL**。
记住
+ 优先考虑nullptr而非0和NULL
+ 避免重载指针和整型
## 译注
[0] 请务必注意结合上下文使用这条规则
[1] 由于依赖于具体实现所以不一定是整数类型所以用整型泛指int,long等类型