[Effective Modern C++(11&14)]Chapter 3: Moving to Modern C++

1. Distinguish between () and {} when creating objects

  • C++11中,初始化值的指定方式有三种:括号初始化,等号初始化和花括号初始化;其中花括号初始化是为了解决C++98的表达能力而引入的一种统一初始化思想的实例。
    • 等号初始化和花括号初始化可以用于非静态成员变量的初始化
      class Widget {
        ...
        private:
           int x {0}; // ok
           int y = 0; // ok
           int z(0); // error
      };
    • 括号初始化和花括号初始化可以用于不可拷贝对象的初始化
      std::atomic<int> ai1 {0}; // ok
      std::atomic<int> ai2 (0); //ok
      std::atomic<int> ai3 = 0; // error
    • 花括号初始化会禁止窄化转型,而等号初始化和括号初始化会自动窄化转型
      double x, y, z;
      ...
      int sum1 {x+y+z}; // error
      int sum2 (x+y+z); // ok
      int sum3 = x+y+z; // ok
    • 调用对象的无参构造函数时,使用括号初始化会被编译器错误识别为声明了一个函数,而花括号初始化则能正确匹配到无参构造函数的调用
      Widget w1(); // error
      Widget w2{}; // ok 
    • 花括号初始化与std::initializer_lists和构造函数重载解析的同时出现时容易造成错误调用
      • 在调用构造函数的时候,只要不涉及到std::initializer_list参数,括号和花括号初始化有相同的含义
        class Widget {
            public:
                Widget(int i, bool b);
                Widget(int i, double d);
                ...
        };
        
        Widget w1(10, true); // calling 1
        Widget w2{10, true}; // calling 2
        Widget w3(10, 5.0);  // calling 1
        Widget w4{10, 5.0};  // calling 2
      • 如果涉及到std::initializer_list参数,在使用花括号初始化时,编译器会强烈地偏向于调用使用std::initializer_list参数的重载构造函数
        class Widget {
            public:
                Widget(int i, bool b);
                Widget(int i, double d);
                Widget(std::initializer_list<long double> il);
                ...
        };
        
        Widget w1(10, true); // calling 1
        Widget w2{10, true}; // calling 3, 10 and true convert to long double
        Widget w3(10, 5.0);  // calling 1
        Widget w4{10, 5.0};  // calling 3 , 10 and 5.0 convert to long double
        • 甚至本来应该调用拷贝构造函数或者移动构造函数,也会被std::initializer_list构造函数给劫持
          Widget w5(w4); // copy construction 
          Widget w6{w4}; // std::initializer_list construction
          Widget w7(std::move(w4)); // move construction
          Widget w8{std::move(w4)}; // std::initializer_list construction
        • 编译器非常偏向选择std::initializer_list构造函数,以至于即便最匹配的std::initializer_list构造函数不能被调用,编译器也会优先选择它
          class Widget {
             public:
                 Widget(int i, bool b);
                 Widget(int i, double d);
                 Widget(std::initializer_list<bool> il);
                 ...
          };
          
          Widget w{10, 5.0}; // error, requires narrowing conversions
        • 只有当没有办法在花括号初始化的参数类型和std::initializer_list的参数类型之间进行转换时,编译器才会重新选择正常的构造函数
          class Widget {
             public:
                 Widget(int i, bool b);
                 Widget(int i, double d);
                 Widget(std::initializer_list<std::string> il);
                 ...
          };
          
          Widget w1(10, true); // calling 1
          Widget w2{10, true}; // calling 1
          Widget w3(10, 5.0);  // calling 2
          Widget w4{10, 5.0};  // calling 2
        • 当类同时支持默认构造函数和std::initializer_list构造函数时,此时调用空的花括号初始化,编译器会解析为调用默认构造函数,而要解析成std::initializer_list构造函数,需要在花括号中嵌套一个空的花括号进行初始化
          class Widget {
              public:
                  Widget();
                  Widget(std::initializer_list<int> il);
                  ...
          };
          
          Widget w1; // calling 1
          Widget w2{}; // calling 1
          Widget w3{{}}; // calling 2
          Widget w4({}); // calling 2

