Effective Modern C++概述(一)

(本文有 11 个 Item)

《Effective Modern C++》概要

  • 因为 Scott 大大的这本新书迟迟没有中译本,百般无奈之下,选择了英文原版阅读。
  • 记录自己阅读这本书的过程,以及一些细节,知道自己不可能读一遍就能将所有信息记下,故写成博文,方便自己及需要者查阅
  • 本文是我自己的感受,如果有想要体会完整的思想请自行查阅原书,最好配一本完备的C++的语法工具书(例如The C++ Programming Languag, 4th Edition--Bjarne Stroustrup),以便随时查询,当然还需要一个可编程平台。

代码尽量和原书相同,除非是想更详细的解释才会自行修改

Chapter 1 - Deducing Types

Item 1 - Understand template type dedution

  • 这节讲述了C++有两种类型推断(不许程序员介入), 自动类型推断(auto type dedution)模板类型推断(template type dedution), 两者的联系以及唯一的区别。
  • 首先介绍的是模板类型推断,这是来自(C++98)的特性了,现在以及在沿用,是最成功的特性之一。
  • 对于模板类型推断,一般形式可归纳为:

    template<typename T>
    void f(ParamType param);
    // 调用
    f(expr); // expr 可能是一条语句
    

    ParamType 是一种缩写,用来表示 可能 类型T会有一些限定词来修饰,例如const, & 之类的。

    例如 把ParamType 可以代表 const T &

  • 而此时,编译器在进行 模板类型推断 的时候会做两件事(两个推断的结果),它会判断 TParamType的类型分别是什么。当然,如果 ParamType 就是 T 那自然就相同了。
  • 因为 T 究竟是什么,取决于 ParamType
  • 进行推断的过程中,有三条编译器遵循的判断情况:
    1. ParamType 是一个指针或者引用,但不是一个 通用引用(Universal reference 不知道怎么翻译)。
    2. ParamType 是一个 Universal reference
    3. ParamType·不是一个指针也不是一个引用·
    4. 所以将模板类型推断分为三种情况。
  • 第一种情况比较简单

    • 如果此时 expr是一个引用,那就把引用去掉,剩下的就是 T

      template<typename T>
      void f(T & param);
      // 调用
      int         x  = 0;
      const int     cx = x;
      const int & rx = x;
      char strings[] = "Hello World!" // 原书没有
      f(x);  // T: int ; ParamType: int &
      f(cx); // T: const int; ParamType: const int &
      f(rx); // T: const int; ParamType: const int &
      f(strings); // 原书没有 T: char [13] ; ParamType: char(&)[13]
      

      如果是 void f(const T & param)呢,结果并没有什么太大的差别,只是 const 限定符略有不同而已。

      f(x);  // T: int ; ParamType: const int &;
      f(cx); // 同上
      f(rx); // 同上
      

      如果是指针 void f(T * param)

      const int * px = &x;
      f(&x); // T: int ; ParamType: int *
      f(px); // T: const int; ParamType: const int *
      

      以上为第一种情形

  • 第二种情况

    • 当参数类型设置为 Universal reference时比较奇怪,不可以常理推断。
    • 具体是,当传入参数为 lvalue 时,两个类型都为 lvalue reference, 否则如果传入一个 rvalue, 则用常规方法(第一种情况)处理。

      template<typename T>
      void f(T && param); // 这就是所谓的 Universal reference
      // 调用
      int x = 0;
      const int cx = x;
      const int & rx = x;
      f(x);  // T: int & ; ParamType: int &
      f(cx); // T: const int &; ParamType: const int &
      f(rx); // T: const int &; ParamType: const int &
      // 特别处, 此时传入一个 rvalue
      f(0); // T: int ; ParamType: int &&
            // 此时和第一种情况一致,由于传入的非引用,则 T 为 intParamType直接是 int &&
      
  • 第三种情况

    • 当参数类型既不是指针也不是引用时,此时就是所谓的 按值传参

      template<typename T>
      void f(T param);
      // 调用,参数和第一种情况一样
      f(x);  // T: int ; ParamType: int
      f(cx); // 同上
      f(rx); // 同上
      

      似乎很奇怪,但是实际上可以这么理解:既然都按值传递了,自然是拷贝一个副本了,既然是副本,那怎么操作都不会影响原来的对象(函数栈外的对象),所以直接用最简单的最普通的,不带任何限定符的方式就行。

      而对于引用而言只需要记住,任何作用在引用”身上”的操作,都会反射(用反射形容比较形象)到源对象上,所以相当于在传递 x

      从此处以及前方所述,可以看出 ,在进行类型推断的时候,所有的 限定符(const, volatile) 都会被忽略。

      还有一些情况,例如传入指针

      f(&x); // 原书没有,原书使用的是字符串的首地址,并讲了一遍指针和数组的差异。
             // T: int * ; Param: int *
      
  • 在篇末,作者提到,如果想要在 编译期 得到一个数组的长度,并且使这个得到的长度可以用在其他数组的声明上,可以如此:

    template<typename T, std::size_t N>
    constexpr std::size_t arraySize(T (&)[N]) noexcept // noexcept 可以帮助编译器更少的顾虑,也就是把优化策略更好的用在这个函数上。
    {
        return N;
    }
    

    其中 constexpr 是让返回值可以当成常量使用。所传的参数类型指的就是 某大小的数组引用,这个大小可以由 模板类型推断 来自动取得。

  • Item 1 结束

