07:在创建对象时注意区分(){}

  • 初始化方式
    1
    2
    3
    4
    
    int x1(1); 
    int x2 = 2;  
    int x3{3}; // 统一初始化(列表初始化)
    int x4 = {4}; // 和第三种方式相同
    
  • ()=初始化的限制
    • ()不能用于non-static成员的初始化
    • 不能拷贝的对象不能使用()初始化
  • {}初始化的优点
    • 禁止基本类型之间的隐式窄化类型转换:比如不能使用double初始化int型变量
    • 避免了C++复杂的语法分析:C++’s most vexing parse
      1
      2
      3
      
      Widget w1(10); // 传入一个实参,构造出一个对象
      Widget w2(); // 本来想调用无形参的构造函数构造一个对象,但是实际上声明了一个函数
      Widget w3{}; // 调用无形参的构造函数,构造出一个对象
      
  • {}的缺陷
    • auto类型推导中使用{}进行初始化,则auto被推断为initializer_list<T>
    • 会优先使用形参为initializer_list<T>的构造函数,即使其他的构造函数更加匹配
      • 只有当{}中参数无法转换为initializer_list中类型时,编译器才匹配普通函数
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      class Widget{
      public:
          Widget() {cout<<"0"<<endl;}
          Widget(int i, int d) {cout<<"1"<<endl;}
          Widget(int i, bool d) {cout<<"2"<<endl;}
          Widget(initializer_list<int> il) {cout<<"2"<<endl;}
          Widget(const Widget& w) {cout<<"copy ctor"<<endl;}
          Widget(Widget&& w) {cout<<"move copy ctor"<<endl;}
          operator int() const { cout<<"convert to int"<<endl; return 1; }
      };
      Widget w1{1, true}; // 调用Widget(initializer_list<int> il),即使Widget(int i, bool d)更加匹配
      Widget w2{1, 1.0}; // 编译报错,本来调用Widget(initializer_list<int> il),但是使用{}初始化禁止窄化类型转换(存在从double到int的转换)
      Widget w3{w1}; // 调用Widget(initializer_list<int> il)(中间先将w1转为int),即使Widget(const Widget& w)更加匹配(如果w1无法转换为int,则调用该构造函数)
      Widget w4{std::move(w1)}; // 调用Widget(initializer_list<int> il),即使Widget(Widget&& w)更加匹配
      
      // 特殊情况:
      Widget w4{}; // 调用Widget(),而非调用Widget(initializer_list<int> il)
      Widget w5{{}}; // 调用Widget(initializer_list<int> il),而非调用Widget()
      Widget w6({}); // 调用Widget(initializer_list<int> il),而非调用Widget()
      
  • 使用模板创建对象时,仔细考虑使用()还是{}进行初始化
    • 标准库函数std::make_uniquestd::make_shared也面临着这个问题,它们的解决方案是在内部使用小括号,并将这个决定写进文档中,作为其接口的组成部分。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    template <typename T, typename... Ts>
    void f(Ts&&... params){ // 使用可变参数模板
        T localVector1(std::forward<Ts>(params)...);
        T localVector2{std::forward<Ts>(params)...};
    }
    
    f<vector<int>>(3,4); // 推断出T=vector<int>, Ts=int
    // localVector1: 4,4,4
    // localVector2: 3,4
    
  • 参考

08:优先选用nullptr,而非0或NULL

  • 字面量0是一个int,NULL的实现为0L,可以转换为int,bool,void*
  • nullptr可以理解为任意类型的空指针
    • 使得重载函数的调用明确
    • 提高代码的清晰度
    • 使用类型推导时,nullptr可以隐式转换为任意类型指针
  • 参考