2. Prefer nullptr to 0 and NULL

  • C++会在需要指针的地方把0解释成指针,但是需要策略还是把0解释成int
  • C++98中上面这种做法会使得在指针和int型重载共存时产生意外匹配调用
    void f(int);
    void f(bool);
    void f(void*);
    f(0);  // calls f(int)
    f(NULL); // might not compile, but typically calls f(int)
  • nullptr的优点在于它没有一个整型类型,也没有一个指针类型,但是可以代表所有类型的指针,nullptr的实际类型是nullptr_t,可以被隐式地转换成所有原始指针类型

    f(nullptr); // calls f(void*)
  • 当在使用模板时,nullptr的优势就发挥出来了,可以转换成任意指针类型

    int f1(std::shared_ptr<Widget> spw);
    int f2(std::unique_ptr<Widget> upw);
    bool f3(Widget* pw);
    std::mutex f1m, f2m, f3m;
    
    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 result1 = lockAndCall(f1, f1m, 0); // error, PtrType is int
    auto result2 = lockAndCall(f2, f2m, NULL); // error, PtrType is int / long
    auto result3 = lockAndCall(f3, f3m, nullptr); // ok 

3. Prefer alias declarations to typedefs

  •  aliastypedef更容易理解
    typedef void (*FP)(int, const std::string&);
    using FP = void(*)(int, const std::string&);
  • alias可以模板化,而typedef不能直接模板化,需要借助结构体来实现
    • 如果要定义一个使用自定义分配器的链表
      template<typename T>
      using MyAllocList = std::list<T, MyAlloc<T>>;
      MyAllocList<Widget> lw;
      
      template<typename T>
      struct MyAllocList {
         typedef std::list<T, MyAlloc<T>> type;
      };
      MyAllocList<Widget>::type lw;
    • 如果要在模板内部创建一个持有模板参数类型的链表,必须在typedef名字前面加上typename

      template<typename T>
      class Widget {
          private:
              typename MyAllocList<T>::type list;
              ...
      };
      • MyAllocList<T>::type指的是一个取决于模板类型参数T的类型,因此就是一个依赖类型,C++规定依赖类型前面必须加上typename

      • 如果使用alias定义模板,就不需要typename

        template<typename T>
        using MyAllocList = std::list<T, MyAlloc<T>>; 
        
        template<typename T>
        class Widget {
           private:
               MyAllocList<T> list;
               ...
        };
        • 此处看起来MyAllocList<T>是一个与模板参数T存在依赖关系的对象,但是当编译器处理Widget模板时,它知道MyAllocList<T>是一个类型的名字,因为MyAllocList是一个别名模板:它必须命名一个类型,因此MyAllocList<T>是一个无依赖类型,也就不需要typename

        • typedef中,当编译器在Widget模板中看到MyAllocList<T>::type时,它们不能确定这是否是一个类型,因为有可能是MyAllocList<T>的一个特例而它们没看到,例如:

          class Wine{...};
          
          template<>
          class MyAllocList<Wine> {
              private:
                  enum class WineType {White, Red, Rose};
                  WineType type;  //!!!!!!!!!!!!!!!
                  ...
          };
  • C++11以类型萃取的形式提供了许多形式转换工具,模板都在<type_traits>头文件中,例如

    std::remove_const<T>::type
    std::remove_reference<T>::type
    std::add_lvalue_reference<T>::type
    • 但是要在模板内部使用它们时,仍然要在前面加上typename,因为它们实际上还是用嵌套typedef实现的

    • 但在C++14中,它们有了替代的方案

      std::remove_const_t<T>
      std::remove_reference_t<T>
      std::add_lvalue_reference_t<T>
      • 原理显而易见

        template<class T>
        using remove_const_t = typename remove_const<T>::type;
        
        template<class T>
        using remove_reference_t = typename remove_reference<T>::type;
        
        template<class T>
        using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;