Item 2 - Understand auto type dedution

  • 这是 C++ 的第二种类型推断,由 C++11 标准引入,也就是关键字 auto
  • 前方提到, 模板类型推断自动类型推断 出了一个地方不同,其他处处一致,具体体现在哪里?比如 TParamType自动类型推断 中是怎么体现的?

    auto x = 27; // 其中 auto 相当于 T
                  // 而 x 前面的所有相当于 ParamType,可能还不太明显,换一个例子
    const auto cx = x;  // T: auto ; ParamType: const auto
    const auto & rx = x; // T: auto ; ParamType: const auto &
    

    注: T 当然不是 auto,只是此处为了体现作用,而故意写成 auto,实际为 int

  • 有了这种对应关系之后,可以将 模板类型推断 的三种情况直接用在 自动类型推断上

  • 第一种情况(是一个指针或引用,但不是 Universal reference)

    int x = 27;
    auto & rx = x; // T: int ; ParamType: int &
    const auto & crx = x; // T: int ; ParamType : const int & 
    

    可以看出这是第一种情况,但是并没有太大的意外

    const int cx = 27;
    auto & rx = x; // T: const int ; ParamType: const int &
    

    结合上述例子看,能够更好理解。大体就是将等号右端看作是要传入的对象,左端则是参数列表

  • 第二种情况(是 Universal reference)

    int x = 27;
    const int cx = x;
    auto && uref1 = x; // T: int &; ParamType: int &
    auto && uref2 = cx; // T: const int & ; ParamType: const int &
    auto && uref3 = 27; // T: int && ; ParamType: int &&
    

    解释和 Item-1 的一样,当传入的是 lvalue 时,就是该类型的引用,是rvalue时就是正常的第一种情况,是什么类型推断什么类型。

  • 第三种情况(既不是指针也不是引用)

    • 这种情况还是老样子,看作按值传参

      auto x = 27;   // T: int ; ParamType: int 
      const auto cx = x; // T: int ; ParamType: const int
      auto atd_x1 = x; // T: int ; ParamType: int 
      auto atd_x2 = cx; // T: int ; ParamType: int
      

      可以看出,忽略了所有的限定符。

  • 还有一种也是包含在上述三种情况中的情形,单独提出来,便是 数组和函数 的推断

    const char * name[] = "R. N. Briggs";
    auto arr1 = name;  // T: const char * ;ParamType: const char *
    auto & arr2 = name; // T: const char[13] ;ParamType: const char (&)[13]
    

    如果是函数

    void someFunc(int, double);
    auto func1 = someFunc; // func1的类型为 void(*)(int, double)
    auto & func2 = someFunc; // func2的类型为 void(&)(int, double)
    
  • 不同点

    • 两种类型推断唯一不同的地方一种新的 初始化方式,许多书本译作 列表初始化,即用一个花括号,包裹一系列同类型的值,赋给等号左边的对象

      int x1 = 27; // C++98
      int x2(27); // C++98
      int x3 = {27}; // C++11
      int x4{27}; // C++11
      

      这是我们熟悉的初始化语法,C++提供了这四种方式。如果改成 auto 呢?

      auto x1 = 27; // T: int 
      auto x2(27);  // T: int
      auto x3 = {27}; // T: std::initializer_list<int>
      auto x4{27}; // T: std::initializer_list<int>
      

      看起来似乎是一样的,实际上我们在使用的时候,前两者是最常见的,后两者要出现只出现咋需要同时传入多个类型相同的值时才使用。

      但是,自动类型推断能够直接接受这种语法,模板类型推断却不行!

      auto x = {11,23,9}; // 可以
      
      template<typename T>
      void f(T param);
      //调用
      f({11, 23, 9}); // 这是错误的!无法通过编译
      

      这就是两者的区别,当一个类型推断使用的是 模板类型推断 时(例如函数返回值),其无法直接推断出std::initializer_list<T>这种列表形式,当然事无绝对

      template<typename T>
      void f(std::initializer_list<T> param);
      // 调用
      f({11, 23, 9}); //这样是可以的。每次传入一个初始化列表即可
      

      就像上面所说,当返回值是一个列表时,模板类型推断 是无法工作的,除非显式说明,返回的是一个列表。

      这里提到了显式说明,自然可以用 类型推断来替代,但是因为此时使用的是 模板类型推断,所以除了使用 auto进行“占位”(占返回值类型的位), 还需在后面添上一些东西

      // 原书没有
      template<typename T>
      auto f(std::initializer_list<T> param)
      -> decltype(param)
      {
          auto tmp = param;
          // do some work
          return tmp;
      }
      

      似乎很奇怪,但是这个语法的确反人类,不过还好在 C++14 中修正过来了。

      所谓修正就是不需要再参数后面添加这么一个吊车尾的 -> decltype(param)样式,而是直接用 auto 作为返回值即可(因为在C++11中只有 部分lambda(单个返回语句情况下) 允许不加 -> 而自动推断返回值类型)。

  • Item 2 结束
  • 总结
    • 自动类型推断 和 模板类型推断 总是相同的,除了前者假设一个 列表初始化语法的出现总是代表std::initializer_list<T>的类型,但是后者却不是
    • 在作返回值类型推断的时候,总是使用 模板类型推断,而不是 自动类型推断。

