Effective Modern Cpp 概述(二)

(Item 15)

Item 12 - Declare overriding functions override.

  • 介绍了一个鲜为人知的 C++11 新特性,针对一个类成员函数的

    class Widget {
    public:
      ...
      void doWork() &;   // 只有当 *this 是 lvalue 时才调用
      void doWork() &&;  // 只有当 *this 是 rvalue 时才调用
    };
    ...
    Widget makeWidget(); // 制造一个临时的 Widget
    // 调用
    Widget w; // 默认构造函数创建
    w.doWork(); // 调用第一个版本(lvalue)
    makeWidget().doWork(); // 调用第二个版本
    
  • 对于 重写重载,这两个极易混淆,或者说不是混淆而是十分容易不小心就写错了,的功能,在 C++98 之前我们只能选择相信程序员写下的代码是准确无误的。有几个判断 重写 的条件

    1. 基类里的该函数必须是 virtual
    2. 基类里的该函数和派生类里的该函数的 函数名 必须相同
    3. 基类里的该函数和派生类里的该函数的 参数类型 必须相同
    4. 基类里的该函数和派生类里的该函数的 返回值类型 必须相同或者可以兼容(即继承关系)
    5. 基类里的该函数和派生类里的该函数的 const 属性必须相同
    6. 基类里的该函数和派生类里的该函数的 引用限定词 必须相同(C++11新增)
  • 上述关系缺一不可。

    class Base {
    public:
      virtual void mf1();
      virtual void mf2(int x);
      virtual void mf3() &;  // C++11 
      virtual void mf4() const;
    };
    // C++98
    class Derived : public Base {
    public:
      virtual void mf1();
      virtual void mf2(unsigned int x);
      virtual void mf4() const;
    };        
    // C++11
    class Derived : public Base {
    public:
      virtual void mf1() override;
      virtual void mf2(unsigned int x) override;
      virtual void mf3() & override;
      virtual void mf4() const override;
    };
    

    其中 Derived 类中的 重写 函数前面的 virtual 不是强制性要求。相比于C++98 而言对了一个关键字override,其作用就是帮助我们在编译期检查到是否在重写的时候出了差错

    C++98 版本的mf2函数可以通过编译,但是 C++11 版本的mf2函数便无法通过编译,因为override告诉编译器这是一个重写函数,但是实际上我们的 mf2 并不符合要求。

  • override的作用便在于让编译器替我们检查错误,并且在实现一个工程时,往往需要改变某个函数来修改原有的内容业务,这时候可以借助编译器的错误提示看看修改这个函数,会对整个工程造成多大的影响,以及是否值得如此做。
  • 对于新的 引用限定词 的用法,大体上会用在判断 lvaluervalue 上,其目的是为了避免某些不必要的开销,而不是依赖 编译器优化 这种虚无飘渺的说法。

    class Widget {
    public:
      using DataType = std::vector<double>;
      ...
    DataType& data() & { return values; }
    DataType  data() && { return std::move(values); }
    ...
    
    private:
      DataType values;
    };
    

    可以很明显的看出,两个版本的data函数,是为了适应不同情况下对于 values的使用。

Item 13 - Prefer const_iterators to iterators

  • 很简短,尽量去使用 const_iterator,原因就是安全。
  • C++11 提供了十分简便(还不够完善)的获取上限

    1. 容器中自带的 begin(), end()cbegin(), cend() ……
    2. 非容器自带(非成员函数) std::begin<...>(), std::end<...>()
    3. C++14 中补全了对 1 的非容器实现
  • 更加建议使用 非容器的版本。

Item 14 - Declare functions noexcept if they won’t emit exceptions

  • noexceptC++11 新的标准语法
  • noexcept 的意义就是,告诉编译器这个函数绝对不会抛出异常。
  • 让一个函数成为 noexcept 会让编译器对它做极大的优化,但是这并不是建议你尽量的使用noexcept,你必须谨慎再谨慎的使用它