4. Prefer scoped enums to unscoped enums

  • 通常情况下,在花括号内声明一个名字可以限制名字对外的可见性,但是对于C++98enums中的enumerators并非如此,其对外也是可见的
    enum Color {black, white, red};
    auto while = false; // error, while already declared in this scope
  • C++11的新标准,有范围限制的enums,并不会对命名空间造成污染
    enum class Color {black, white, red};
    auto white = false; // fine
    Color c = white; // error, no enumerator named “white" is in this scope
    Color c = Color::white; // fine
    auto c = Color::white; // fine
  • 有范围限制enums中的枚举常量有更强的类型,而对于无范围限制的enums中枚举常量会被隐式转换成整型类型
    enum Color {black, white, red};
    std::vector<std::size_t> primeFactors(std::size_t x);
    
    Color c = red;
    ...
    if( c < 14.5){ // compare Color to double!!
       auto factors = primeFactors(c); // compute prime factors of a Color!!
       ...
    }
    
    enum class Color {black, white, red};
    Color c = Color::red;
    ...
    if( c < 14.5){ // error, can't compare Color and double!!!
        auto factors = primeFactors(c); // error, can't pass Color to function expecting std::size_t
        ...
    }
  • 如果要把C++11中的enums变量转换成其他类型,需要使用static_cast<>()
    if( static_cast<double>(c) < 14.5 ){ // valid
        auto factors = primeFactors(static_cast<std::size_t>(c)); // valid
        ...
    }
  • C++中每个enum都有一个由编译器决定的整型底层类型,为了有效利用内存,编译器通常会选择足够代表枚举量范围的最小的底层类型,为此,C++98只支持enum定义(列出所有的枚举值),而不支持声明,这使得在使用enum前,编译器能选择一个底层类型。
  • 无法对enum前置声明有许多缺点,最显著的就是增加编译的依赖性,如果一个enum被系统中每个组件都有可能用到,那么都得包含这个enum所在的头文件,如果需要新加入一个枚举值,整个系统就有可能重新编译,即便只有一个函数使用这个新的值
  • C++11中的enum类可以消除这个编译需求,例如
    #file 1
    enum Status {
       good = 0,
       failed = 1,
       incomplete = 100,
       corrupt = 200,
       audited = 500,
       indeterminate = 0xFFFFFFFF
    };
    
    #file 2
    enum class Status;
    void continueProcessing(Status s);
    • 如果修改了Status,而且continueProcessing没有使用到新的值,那么file2就不需要重新编译
    • 但是如果编译器在使用一个enum之前,需要知道它的大小该怎么办?
      • 对于一个有范围限制的enum,它的底层类型是已知的(默认是int,可以手动覆盖),而对于没有范围限制的enum,底层类型可以指定
        enum class Status; //int, declaration 
        enum class Status: std::uint32_t; //uin32_t, declaration
        enum Color: std::uint8_t;// uint8_t, declaration
        
        enum class Status: std::uint32_t {
            good = 0,
            failed = 1,
            incomplete = 100,
            corrupt = 200,
            audited = 500,
            indeterminate = 0xFFFFFFFF
        };
  • 无范围限制的enumC++11std::tuples中的用途

    using UserInfo = std::tuple<std::string, std::string, std::size_t>; // name, email, reputation
    UserInfo uInfo;
    ...
    auto val = std::get<1>(uInfo); // get value of field 1, but can you always remember what the hell 1 represents?
    
    //Improve
    enum UserInfoFields {uiName, uiEmail, uiReputation};
    UserInfo uInfo;
    ...
    auto val = std::get<uiEmail>(uInfo); // implicit conversion from UserInfoFields to std::size_t, which is the type that std::get requires
    • 如果要改写成有范围限制的enum,略显拖沓

      enum class UserInfoFields {uiName, uiEmail, uiReputation};
      UserInfo uInfo;
      ...
      auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

5. Prefer deleted functions to private undefined ones

  •  删除的函数和声明为private的函数之间的区别
    • 删除的函数在任何地方都不能使用,所以成员函数和友元函数都不能使用已经删除的函数,否则会编译失败,这在C++98中会推迟到链接阶段才会报错
    • 删除的函数是pulic而不是private,因为当客户端代码试图使用这个删除的成员函数时,C++会首先检查访问权限,后检查删除状态,如果设为private,编译器给出的是权限不足警告而不是函数不可用警告
    • 任何函数都可以是deleted状态,而只有成员函数可以是private,例如删除某些过时的重载函数
      bool isLucky(int number);
      bool isLucky(char) = delete;
      bool isLucky(bool) = delete;
      bool isLucky(double) = delete;
    • 虽然删除的函数不能使用,但仍然是程序的一部分,因此,在重载解析过程中也会被纳入考虑中

    • 模板函数可以通过删除来阻止部分实例化函数,而允许其他实例化存在
      template<typename T>
      void processPointer(T* ptr);
      
      template<>
      void processPointer<void>(void*) = delete;
      
      template<>
      void processPointer<char>(char*) = delete;
    • 有意思的是,如果在类里面有一个模板函数,则不能通过设置private来禁用一些实例化,因为不能给一个成员函数的模板特化一个不同于主模板的访问权限,例如

      class Widget {
          public:
              ...
              template<typename T>
              void processPointer(T* ptr) {...}
          private:
              template<>
              void processPointer<void>(void*); // error
      };
      • 问题在于模板特化必须被卸载命名空间范围内,而不是在类范围内,因此可以使用delete来实现

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