Item 3 - Understand decltype

  • 首先decltype的确是一个很奇怪的东西,但是从种种解释来看,他似乎是C++中一个很中规中矩的一个 关键字,因为它总是能够把 正确的类型不加修改 的返回给你。
  • 但是在初试这个关键字的一段时间之后,总能出现让使用者抓狂的现象

    例如:“啊!怎么是引用!我明明返回的是一个对象!”

例如:“啊!怎么这回不是引用啦!”

const int i = 0; //decltype(i) 返回的类型是 const int

bool f(const Widget & w) // decltype(w) 返回 const Widget &
Widget w; // decltype(w) 返回 Widget
f(w);     // decltype(f(w)) 返回 bool

vector<int> tmp; tmp.push_back(10); // 改造一下原文程序
tmp[0] = 20; // decltype(tmp[0]) 返回 int &

综上而观,并没有什么出乎意料的地方,(最后一个返回 int & 是因为 vector<T>这个容器重载了运算符 [],其返回值是 T &),所以总的来说还是没有出乎我们意料之外。

  • 在此处,原作者引出了一个问题,实际上也是 Item 2 里提到的,怎样让函数返回值也可以使用 类型推断?里要区分 C++11 和 C++14 标准的区别了,当然前者的方法在后者也可以使用,但是后者的方法却不一定能在前者内使用。
  • 引用 Item 2 末尾的例子(自行添加的),这是 C++11 标准的方法,如果将其改写成 C++14 的形式:

    // C++14 
    template<typename T>
    auto f(std::initializer_list<T> param)
    {
        auto tmp = param;
        // do some work
        return tmp;
    }    
    

    只需要简单的将 “吊车尾” 删掉,这段代码在支持 C++14 标准的编译器的编译下是通过的。

  • 那作者提出这个问题是为什么?实际上看到这里,都忽略了一个细节,那就是在 返回值的类型推断中,使用的是模板类型推断,无论什么类型推断,都会有一个事实那就是,将 限定符 删除。

  • 这会导致什么呢,如果我们返回的是一个简单的对象,自然没有什么问题,如果我们返回的是引用呢?(实际上这只影响 C++14的写法,对C++11的写法 并没有这种问题,因为我们所讨厌的“吊车尾”帮我们避免了这种问题的发生):

    // C++11
    template<typename Container, typename Index>
    auto authAndAccess(Container & c, Index i)
     -> decltype(c[i])
    {
        authenticateUser(); //做一些事
        return c[i];
    }
    // -----------------------------------------
    // C++14
    template<typename Container, typename Index>
    auto authAndAccess(Container & c, Index i)
    {
        authenticateUser(); //做一些事
        return c[i];
    }            
    

    诚然第二种写法比较简洁,打呢带来了一些隐患,就是返回值类型没有那么“明确”了,所以此处我们如果向参数 c 传入一个容器对象,例如 vector,那么最后返回的时候,C++11 版本返回的是一个引用, C++14 版本返回的则是一个 rvalue(类型推断把 限定符 给忽略了!),也就是说我们的原意可能已经被编译器给理解错了!这回导致什么结果呢?

    std::deque<int> d;
    //... 加入元素
    // C++11
    authAndAccess(d, 5) = 10; // 可行,因为是引用
    // C++14
    authAndAccess(d, 5) = 10; // 错误!连编译都无法通过
    

    无法通过编译的理由,上面已经叙述过了,实际上就是 Item 1/2的情况。

    解决的办法,还是要靠 decltype

    // C++14
    template<typename Container, typename Index>
    decltype(auto) authAndAccess(Container & c, Index i)
    {
        authenticateUser(); //做一些事
        return c[i];
    }        
    

    因为 decltype 会如实的将类型返回给使用者,所以这时候,返回值就是 int& 了。

    这样,C++14的版本就能愉快的工作了。之后原文还介绍了更深入的知识 std::forward,这个不记录,在 Item 25有专门讲解。

    什么时候 decltype 会出乎意料?

    // C++14
    // 原书的例子不太好,但是用来解释却是最好的
    decltype(auto) f()
    {
        int x = 0;
        //...
        return x;
    }
    

    毫无疑问,返回值类型就是 int

    decltype(auto) f()
    {
        int x = 0;
        //...
        return (x);
    }
    

    这回返回值类型就是 int &

    原因就是用 () 包裹起来在C++看来,这就不再是一个单纯的对象,所以返回了引用。

  • 这里需要补充,一旦多加了一层括号

    • 当括号内是 lvalue 或最终结果是 lvalue时,返回的是该类型的引用
    • 当括号内是 rvalue 或最终结果是 rvalue时,返回的是该类型本身
  • 总结

    • decltype 几乎总是没有任何修改的返回正确的类型
    • 如果是一个 lvalue expression 的话,那就返回表达式结果类型的引用。
    • C++11写法 在函数返回值类型推断方面不必太在意decltype的这两个规则,因为“吊车尾”帮我们避免了问题,但是 C++14 的写法就必须要注意。
  • Item 3 结束

