effective_modern_c++
  • 前言
  • 1 类型推导
    • 条款1 理解模板类型推导
    • 条款2 理解auto类型推导
    • 条款3 理解decltype
    • 条款4 知道如何查看类型推导
  • 2 auto
    • 条款5 优先使用auto而非显式声明
    • 条款6 当auto推导出非预期类型时应当使用显式的类型初始化
  • 3 使用高级cpp特性
    • 条款7 创造对象时区分()和{}
    • 条款8 优先使用nullptr而不是0或者NULL
    • 条款9 优先使用声明别名而不是typedef
    • 条款10 优先使用作用域限制的enum而不是无作用域的enum
    • 条款11 优先使用delete关键字删除函数而不是private却又不实现的函数
    • 条款12 使用override关键字声明覆盖的函数
    • 条款13 优先使用const_iterator而不是iterator
    • 条款14 如果函数不抛出异常请使用noexcept
    • 条款15 尽可能使用constexpr
    • 条款16 让const成员函数线程安全
    • 条款17 理解特殊成员函数的生成
  • 4 智能指针
    • 条款18 对于独占资源使用std::unique_ptr
    • 条款19 对于共享资源使用std::shared_ptr
    • 条款20 当std::shared_ptr可能悬空时使用std::weak_ptr
    • 条款21 优先考虑使用std::make_unique和std::make_shared,而非直接使用new
    • 条款22 当使用Pimpl惯用法,请在实现文件中定义特殊成员函数
  • 5 右值引用和完美转发
    • 条款23 理解std::move和std::forward
    • 条款24 区分通用引用和右值引用
    • 条款25 对右值引用使用std::move,对通用引用使用std::forward
    • 条款26 避免在通用引用上重载
    • 条款27 熟悉通用引用重载的替代方法
    • 条款28 理解引用折叠
    • 条款29 假定移动操作不存在,成本高,未被使用
    • 条款30 熟悉完美转发失败的情况
  • 6 lambda表达式
    • 条款31 避免使用默认捕获模式
    • 条款32 使用初始化捕获来移动对象到闭包中
    • 条款33 对auto&&形参使用decltype以便std::forward它们
    • 条款34 考虑lambda而非std::bind
  • 7 并发api
    • 条款35 优先考虑基于任务的编程而非基于线程的编程
    • 条款36 如果有异步的必要请指定std::async::launch
    • 条款37 使std::thread在所有路径最后都不可结合
    • 条款38 关注不同线程句柄的析构行为
    • 条款39 对于一次性事件通信考虑使用void的futures
    • 条款40 对于并发使用std::atomic,对于特殊内存使用volatile
  • 8 些许调整
    • 条款41 对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递
    • 条款42 考虑使用置入代替插入
Powered by GitBook
On this page
  1. 3 使用高级cpp特性

条款8 优先使用nullptr而不是0或者NULL

0字面上是一个int类型,而不是指针,这是显而易见的。C++扫描到一个0,但是发现在上下文中仅有一个指针用到了它,编译器将勉强将0解释为空指针,但是这仅仅是一个应变之策。C++最初始的原则是:0是int而非指针。

经验上讲,同样的情况对NULL也是存在的。对NULL而言,仍有一些细节上的不确定性,因为赋予NULL一个除了int(即long)以外的整数类型是被允许的。这不常见,但是这真的是没有问题的,因为此处的焦点不是NULL的确切类型,而是0和NULL都不属于指针类型。

在C++98中,这意味着重载指针和整数类型的函数行为会令人吃惊。传递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整形),函数的调用是有歧义的,因为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不能被视为整数类型的:

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

使用nullptr而不是0或者NULL,可以避免重载解析上的令人吃惊的行为,但是它的优势不仅限于此。它可以提高代码的清晰度,尤其是牵扯到auto类型变量的时候。举个栗子:

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

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

如果你不能轻松的看出findRecord返回的是什么,要知道result是一个指针还是整数类型并不是很简单的。毕竟,0(被用来测试result的)既可以当做指针也可以当做整数类型。另一方面,如果是下面这个栗子:

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 MuxGuard = std::lock_guard<std::mutex>;
...
{
    MuxGuard g(f1m);  // 为f1锁定互斥量
    auto result = f1(0);  // 将0当做空指针作为参数传给f1
}  // 解锁互斥量

...

{
    MuxGuard g(f2m);  // 为f2锁定互斥量
    auto result = f2(NULL);  // 将NULL当做空指针作为参数传给f2
}  // 解锁互斥量

...

{
    MuxGuard g(f3m);  // 为f3锁定互斥量
    auto result = f3(nullptr);  // 将nullptr当做空指针作为参数传给f3
}  // 解锁互斥量

在前两个函数调用中没有使用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,  // C++14
                           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,模板通过类型推导得知它的类型。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

  • 避免整数类型和指针类型之间的重载

Previous条款7 创造对象时区分()和{}Next条款9 优先使用声明别名而不是typedef

Last updated 2 years ago