Effective Modern C++翻译系列之Item8

来源:互联网 发布:matlab gui编程教程 编辑:程序博客网 时间:2024/05/22 12:02

Item 8:Prefer nullptr to 0 and NULL.

有一个协定:字面值0是一个int,不是一个指针。如果c++在只有指针可以被用到的地方发现了了0,它将会将0解释为一个null指针,但是这是一个备选计划。c++的主要政策是0是一个int,不是一个指针。

实际上,对于NULL来说是一样的。NULL的情况中细节方面上有一些不确定因素,因为NULL的实现取决于一个integral类型(非int)。这通常不是一个相同的情况,但是它不会印象什么,因为现在的问题不是NULL的真正类型究竟是什么,而是0NULL都不具有指针类型。

c++98中,主要的问题发生在,调用指针类型和integral类型参数的重载函数会造成令人惊讶的结果。给这样的重载函数传递0或者NULL永远不会调用指针类型版本的重载函数:

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),调用是模棱两可的,因为从longint,从longbool,从longvoid*的转换被认为是一样好的。关于这个函数调用有趣的事情

在于一个矛盾,这个矛盾是指源代码中表现出来的意思(“我要用NULL作为参数调用f--一个null指针”)和它实际的意思(“我要用类似于integer的类型的参数调用f----不是一个null指针”)。这个违反直觉的行为导致了c++98程序员去避免指针类型和integral类型分别作为参数的重载函数的设计。这个指导方针在c++11中依旧保持合理,因为尽管有这个Item的建议,很有可能一些程序员

会一直使用0NULL,即使nullptr是更好的选择。

nullptr的优点是它不具有integral类型。事实上来说,它也不具有指针类型,但是你可以认为它是所有类型的指针。nullptr真实的类型是std::nullptr_t,并且,in a wonderfully circular definitionstd::nullptr_t被定义为nullptr的类型。类型std::nullptr_t隐式的转化为所有类型的原生指针,这就是让nullptr表现的像一个所有类型的指针的原因。

nullptr调用f的重载函数会调用void*重载类型(或者指针参数的重载类型)

因为nullptr不能被视为任何的integral类型:

f(nullptr); //调用f(void*)重载函数

使用nullptr而不是0或者NULL会避免重载决议令人惊讶的行为,但是这不是它唯一的优点。它会让代码更清楚,尤其是当auto变量被使用的时候。例如,假设你在代码中看到:

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

if(result == 0) {

...

}

如果你恰好不知道(或者不能轻易的发现)findRecord函数返回了什么,那么result究竟是指针类型还是integral类型是不那么清楚的。毕竟,0result比较的对象)能够被看成两种类型中的任何一种。另一方面,如果你看到下面的代码:

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

if(result == nullptr)

{

...

}

这里没有困惑的地方:result一定是一个指针类型。

当模板参与进来的时候nullptr更为出色。假设你有一些函数,这些函数只有当合适的mutex锁住的时候才能调用。每个函数都接受不同类型的指针:

 

int f1(std::shared_ptr<Widget> spw);

double f2(std::unique_ptr<Widget> upw);

bool f3(Widget* pw);

 

传入null指针用来调用代码看起来像这样:

std::mutex f1m,f2m,f3m;

using MuxGuard = std::lock_guard<std::mutex>;

...

{

MuxGuard g(f1m);

auto result = f1(0);

}

{

MuxGuard g(f2m);

auto result = f2(NULL);

}

{

MuxGuard g(f3m);

auto result = f3(nullptr);

}

前面两个调用没有使用nullptr让人悲伤,但是代码是有用的,这在某些方面是有价值的。然而,函数调用中的重复的模式--锁住mutex,调用函数,解锁mutex--更让人悲伤。这令人厌烦。这种重复的源代码是模板设计可以避免的,所以我们模板化这种模式:

 

template<typename FuncType,

typename MuxType,

typename PtrType>

auto lockAndCall(FuncType func,

MuxType& mutex,

PtrType ptr) -> decltype(func(ptr))

{

using MuxGuard = std::lock_guard<MuxType>;

MuxGuard g(mutex);

return func(ptr);

}

 

如果这个函数的返回类型(auto...->decltype(func(ptr)))让你抓耳挠腮,去看Item3item3解释了发生了什么。在c++14中你可以看到,返回类型可以被简单的decltype(auto)推断出来:

 

template<typename FuncType,

 typename MuxType,

        typename PtrType>

decltype(auto) lockAndCall(FuncType func,

MuxType& mutex,

PtrType ptr)

{

using MuxGuard = std::lock_guard<MuxType>;

MuxGuard g(mutex);

return func(ptr);

}

 

有了lockAndCall模板,调用者可以像这样写代码:

auto result1 = lockAndCall(f1,f1m,0); //error!

auto result2 = lockAndCall(f2,f2m,NULL); //error!

...

auto result3 = lockAndCall(f3,f3m,nullptr); //fine

是的,它们可以这么写,但是就像评论指示的那样,前两个调用不会通过编译。问题在于第一个调用,当0被传入时,模板类型推断推断它的类型。0的类型总是int,所以ptr的类型也是int。不幸的是,这意味着lockAndCallfunc的调用中,被传入了一个int值,这不和f1期望的std::shared_ptr<Widget>参数一致。传入lockAndCall函数的0企图去代表一个null指针,但是真正传入的是一个int,想要将int传入f1作为std::shared_ptr<Widget>是一种类型错误。用0调用lockAndCall函数的失败,是因为模板中,一个int被传入需要std::shared_ptr<Widget>参数的函数。

涉及NULL的调用的分析几乎是一样的。当NULL被传入lockAndCall,一个integral类型被推断为参数ptr,当ptr--一个int或者int-like类型--被传入f2中时会造成类型错误,因为f2期望着一个std::shared_ptr<Widget>

相反的,设计nullptr的调用没有问题。当nullptr被传入lockAndCall中,ptr的类型被推断为std::nullptr_t。当ptr被传入f3,有从std::nullptr_tWidget*的隐式转换,因为std::nullptr_t隐式的转换为所有的指针类型。

模板类型推断会将0NULL推断出“错误的类型”(例如,它们真实的类型而不是备选的代表着null指针的类型)这一事实是使用nullptr而不是0NULL最合理的原因(当你想要一个null指针的时候)。使用nullptr,模板不会有什么特殊改变。结合nullptr不会因为重载决议(0NULL会受到影响)受到影响这一事实,the case is ironclad。当你涉及一个null指针的时候,使用nullptr而不是0NULL

 

Things to Remember

 

1.相比于0NULL,使用nullptr

2.避免重载函数中有integral和指针类型的参数。

阅读全文
0 0
原创粉丝点击