Item 4 - Know how to view deduced types

  • 这一节讲述,如何才能知道推断出来的类型是否是我们想要的。
  • 第一种方法 : 某些 IDE 自带,这个不再记录
  • 第二种方法 : 通过语法错误产生的编译信息来确定。

    • 给出一个范例使用

      template<typename T>
      class TD; // 故意不写完整
      // 使用的时候
      TD<decltype(x)> xType; // x 即为想知道什么类型的对象
      TD<decltype(y)> yType;
      
  • 第三种方法 : 使用库里的一个方法 typeid

    #include <typeinfo>
    ...
    cout << typeid(x).name() << endl;
    cout << typeid(y).name() << endl;
    

    这会在终端输出类型信息,只不过这个信息略微有些晦涩,可以自行查询资料

  • 但是,很遗憾第一种和第三种方法往往不能正确的打印出正确的类型,换句话说就是不太可靠。Boost 库中,有一个可靠的实现,Boost.TypeIndex 可以使用。原书例子不在记录。

  • 所以本节就这么多例子信息,旨在告诉读者,要善用编译器。
  • Item 4 结束

Chapter 2 - auto

Item 5 - Prefer auto to explict type declarations

  • 用几个例子,讲述 auto 相对于 显式类型声明的 优势
  • 它能缓解由于错误声明带来的隐性错误。但是由前面可知,有时候 auto 的推断并不一定都如我们所愿,所以auto这个选择只是作为一种推荐,而不是硬性要求我们使用。
  • 例子

    • C/C++ 中,常常出现声明变量后,忘记初始化的错误,但是用 auto 进行替换的话,可以有效避免这种情况的出现。

      int x1; // 未初始化,但编译通过
      auto x2; // 未初始化,编译错误!
      auto x3 = 0; // 这样才行
      
    • 匿名函数和闭包的例子,在 C++ 中有一个模板,可以用来存储任意类型的 可调用对象, 叫做 std::function<T>,这不是唯一的方法,我们可以选择使用 auto 来代替

      // auto 来创建对象存储匿名函数
      // C++11
      auto derefUPLess = 
          [](const std::unique_ptr<Widget> & p1,
            const std::unique_ptr<Widget> & p2)
          { return *p1 < *p2; };
      // C++14
      auto derefUPLess = 
          [](const auto & p1, 
            const auto & p2)
          { return *p1 < *p2; };
      
      // 用 function<> 来声明存储
      function<bool(const unique_ptr<Widget> &, 
                  const unique_ptr<Widget> &)>
      derefUPLess = 
          [](const std::unique_ptr<Widget> & p1,
            const std::unique_ptr<Widget> & p2)
          { return *p1 < *p2; };
      

      autofunction 创建的是完全不同的两个东西,一般而言前者要比后者省一些,后者作为一种通用的”模板”存在,最开始具有一定的大小,如果不足以容纳要接受的对象的话,会在堆上申请空间来存储

      所以相对而言, auto 的效率会高于 function

  • 例子

    • 有时候不经意的想当然也会导致一些小隐患

      vector<int> Mat;
      ...
      unsigned size = Mat.size();
      

      这段小代码,看上去没有什么错误

      Mat.size()的返回值类型是 vector<int>::size_type,在 32位 操作系统下,其大小为 32位, 在 64位 操作系统下为 64位,而 unsigned则都是 32位。这会导致如果移植到 64位操作系统时,可能在某一时刻出现问题。

      auto size = Mat.size(); // 这样做就行了
      
  • 例子

    • 范围for循环 (range-for loop)

      unordered_map<string, int> m;
      ...
      for(const pair<string, int> & p : m)
      {
          // 做一些事
      }
      

      这个代码有什么问题?首先,unordered_mapKey-Value 类型并不是string, int 而是 const string, int。这往往会被忽略

      那忽略了能怎样?首先编译器在此时会尽量满足程序员的要求(尽管此时的要求似乎并不是我们所想要的),也就是拷贝一个m的副本,供这个新的 pair 使用。

      当然,此处编译器不会让我们犯下非常大的错误,这里产生的副作用就是在每次循环结束的时候,那个拷贝的副本都将被销毁,下一次循环开始后又被创建,如此往复。

      可以猜得到,如果将 const pair<string, int>const 去掉,编译将不会被通过,因为如果没有了这个const,那么我们就能通过这个pair对象修改这个unordered_mapKey,这与设计理念相驳,所以除非你这么写

      for(pair<const string, int> & p : m)
      

      否则,编译是无法通过的。

      所以,此时可以选择 auto 来帮助完成这些暗藏陷阱的工作

      for(const auto & p : m)
      

      记住要自己加上限定符

  • 但是一定要切记,auto 使用时会发生的事情。
  • Item 5 结束