09:优先选用别名声明,而非typedef

  • using别名的优点:
    • 清晰,比typedef更容易理解
    • 可以直接对模板起别名
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      template <typename T>
      using MyAllocList = std::list<T, MyAlloc<T>>;
      
      // 如果非要使用typedef,需要包装一层
      template <typename T>
      struct MyAllocList{
          typedef std::list<T, MyAlloc<T>> type;
      };
      
      template <typename T>
      class Widget{
          MyAllocList<T> list1; // list1=std::list<T, MyAlloc<T>>,此时MyAllocList一定是一个别名
          typename MyAllocList<T>::type list2; // list2=MyAllocList<T>中的std::list<T, MyAlloc<T>>
          // 需要使用typename显式说明MyAllocList<T>::type是一个类型,而非数据成员
      }
      
  • 应用:标准库的<type_traits>中提供了一整套用于类型转换的类模板
    • 虽然C++11中仍然是使用typedef实现的,但是C++14中是使用using声明实现的
    1
    2
    
    std::remove_const<T>::type // C++11中, 是一个内部包裹typedef的类模板,将T中的const属性移除
    std::remove_const_t<T>     // C++14中, 是一个类模板中typedef别名的别名,将T中的const属性移除
    
  • 参考

10:优先选用限定作用域的枚举类型,而非不限作用域的枚举类型

  • 无作用域限制的枚举(unscoped enums,C++98)
    • 有时使用可能简便一点
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      using UserInfo = std::tuple<std::string, std::string, std::size_t> //name, email,age
      enum UserInfoFields {uiName, uiEmail, uiAge};
      
      UserInfo uInfo;
      auto email = std::get<1>(uInfo); // 位置1为email
      auto email = std::get<uiEmail>(uInfo); // 发生隐式类型转换
      auto email = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo); // 冗余
      
      // C++14下的辅助类:既想使用有作用限制的枚举,又不想过于啰嗦
      template <typename E> 
      constexpr auto toUType(E enumerator) noexcept {
          return static_cast<std::underlying_type_t<E>>(enumerator);
      }
      auto email = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
      
  • 有作用域限制的枚举(scoped enums,C++11)
    • 减少名称污染
      1
      2
      3
      4
      5
      
      enum unscopedColor{black, white}; 
      auto black = false; // 无作用域限制的枚举,因此枚举类型暴露在{}之外
      
      enum class scopedColor {red, blue};
      auto red = false; // 有作用域限制的枚举,枚举类型限制在{}之内,因此减少名称污染
      
    • 有强类型
      1
      2
      3
      4
      5
      
      enum unscopedColor{black, white}; 
      double d1 = black; // 无作用域限制的枚举,可以发生隐式类型转换
      
      enum class scopedColor {red, blue};
      double d2 =  static_cast<double>(scopedColor::red); // 有作用域限制的枚举,不会发生隐式类型转换,类型转换需要显式说明
      
    • 可以前向声明:只有在指定底层类型后,才能进行前向声明
      1
      2
      
      enum unscopedColor: std::uint8_t; // 没有提供默认底层类型
      enum class; //默认底层类型为int
      
  • 参考

11:优先选用删除函数,而非private未定义函数

  • 背景:编译期会自动生成某些函数,但是有时不需要这些函数;
  • C++98的做法:声明为private的,且只声明不定义(effective C++中item6)
    • 在private中声明但是不定义,使之在链接阶段因为没有定义而报错
    • 在基类中声明为private的,会因为无法拷贝控制派生类中的基类部分,将报错从链接期提前到编译期
  • C++11的做法:在声明中标记为=delete
    • 将删除的函数声明为public的,原因是编译器先检查访问权限,再检查delete状态。如果将删除的函数声明为private的,调用删除的函数时,可能报错原因提示是private的;但是更期望的更明确的含义是这些函数是删除的
    • =delete可以在任意函数中进行标记,不仅仅局限于成员函数
  • 应用:
    • 比如可以阻止某些形参的隐式类型转换
      1
      2
      
      void func(int a);
      void func(double) =delete; // 因此禁止double和float两种参数的调用(C++总是倾向于将 float 转换为 double)
      
    • 阻止某些模板类型的实例化
      1
      2
      3
      4
      5
      6
      7
      
      template <typename T> void func(T* ptr);
      template <> void func<void>(void* ptr) =delete; 
      
      struct Widget{
          template<typename T> void g(T* ptr);
      };
      template<> void Widget::g<void>(void* ptr) = delete; // 成员模板函数在类外阻止某些类型的实例化
      
  • 参考

