Effective Modern C++ 条款8 用nullptr代替0和NULL

用nullptr代替0和NULL

你看看这样行不行,0字面上的意思是个int,而不是指针。如果C++代码中把0用作指针,那么C++会勉强地把0翻译为一个空指针,和一开始说的不太一样。C++的主要政策是,0是一个int值,而不是指针。

从实践上来说,NULL也是如此。NULL在细节上有一些不确定性,因为在实现上允许把NULL附给一个不同于int的整数类型(例如long)。这不常见,但在这里却无所谓,因为我们这里讨论的问题不是NULL的确切类型,而是0和NULL都不是指针类型。


在C++98中,基于上面的问题,使用带有指针类型或整数类型重载函数的会出乎我们意料。传递0或者NULL到重载函数中,指针作为参数的重载函数不会被调用:

void f(int);     //  f的3个重载函数
void f(bool);
void f(void*);

f(0);  // 调用f(int)
f(NULL);  // 可能不会通过编译,一般境况下调用f(int)

想要知道f(NULL)的行为就要求我们知道NULL的实现。如果NULL被定义为0L(即0是long类型),那么这次调用是引起歧义的,因为long转换成intlong转换成boollong转换成void*在编译器看来是一样好的。这个函数调用从代码上看(我要用NULL空指针调用f),和从实际上看(我用某个整数类型调用f)是相互矛盾的。这个违反直觉的行为使得C++98程序员的指导方针是避免重载函数带有指针类型和整数类型。这个指导方针在C++11中依然有效,因为尽管有这条款的建议,但是一些开发者依然使用0和NULL,即使nullptr更好。

nullptr的好处是它不是整数类型,但实话说,它也不是指针类型,但是你可以把它看作一个可以指向所有类型的指针。nullptr的实际类型是std::nullptr_tstd::nullptr_t可以隐式转换为所有类型的原生指针,这就使得nullptr像一个可以任何类型的指针。

nullptr调用上面f的重载函数,编译器会选择参数为void*的重载函数,因为nullptr不能被视为整数类型:
f(nullptr) // 调用 f(void*)


nullptr可以避免重载选择的问题,但它不只有这一个优点。它还可以让代码更清晰易懂,尤其是当你使用了auto变量。例如,你遇到了以下代码:

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

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

如果你碰巧不知道findRecord函数返回什么(或者很难查明),那么你可能不知道result是指针还是整数类型,但你别忘了,0在两种情况下都是可以继续运行的。另一方面,如果我们用以下代码:

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

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

这就没有二义性了:result一定是指针类型。


nullptr在涉及模板的时候十分有用。假如你有些函数只能在互斥锁被锁的时候才可以调用,而每个函数又有不同的指针类型:

int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f3(Widget *pw);

我们像这样传递空指针给函数:

std::mutex f1m, f2m, f3m;  // f1,f2, f3的互斥锁

using MuGuard = std::lock_guard<std::mutex>;
...
{
  MuGuard g(f1m);
  auto result = f1(0);
}
...
{
  MuGuard g(f2m);
  auto result = f2(NULL);
}
...
{
  MuGuard g(f3m);
  auto result = f3(nullptr);
}

前两个函数不使用nullptr让人感到悲伤,但函数是可以运行的,代码还是有些价值。但是这调用函数的模式——上锁,调用函数,解锁——更让人悲伤,这让人很烦恼,这种重复的代码可以设计一个模板来避免:

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)))让你头痛,那么你可以阅读条款3缓解头痛,那里解释了原理。如果在C++14,我们可以使用decltype(auto)

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

有了lockAndCall这个模板(任意一个版本),调用者可以写出这样的代
码:

auto result1 = lockAndCall(f1, f1m, 0);  // 错误
...
auto result2 = lockAndCall(f2, f2m, NULL);  // 错误
...
auto result3 = lockAndCall(f3, f3m, nullptr);  // 正确

就像注释里所说,前面两个函数无法通过编译。第一个函数调用的问题是,当传0给lockAndCall时,模板类型推断会把ptr推断成int类型,因此模板把int类型参数传给func函数,int类型和f1期待的std::shared_ptr<Widget>类型不兼容,就会出错。我们传递0想表示的是空指针,但是实际变成了传递一个普通的int,试图将int传递给接收std::shared_ptr<Widget>的f1是会报错的。这错误主要是因为在模板中,int的值传递给接收std::shared_ptr<Widget>的函数。

第二个函数在本质上错误相同。NULL被模板推断为整型数类型,那么参数为std::unique_ptr<Widget>的f2函数接收到的整数类型的值,所以报错。

作为对比,使用nullptr的那个函数调用没有出现问题。当nullptr传递给lockAndCall时,ptr的类型被推断为std::nullptr_t,当ptr传递给f3时,发生了std::nullptr_tWidget *的隐式转换,因为std::nullptr_t可以隐式转换为所有类型的指针。

事实上,最迫使你使用nullptr代替0和NULL的原因是,模板类型推断会为0和NULL推断出“错误”的类型当你想要一个空指针时。有了nullptr,模板就不会引起什么问题了。再想想nullptr不受重载函数选择策略影响,而0和NULL却容易受到影响。所以,当你要说明空指针时,用nullptr,而不是0或者NULL

总结

需要记住的两点:

  • 使用nullptr代替0和NULL
  • 避免整数类型与指针类型之间的函数重载。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值