Item 6 - Use the explictly typed initializer idiom when auto deduces undesired types

  • 本篇几乎为上述篇幅的一个小补充,叙述的是,当 auto 返回了一个出人意料的结果时,应该改用显式的类型声明。
  • 何时会出现出人意料的结果? 文中给出了一种叫做 代理(Proxy) 的模式下,会有出人意料的结果
  • 简单来说就是,例如一个 std::vector<bool> 的使用,几乎大部分人都知道 std::vector<bool>是一个模板特例化的产物,且一般而言 operetor[]操作返回的是容器元素的引用,但是在 C++ 中, bit是不允许被引用指向的,也就是说bool&是不存在的。所以,使用了一种称为代理的机制,返回的真正类型是 std::vector<bool>::reference,这是一个内嵌类(vector中)

    std::vector<bool> isTriangle;
    ...
    bool rights = isTriangle[1]; // 正确,有类型转换发生,但是合法的
    auto wrongs = isTriangle[1]; // 也是能够编译通过,但是不太好
    

    但是 wrong 的类型是 std::vector<bool>::reference

    如果这个 vector 是个临时对象呢?

    // 假设有一个函数 std::vector<bool> retVec(int para);
    bool rights = retVec(2)[1]; // 可以
    auto wrongs = retVec(2)[1]; // 绝对不能这么做
    

    这取决于,std::vector<>::reference 的实现了,有一种实现是返回一个指向区域的类似指针的东西,假设如此,那么当这个临时对象被销毁之后呢?所以 wrongs 是一种不确定行为。

    当然有一种做法就是给它加上类型转换

    auto change = static_cast<bool>(retVec(2)[1]);
    
  • Item 6 结束

Chapter 3 - Moving to Modern C++