12:给意在改写的函数添加override声明

  • 重写override需要满足的条件
    • 基类的重写函数必须是虚函数
    • 基类和派生类的重写函数
      • 函数名(析构函数除外)、形参类型、函数常量性完全相同
      • 函数引用限定符完全相同(C++11,函数引用限定符:该成员函数可以被左值对象还是右值对象调用)
      • 返回值类型、异常规格说明兼容
  • 将重写的函数标记为override,如果不满足重写条件则报错
  • 参考

13:优先选用const_iterator,而非iterator

  • C++98在容器的成员函数中对const_iterator支持有限
  • C++11在容器的成员函数中支持const_iterator,但是只提供了非成员的begin和end
    1
    2
    3
    4
    
    template <typename Container> // C++11实现cbegin的方法
    auto cbegin(const Container& container) -> decltype(std::begin(container)){ // auto=const Container::iterator&
        return std::begin(container);
    }
    
  • C++14提供了非成员的cbegin和cend
  • 尽量使用非成员的cbegin和cend,因为某些数据结构(比如数组)没有成员函数cbegin和cend,非成员的cbegin和cend更加通用
1
2
3
4
5
6
7
template<typename C, typename V>  
void findAndInsert(C& container, const V& targetVal, const V& insertVal) {  
    using std::cbegin;  
    using std::cend;  
    auto it = std::find(cbegin(container), cend(container), targetVal);  
    container.insert(it, insertVal);  
}

14:只要函数不会抛出异常,就为其加上noexcept声明

  • noexcept 是函数接口的一部分,并且调用者可能会依赖这个接口。
  • 相较于 non-noexcept 函数,noexcept 函数有被更好优化的机会。
  • noexcept 对于 move 操作、swap、内存释放函数和析构函数是非常有价值的。
  • 大部分函数是异常中立的而不是 noexcept。
  • 背景:
    • C++98中异常规范的局限性:接口的实现一旦被修改,其异常规范可能也变化
    • 因此C++11只需要指明接口是否可能抛出异常
  • 优点:一个noexcept函数有更多编译优化的机会
    • 不需要保持运行栈为解开的状态
    • 不需要保证对象以构造顺序的逆序完成析构
  • 应用:如果知道一个函数不会抛出异常,一定要加上noexcept
    • noexcept属性对于移动操作、swap、内存释放函数和析构函数最有价值。C++11 STL 中的大部分函数遵循 “能移动则移动,必须复制才复制” 策略
    • 默认noexcept函数:C++11内存释放函数和所有的析构函数都默认隐式地具备noexcept属性
      • 析构函数未隐式地具备noexcept属性的唯一情况,就是所有类中有数据成员(包含递归的成员)的类型显式地将其析构函数声明为noexcept(false)
      • 如果标准库使用了某个对象,其析构函数抛出了异常,则该行为是未定义的。
  • 条件noexcept:一个函数是否为noexcept,取决于noexcept中的表达式是否为noexcept
    • 只有被调用的低层次的函数是noexcept,高层次的调用方才是noexcept
    1
    2
    3
    4
    
    template <typename T1, typename T2>
    struct myPair{
        void swap(myPair& p) noexcept( noexcept(swap(first, p.first)) && noexcept(swap(second, p.second)) );
    }
    
  • 异常中立函数:本身不抛出异常,但是调用的函数可能抛出异常,因此不适合标记为noexcept
    • 但是允许noexcept函数中调用没有noexcept保证的函数
  • 通常只为宽松规约提供noexcept声明
    • 宽松规约(wide contract,宽接口):不带前提条件,被调用时不需要关注程序的状态,传入的参数方面没有限制,宽接口的函数永远不会出现未定义的行为
    • 狭隘规约(narrow contract,窄接口):带前提条件,如果违反前提条件,则结果是未定义的
      • 调用者来保证调用时满足前提条件
      • 如果调用时违反前提条件,则抛出异常;如果定义为noexcept的,违反前提条件结果是未定义的;相较而言,找出抛出异常的原因相对简单一些
  • 参考