noexcept 对于 移动语义swap函数delete,和 析构函数十分有意义,后两者一般情况下就是 noexcept

  • 但事实上,大部分函数是 “中立的”,也就是不清楚是否会抛出异常
    • 因为有时候写一个函数时,会调用其他函数,这个被调用的函数是否会抛出异常,往往无从得知。
  • noexcept是作为函数接口的一部分 int f(int x) noexcept;

    • C++98时代,有一种同样意义(效果不同)的写法 int f(int x) throw();
    • 但这种看似等价的写法并不能让编译器对其进行极大的优化。

      优化指的是noexcept可以让编译器不为这个函数维护一个 runtime stack,且不必确保按照创建对象相反的顺序销毁对象。

  • 首先,是对于移动语义swap函数而言,noexcept意义非凡

    • 移动语义也就是C++11新的特性,右值引用的意义所在,众所周知它可能会带来极大的性能提升,例如在拷贝一组不小的数据时。
    • 但问题就在于“拷贝”这组数据时(实际上应该说是移动它们),假如在移动的过程中突然发生异常那该如何?因为是移动,而不是拷贝,所以之前移动的那部分数据就算是丢失了!
    • 相比较而言,拷贝的做法就较为妥当,因为不管会不会在拷贝过程中发生异常,原始数据始终是不会丢失的。
    • 所以我们必须确保 移动操作noexcept,这样它才能在必要时对拷贝构造函数进行替换,以此来达到提升性能的作用。
    • 例如对一个容器(std::vector)进行push_back操作,编译器除非能确定操作对象的移动操作noexcept的,否则是不会默默(隐式) 地将拷贝换成移动
  • 其次 swap 函数也是如此

    // 只有当swap数组中的元素是noexcept,这个函数才是noexcept
    template <class T, size_t N>
    void swap(T (&a)[N]),
              T (@b)[N])noexcept(noexcept(swap(*a, *b)));
    
    //只有当对pair内部元素进行swap是noexcept时,对pair进行swap才是noexcept
    template <class T1, class T2>
    struct pair {
        ...
        void swap(pair& p)noexcept(noexcept(swap(first, p.first)) &&
                                      noexcept(swap(second, p.second)));
    };
    

    这段代码出自 标准库,这种语法的意思就是,只有当noexcept括号内的是noexcept时,才是noexcept,绕口却好理解。

    总结起来就是,对高级封装的数据结构进行操作的swap函数是否是noexcept,取决于这个高级封装的数据结构内部的成员是否对于swap操作是noexcept(并且是对于每一成员都必须成立)

  • 如果硬是把一个不确定的函数,设定为except,那是在是有些本末倒置,但对于移动语义swap函数而言,却是一个很有意义的做法。

  • 总之本节最主要的目的就是,当我们十分确定某个函数一定不会抛出异常,那就让它成为noexcept