Item 7 Distinguish between () and {} when creating objects

  • 前者代表 () 后者代表 {}
  • 首先,后者能让初始化一个容器,或者类似事物,在某种程度上更加简便

    // 只需
    std::vector<int> index1{1, 2, 3, 4, 5};
    // 不需
    std::vector<int> index2;
    for(int i = 0; i < constance; i++)
    {
        index2.push_back(i);
    }
    
  • 不可拷贝的对象的创建,有时候必须借用这种机制

    std::atomic<int> al1{ 0 }; //Okay
    std::atomic<int> al2(0); //Okay
    std::atomic<int> al3 = 0; //这就不行了 
    
  • 对于可能在不经意间造成的 类型缩窄(narrowing convertions),可以尝试使用后者来进行杜绝,也就是说,让编译器监督我们是否有类型被缩窄了。

    double x, y, z;
    ...
    int sum{ x + y + z }; // 错误!花括号内表达式的结果是 double
    int sum2(x + y + z); // 这样可以
    int sum3 = x + y + z; // 也可以
    
  • 再有一点,涉及到了 C++ 的缺陷,那就是有时候一个表达式可能会引起理解上的混淆(于程序员而言)

    // 函数声明 还是 函数调用?
    Widget w2(); // 这是什么?
    Widget w1(10); // 这是调用一个 以 10 为参数的Widget的构造函数
    

    很多人看完第二个,会直觉的默认第一个就是调用 Widget的无参构造函数,其实不是,在编译器看来(至少目前是)这是一个函数声明

    那如果要调用无参数构造函数怎么办?

    // 新标准下,可以使用后者
    Widget w2{}; // 这就调用了 无参的构造函数
    

    当然,后者也有它不好的地方

  • 当构造一个类的时候

    class Widget{
    public:
        Widget(int i, bool b);
        Widget(int i, double b);
    };
    // 调用
    Widget w1(10, true); // 调用第一个
    Widget w2{10, true}; // 第一个
    Widget w3(10, 5.0);  // 第二个
    Widget w4{10, 5.0};  // 第二个
    

    看起来似乎没什么问题,如果加一个构造函数

    class Widget{
    //.. 前方一致
        Widget(std::initializer_list<long double> il); //第三个构造函数
    };
    // 调用
    Widget w1(10, true); // 调用第一个
    Widget w2{10, true}; // 第三个
    Widget w3(10, 5.0);  // 第二个
    Widget w4{10, 5.0};  // 第三个    
    

    如果此时再多加一个隐式的转换(重载),那连拷贝构造函数都会出现意外

    class Widget{
    //.. 前方一致
        operate float() const; // 作用是将这种类型(Widget)转换成 float
    };
    // 使用
    Widget w5(w4); // 调用拷贝构造函数
    Widget w6{w4};    // 调用第三个构造函数
    Widget w7(std::move(w4)); // 调用移动拷贝构造函数
    Widget w8{std::move(w4)}; // 调用第三个构造函数
    

    更让人吃惊的是,即使这么做会让编译无法通过,它(编译器) 依旧会选择匹配以初始化列表为类型的构造函数

    // 将第三个构造函数改成
    class Widget{
    // 其他一致
        Widget(std::initializer_list<bool> il); // long double -> bool
    };
    Widget w{10, 5.0}; // 这回,编译出错了!明明有更好的选择,但是出错了。
    

    只有当没有任何途径可以转换类型的情况下,编译器才会舍弃倔强,回到普通的构造函数选择中

    // 将第三个构造函数改成
    class Widget{
    // 其他一致
        Widget(std::initializer_list<std::string> il);
    };
    // 调用
    Widget w1(10, true); // 调用第一个
    Widget w2{10, true}; // 第一个
    Widget w3(10, 5.0);  // 第二个
    Widget w4{10, 5.0};  // 第二个
    

    回到最初,如果使用一个空的 {} 呢?会发生什么,是调用第三个构造函数,还是调用无参构造函数?

    答案是调用 无参的构造函数

  • 不记录一个和模板有关的信息,以及如何用 {} 传递空参数的记录

  • Item 7 结束

Item 8 - prefer nullptr to 0 and NULL

  • 首先,对于 0NULL 的使用,是历史遗留的问题,依靠类型转换来勉强维持正确性,但他们究竟是什么类型,特别是后者,根本无法确切得知
  • 0 毫无疑问是 int,而 NULL 可以是int,也可以是其他类型,具体看编译器实现。
  • nullptr 则是一种新的类型,用来代替前两者,类型是std::nullptr_t,它能够自动转换成通用指针类型。所以它是真正意义上的空指针

    void f(int);
    void f(bool);
    void f(void*);
    f(0); // 调用第一个
    f(NULL); // 可能调用第一个,也可能都无法编译通过
    f(nullptr); // 调用第三个
    
  • 在模板使用中,就会严格很多

    template <typename FuncType,
                typename MuxType,
                typename PtrType>
    auto lockAndCall(FuncType func,
                        MuxType mutex,
                        PtrType ptr) -> decltype(func(ptr))
    {
        using MuxGuard = std::lock_guard<std:mutex>;
        MuxGuard g(mutex);
        return func(ptr);
    }
    

    在对这个函数使用的时候,如果我们再将 0 或者 NULL 当作空指针传递的话,就会发生错误,原因就在于,模板类型推断已经限制了进一步的转换。

  • 最后一点就是,要尽量避免 整形 和 指针类型的 函数重载。

  • Item 8 结束