6. Declare overriding functions override

  • 覆盖产生的必要条件
    • 基类函数必须是virtual
    • 基类和派生类的函数名必须一致
    • 基类和派生类函数的参数类型必须一致
    • 基类和派生类函数的const属性必须一致
    • 基类和派生类函数的返回类型以及异常说明必须兼容
    • 函数的引用修饰必须一致(C++11)
      • 限制成员函数的使用只能是左值或者右值(*this)
        class Widget {
           public:
           ...
           void doWork() &; // only when *this is an lvalue
           void doWork() &&; // only when *this is an rvalue
        };
        
        ...
        Widget makeWidget();
        Widget w;
        ...
        w.doWork();
        makeWidget().doWork();
  • 显式地对成员函数声明override能使得编译器检查是否正确覆盖,而不是在没有正确覆盖时隐式地转换成了重载或者其他合法函数,而使得调用时发生意外调用,例如

    class Base{
       public:
           virtual void mf1() const;
           virtual void mf2(int x);
           virtual void mf3() &;
           void mf4() const;
    };
    
    class Derived: public Base {
       public:
           virtual void mf1();  // not const 
           virtual void mf2(unsigned int x);  // not int
           virtual void mf3() &&; // not &
           void mf4() const; // not virtual in base
    };
    • 虽然上面的函数都没有发生覆盖,但是有些编译器认为都是合法的,而不会给出警告,正确的做法是

      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;
      };
      • 此时,编译器能检查出所有的错误覆盖

7. Prefer const_iterators to iterators

8. Declare functions noexcept if they won't emit exceptions

9. Use constexpr whenever possible

  • 对于constexpr对象,它们具有const属性,并且它们的值在编译的时候确定(从技术角度讲,是在转换期间确定,转换期包括编译和链接),它们的值也许会被放在只读内存区中,它们的值也能被用在整型常量表达式中,例如数组长度,整型模板参数,枚举值,对齐指示符等等
  • constexpr函数使用constexpr对象时,它们会产生编译期常量,如果constexpr函数使用了运行时的值,它们就会产生运行时的值,但是如果constexpr函数使用的所有参数都是运行时的值,那么就会报错
  • C++11中,constexpr函数只能包含不超过一条return语句的执行语句,但是可以使用条件运算符和递归来实现多重运算。
  • C++14中,constexpr函数的语句数量没有限制,但是函数必须接收和返回字面值类型,也就是指可以在编译期间确定值的类型。
  • 字面值类型包括除了void修饰的类型和带有constexpr修饰的用户自定义类型(因为构造函数和其他成员函数也可能是constexpr)
    class Point {
       public:
           constexpr Point(double xVal = 0, double yVal = 0) noexcept: x(xVal), y(yVal) {}
           constexpr double xValue() const noexcept { return x;}
           constexpr double yValue() const noexcept { return y;}
           void setX(double newX) noexcept { x = newX;}
           void setY(double newY) noexcept { y = newY;}
       private:
           double x, y;
    };
    constexpr Point p1(9.4, 2.7);
    constexpr Point p2(28.8, 5.3);
    
    constexpr Point midpoint(const Point& p1, const Point& p2) noexcept
    {
         return { (p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2 };
    }
    
    constexpr auto mid = midpoint(p1, p2);
  • C++11中,setXsetY不能被声明为constexpr,因为不能在const成员函数中修改成员变量,而且返回值为void,并不是字面值常量,但是C++14中是允许的

10. Make const member functions thread safe

11. Understand special member function generation

  • 特殊成员函数是C++会自动生成的函数,C++98中有四个这样的函数:默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符;C++11中多了两个:移动构造函数和移动赋值运算符
  • 两个拷贝操作是无关的,声明一个不会阻止编译器产生另一个
  • 两个移动操作是相关的,声明一个会阻止编译器自动产生另一个
  • 显式声明一个拷贝操作后,移动操作就不会被自动生成,反之依然,理由是:比如声明了拷贝运算,就说明移动操作不适合用于此类
  • 三条规则:如果声明了拷贝构造,拷贝赋值或者析构函数中任何一个,都应该将三个一起声明,因为这三个函数是相互关联的
  • 三条规则暗示了析构函数的出现使得简单的memberwise拷贝不适合类的拷贝操作,也就是说如果声明了析构函数,那么就不应该自动生成拷贝操作相关的函数,因为可能会存在不一致的资源管理行为。同样的,也不应该自动生成移动操作相关的函数。所以,只有当类满足下面三个条件时,移动操作才会自动生成:
    • 没有声明拷贝操作
    • 没有声明移动操作
    • 没有声明析构函数
  • 假如编译器生成的函数行为正确,那么我们只需要在函数名后面加上default就可以了,然编译器接管一切具体事务。  

猜你喜欢

转载自www.cnblogs.com/burningTheStar/p/8975712.html