Item 15 - Use constexpr whenever possible

  • constexpr用在一个变量名上时,它的作用效果是 const超集

    // 对于 constexpr
    int sz;
    constexpr auto arraySize1 = sz; // 编译错误,sz在编译期值未知
    std::array<int, sz> data1; //这样也错,模板常量的值同样需要在编译期知道
    
    constexor auto arraySize2 = 10; //这样可以
    std::array<int, arraySize2> data2; // 编译通过
    

    从代码中可以看出,constexpr保证在编译期(其实还有链接期)这个被修饰的变量的值必须被初始化。

    // 对于 const
    int sz;
    const auto arraySize = sz; // 这样可以
    std::array<int, arraySize> data; //编译出错,还是上面的老原因
    // 补充
    const int arraySize2 = 10;
    std::array<int, arraySize2> data2; // 这样编译就通过了
    

    也就是说,const并不能保证其被修饰的变量在编译期就一定被初始化完成。

    对于一个变量名而言,其余方面 constexprconst 的作用是一样的

    实际上, constexpr 的意义就在于尽量牺牲编译时间来换取软件的运行时间。

  • constexpr 可以作为函数接口的一部分。constexpr int f(int x);

  • 如果constexpr函数的返回值被用在了需要编译期时使用的地方,例如数组维度模板常量 等地方,那么传给其的参数也必须是在编译期就能够确定下来的,如果传给这个constexpr函数的参数并不能在编译期得到其值,那么编译就将出错。(试想声明一个数组,如果你用这个函数返回值做其维度,却发现返回值没办法在编译期计算出来,那还怎么给这个数组在栈上分配内存
  • 当然,如果是纯粹调用这个constexpr函数,就没有如此多限制,相反还具有灵活性,如果调用的参数,其中一个或多个或全部,都无法在编译期获取其值,那么这个函数就会像普通函数一样等到运行时才能得到其返回值。否则就会在编译期就计算出其返回值。
  • 但此处有一些小遗憾,因为C++11constexpr有一定的局限性,但这些局限性在C++14版本中被修缮了。

    • C++11版本的constexpr函数里只能写有一个return语句:

      constexpr int pow(int base, int exp) {
          return (exp == 0 ? 1 : base * pow(base, exp-1));
      }
      
    • C++14版本则可以自由的书写:

      constexpr int pow(int base, int exp) {
          auto result = 1;
          for(int i = 0; i < exp; ++i) 
              result *= exp;
          return result;
      }
      
  • 任何类型,无论是内建(built-in),还是用户自己定义的类型,都可以被constexpr修饰,这就带来了一个好处,那就是如果构造函数被声明为constexpr,那么一旦其所传入的构造参数是可以在编译期内获得值的,那么,这次构造函数所创建对象将被保存在只读区域

    • 这种途径用的越多,你的程序也就可能跑得越快(当然更费编译时间)
    • 但是C++11constexpr在此处又体现出一些瑕疵,那就是它所修饰的类成员函数,是const的,也就是说无法通过这个函数修改其内的值,换句话说就是没办法将setter类的成员函数声明为constexpr;

      class Point{
      public:
          constexpr void setX(double newX) {
              x = newX; //C++14 可行
          }
      ...
      };
      constexpr Point reflection(const Point & p) {
          Point result;
          result.setX(-p.xValue());
          result.setY(-p.yValue());
          return result;
      }
      // 使用
      constexpr Point p1(9.4, 27.7);
      constexpr auto reflected = reflection(p1); // 这个对象也是被创建在只读区。
      

Item 16 - Make const member functions thread safe

  • 不太清楚这一小节的用意,只是粗略介绍一下如何实现线程安全的其中两种方法,而且是说让const成员函数在多线程环境下安全
  • 方法1:

    • 使用std::mutex进行真个代码块的阻塞保护。

      class Polynomial {
      public:
          using RootsType = std::vector<double>;
          RootsType roots() const {
              std::lock_guard<std::mutex> g(m); //也可以直接使用 m.lock(),而不用这个 lock_guard
              if(!rootsAreValid) {
                  ...
                  rootsAreValid = true;
              }
              return rootVals;
          }
      private:
          mutable std::mutex m;
          mutable bool rootsAreValid{ false };
          mutable RootsType rootVals{};
      };
      

      这样就实现了线程安全,其中mutable关键字是为了在 const成员函数中也能修改成员变量的。

      lock_guard 则是实现了自动加锁,解锁的功能。

  • 方法2:

    • 对于轻量级的场景,使用std::atomic来达到原子操作的目的,以此实现一定程度上的线程安全

      class Point {
      public:
          double distanceFromOrigin() const noexcept 
          {
              ++callCount;
              return std::sqrt((x * x) + (y * y));
          }
      private:
          mutable std::atomic<unsigned> callCount{ 0 };
          double x, y;
      };
      

      callCount 在此处实现了成了 具有原子操作的对象。且原子操作比锁操作所耗费的资源更少。

  • 综上。

Item 17 - Understand special member function generation

  • 在一个类里面,有一种特殊的函数,是由编译器自动生成的(视情况而定)
    • C++98时代,它们分别是,默认构造函数拷贝构造函数拷贝赋值构造函数析构函数
    • C++11时代,除了上述四种以外还多了两种移动操作移动构造函数移动赋值构造函数

以下情况,均是在未有显式声明定义的前提,要是显式定义了,那么本节的大部分内容是无意义的,关键就在于是否由编译器来生成这些函数

  • C++98时,有提到过一个 Rule of Three 原则,那就是 : 如果你需要声明定义了 拷贝构造函数,拷贝赋值构造函数,析构函数中的任意一个,那么你就应该把他们全部都声明定义出来
  • C++11的时代,这些依然适用,但是有区别的是,它们对于移动操作的影响。

    • 对于移动操作而言,只要声明其中任意一个都会阻止另一个的自动生成。
    • 而且,只要声明拷贝构造函数,拷贝赋值构造函数,析构函数中的任意一个,都会阻止移动操作的相关函数的生成。
    • 其他三个函数则不会,即使显式声明了 拷贝构造函数,也不会影响编译器默认生成拷贝赋值构造函数
  • 那我们需要编译器为我们自动生成移动操作该怎么办呢?满足三个条件:

    1. 没有拷贝操作函数在类中声明
    2. 没有移动操作函数在类中声明
    3. 没有析构函数在类中声明
  • 停下来想一想,是否太过,在C++98时代,这种特殊函数的生成规则尚且比较单一,能用Rule of Three来概述,但是C++11时代,由于引入了移动操作,这个规则变得复杂起来(远不止上面说的哪些情况)。

有没有更简洁的方法?

  • 当然有,这就是C++11中引入 = default 的原因。
  • 使用它我们就可以屏蔽一切所谓的规则,这个新语法的意义就是我想使用这个函数的编译器生成版本

    class Widget {
    public:
        ...
        ~Widget();
        Widget(const Widget &) = default;
        Widget & operator=(const Widget &) = default;
    };
    

    对于这个类而言,即使不显式写上 拷贝操作的两个函数,在我们用这个类的对象进行操作时,使用了拷贝操作,编译器也会自动为我们生成。

    但是如果我们并不懂这些规则,或者说我们不想花费时间去记,那就每次都将想要的写上并在其后跟上 = default

  • 书中提到了另一个完整的例子

    class Base {
    public:
        virtual ~Base() = default; //对于析构函数,如果作为基类最好为virtual
        Base(Base &&) = default;
        Base& operator=(Base &&) = default;     // 移动操作
    
        Base(const Base &) = default;
        Base& operator=(const Base &) = default; // 拷贝操作
    };
    
  • 最后,如果这些特殊函数在类中被写成了模板的形式,那并不会影响编译器自动生成的规则,即当他们不存在一样。

    class Widget {
    ...
        template <typename T>
        Widget(const T& rhs);
    
        template <typename T>
        Widget& operator=(const T& rhs);
    };
    

Chapter 4 : Smart Pointer

Use std::unique_ptr for exclusive-ownership resource management

  • 内存泄漏一直是C/C++ 程序员的痛处,自从 C++11的智能指针被引入,问题得到了挺大的改善。
  • unique_ptr 用来表示某个资源被这个智能指针唯一的占有,不允许对其进行拷贝(当然你不能用裸指针刻意指向它,那就没意义了),只能被 移动操作
  • 本节用一个继承类及工厂模式来讲解

    ckass Investment {...}
    class Stock : public Investment { ... }
    class Bond : public Investment { ... }
    class RealEstate : public Investment { ... }
    

    对于这种模型下的工厂模式,我们可以

    template <typenam... Ts>
    std::unique_ptr<Investment>
    makeInvestment(Ts&&... params);
    

转载注明: http://www.wushxin.top/2016/02/22/Effective-Modern-Cpp-%E6%A6%82%E8%BF%B0%EF%BC%88%E4%BA%8C%EF%BC%89.html