15:只要有可能使用constexpr,就使用它

  • constexpr对象:具备const属性,并且在编译期(和链接期)可以确定其值
    • const对象不能保证在编译期确定其值
  • constexpr函数
    • 含义:
      • 如果所有传入 constexpr 函数的参数都能在编译时知道,则结果将在编译时计算出来。
      • 如果传入 constexpr 函数的参数有任何一个不能在编译期知道,则结果在运行时计算出来
    • 使用
      • C++11中,constexpr函数有且只能有一条return语句;C++14无此限制
      • constexpr 函数被限制只能接受和返回 literal 类型(字面量,非指针和引用,自定义类型也可能是字面量类型的)
      • C++11中,如果成员函数修改了操作的对象,或者成员函数的返回值是void的,则该成员函数无法成为constexpr的;C++14无此限制
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      class Point{
          public:
              constexpr Point(double xVal=0, double yVal=0) noexcept: x(xVal), y(yVal) {}
              constexpr double getX() const noexcept {return x;}
              constexpr double getY() const noexcept {return y;}
              constexpr void setX(double newX) noexcept { x = newX;} // C++14中,移除了两条限制,因此可以设置为constexpr的
              constexpr void setY(double newY) noexcept { y = newY;}
          private:
              double x, y;
      };
      constexprt Point p1(1.0, 2.0);
      
  • 参考

16:保证const成员函数的线程安全性

  • const成员的好处:不会修改成员变量,而且可以区分重载(const对象和非const对象调用)
  • 保证const成员函数的线程安全性
    • 使用std::mutex,进入临界区锁对象获取互斥量,出临界区析构锁(释放互斥量)
    • 使用std::atomic,但是只能同步单一变量或者内存单元
    • std::mutexstd::atomic都是move-only的
  • 参考

17:理解特殊成员函数的生成机制

  • 特殊成员函数(special member function):
    • 一般是public、inline和novirtual的
      • 例外:如果基类中的析构函数是virtual的,派生类中的析构函数也是virtual的
    • 拷贝构造和拷贝赋值是两个独立的操作
    • 移动构造和移动赋值不是独立的操作,如果声明了其中一个,编译器会阻止生成另外一个
    • 如果显式申明一个拷贝操作,则两个移动操作不会自动生成
  • 三法则(The Rule of Three):如果声明了{拷贝构造函数、拷贝赋值操作、析构函数}中任意一个,则应该声明所有这三个函数,因为往往意味着类要管理某些资源
    • 因此,如果只声明了一个析构函数,编译器应该不会自动生成拷贝操作
    • 但实际上编译器还是可能自动生成拷贝操作(历史遗留原因,以及C++11为了兼容历史代码)
    • 因此,只有当类中没有声明析构函数、拷贝操作、移动操作,而且需要时,编译器才会生成移动操作
  • 如果想让编译器自动生成相关函数(即使违背了这些限制),添加=default进行标记
  • C++11中对特殊成员函数的生成规则:
    • 默认构造函数:同C++98
    • 析构函数:本质同C++98,只是默认声明为noexcept
    • 拷贝构造函数:运行期行为同C++98(memberwise 拷贝构造 non-static 成员变量)
      • 如果类中声明了一个移动操作,则拷贝构造函数和拷贝赋值运算符被标记为=delete
      • 如果类中自定义拷贝赋值运算符或析构函数,可以生成拷贝构造函数,但是已经成为被废弃的方法
    • 拷贝赋值运算符:规则同拷贝构造函数
    • 移动构造函数和移动赋值运算符:仅当类中不包含用户声明的拷贝操作、移动操作和析构函数时才生成
  • 特殊情况:成员模板函数不会抑制特殊成员函数的自动生成
    1
    2
    3
    4
    5
    
    class Widget{
    public:
        template <typename T> Widget(const T &rhs); 
        template <typename T> Widget& operator=(const T& rhs); 
    }; // 编译器仍然会生成copy和move操作,即使可以实例化得到
    
  • 参考