Item 9 - Prefer alias declarations to typedefs

  • C++11 后增加一个新的语法,作用和 typedef 十分相似,但是按照说法,它能做的更多更好。
  • 首先是两者的共同点

    typedef
    std::unique_ptr<std::unordered_map<std::string, std::string>>
    UPtrMapSS; // 注意此处有连续的 >> 如果在 C++98下会编译出错,加一个空格即可     
    using UPtrMapSS = 
    std::unique_ptr<std::unordered_map<std::string, std::string>>;
    

    两种方法最后的效果是一样的,有时候 using 更加清晰

    typedef void (*FP)(int, const std::string &);
    using FP = void (*)(int, const std::string &);
    

    明显那看出第二种写法更加清晰,当然也有人不这么认为,所以在这方面 using 和 typedef 对比并没有谁更优。

  • 真正区分开两者的地方是在模板方面

    template<typename T>
    using MyAllocList = std::list<T, MyAlloc<T>>;
    // 使用
    MyAllocList<Widget> lw; 
    

    如果是在 C++98,只能

    template<class T>
    struct MyAllocList{
        typedef std::list<T, MyAlloc<T>> type;
    };
    // 使用
    MyAllocList<Widget>::type lw;
    

    既然使用了 ::,那么自然会引申出在 模板类中的情况:

    例如,在一个模板类中使用上述这个模板的情况

    template<typename T>
    class Widget{
    private: 
        typename MyAllocList<T>::type list; // 出现了typename
    };
    

    此处 typename 的作用就是告诉编译器,接下来这个东西是一个依赖于T的类型,而不是其他东西,如果没有这个 typename 在前方修饰那么编译器就不明白这是个什么东西,因为可能这回事某个类中的 成员对象,考虑模板特例化的情形。

    那使用 using 的语法就可以直接使用,而不用加 typename了。

    template<typename T>
    class Widget{
    private: 
        MyAllocList<T> list;
    };        
    

    这里需要注释一下,不要将 using 想象的太乐观,很多情况下即使用了 using,也还是要用 typename 的。例如

    // 补例
    class outerClass{
    public:
        using size_type = std::size_t; // 作为一种约定俗成的类似接口的规范
    private:
        std::size_t capacity;
    };
    template<typename T>
    class Widget{
    public:
        typename T::size_type call_me() {...}
    private:
    };
    

    这个例子里,即使用了 using ,照样需要告诉编译器,这是一个类型,而编程中经常遇到这种情况。

  • 接下来涉及了模板元编程,后续再记录。
  • Item 9 结束

Item 10 - Prefer scoped enums to unscoped enums

  • 本节讲述的是要尽量使用新标准下的 Scoped Enum 代替原来标准的 UnScoped Enum

    // Unscoped enum
    enum Color {black, red, white};
    auto white = false;    // 编译错误
    
    // Scoped enum
    enum class Color {black, red, white};
    auto white = false;    // 编译通过
    
    //所以
    enum class Color {black, red, white};
    Color c1 = white; // 错误
    Color c2 = Color::white; // 正确
    auto  c3 = Color::white; // 正确
    

    区别就是这样,当然还有使用上的差异,以及行为上的差异。

    首先,前者具有 强作用域意识,什么意思,就是没有命名污染。所以第一句编译错误,因为 white 已经使用过了!而对于第二句中的 white 在全局里并没有出现过,只出现在Color::white

  • 对于 Unscoped enum 来说,其限制几乎没有

    enum Color {black, white, red};
    std::vector<std::size_t> primeFactors(std::size_t x);
    
    Color c = red;
    ...
    if(c < 14.5){ // 发生了隐式类型转换
        auto factor = primeFactors(c);
    } 
    

    这段代码编译通过,因为 Unscoped enum 可以隐式转换为数值类型,包括整形和浮点数。但是如果换成 Scoped enum 就不行,必须使用强制类型转换

    enum class Color {black, white, red};
    Color c = Color::red;
    ...
    if(c < 14.5) { // 编译失败,因为 c 没办法转换成 double类型
    ...
    

    解决办法就是:

    if(static_cast<double>(c) < 14.5) {
    ...
    
  • 其次,Unscoped enum 其不可以只声明不定义,因为编译器无法得知它的大小,至少在 C++98标准及以前无法办到

    enum unreach; // 这是不行的
    enum class reach; // 可以,因为编译器知道它默认为 sizeof(int)
    

    当然,在 C++11 标准下, 前者同样可以只声明不定义,只需要小小改动:

    enum unreach: std::int8_t; // 显式提供大小即可
    

    这个语法对 Scoped enum 同样有效,意思就是显式规定了枚举成员的大小类型。

  • Item 10 结束

Item 11 - Prefer delete function to private undefined ones

  • 一共是三个理由
  • 第一个理由,是最熟悉的,如何禁止一个类的拷贝,即拷贝构造函数,例如istream,这是最常用的位置,在C++98时代,如果想要禁止一个类的拷贝,那就只能使用一些技巧,将拷贝构造函数声明为 private 级别来达到禁止一定程度上的访问,别无他法。
  • 一个类会为我们自动生成一些函数(使用的时候生成),其中就包括和拷贝相关的,拷贝构造函数拷贝赋值构造函数(即重载 = 运算符),在C++11中我们就不需要投机取巧,而是有一个明确的机制来实现这个功能,那就是 =delete

    // C++98
    template <class charT, class traits = char_trait<charT> >
    class basic_ios : public ios_base {
    public:
    ..
    private:
      basic_ios(const basic_ios &);
      basic_ios& operator=(const basic_ios &);
    };
    

    唯一的办法就是将函数声明为 private 级别,并且不定义它,着能在一定程度上禁止访问该函数,但是如果是 成员函数 或者 友元函数/类 就无能为力了。所以说这是一种不完善的技巧。

  • 而在C++11里,我们可以:

    // C++11
    template <class charT, class traits = char_trait<charT> >
    class basic_ios : public ios_base {
    public:
      basic_ios(const basic_ios &) = delete;
      basic_ios & operator=(const basic_ios&) = delete;
     ..
    };
    

    这样就完美解决了上述问题,无论怎么样都无法访问这个函数,并且一旦有某个地方试图使用它们,就会生成编译错误,帮助我们及早发现问题。

  • 第二个理由

  • 用在一般的函数上,C++这中有重载和类型转换这一个概念,而这有时候会带来麻烦:

    bool isLucky(int number);
    

    这是一个很普通的函数,如果我们这么调用

    isLucky('a');
    isLucky(true);
    isLucky(3.5);
    

    这样能够编译通过并且运行,因为参数都被转换成 int 了,但是有时候我们不需要其他类型该如何做?

    bool isLucky(char) = delete;
    bool isLucky(bool) = delete;
    bool isLucky(double) = delete;
    

    这样就行了,再编译上面的调用,就无法通过了,也就是禁止了这些隐式转换的可能。

  • 这个对于模板函数同样通用,例如想要禁止某一种类型的特例化,直接 =delete就行。

  • 最后一种情况,实际上是出现在类内的问题,和模板特例化有关。

  • 在一个类中,如果有一个模板函数,我们不希望出现某种类型的特例化,并且想禁止它,一般会想到可不可以将模板特例化的声明写在 private 下,以此来达到禁止的目的,但是实际上是不行的。
  • 原因是,模板函数和它的模板特例化必须是在同一个命名空间中,而不能在不同的 访问级别(public,pretected,private) 中,所以此法行不通。

    class Widget {
    public:
      ...
      template <typename T>
      void processPointer(T* ptr) { ... }
    
    private:
      template <>
      void processPointer<>(void *)
    

    这样是不行的,编译都无法通过

  • C++11中,我们可以:

    class Widget {
    public:
      ...
      template <typename T>
      void processPointer(T* ptr) { ... }
    ...
    };
    
    template<>
    void Widget::processPointer<void>(void*) = delete;
    

    这样就关闭了这个函数的 类型特例化

  • 在这里有一个需要特别注意,就是在 delete 一个模板函数的时候,删除函数的某一个特例话只意味着和它完全一致的参数的那种特例化会被禁止,如果加了限定词那就无效了。

    template<>
    void Widget::processPointer<const volatile double>(const volatile double*) = delete; // 第一个
    template<>
    void Widget::processPointer<const double>(const double*) = delete; // 第二个
    template<>
    void Widget::processPointer<double>(double*) = delete;  // 第三个
    

    从上至下,一一对应范围:

    Widget ObjTest;
    double testDouble = 1.0;
    double * pDouble1 = &testDouble;
    const double * pDouble2 = &testDouble;
    const volatile double * pDouble3  = &testDouble;
    ObjeTest.processPointer(pDouble1); // 第三个会编译错误
    ObjeTest.processPointer(pDouble2); // 第二个会编译错误
    ObjeTest.processPointer(pDouble3); // 第一个会编译错误
    

    所以如果想禁止某一类型的所有情况,就需要写三个 =delete

  • 任何函数都可能会被 =delete

  • Item 11 结束

转载注明: http://www.wushxin.top/2015/12/15/Effective-Modern-Cpp-%E6%A6%82%E8%BF%B0.html