Skip to content

Latest commit

 

History

History
423 lines (415 loc) · 37.3 KB

1.markdown

File metadata and controls

423 lines (415 loc) · 37.3 KB

1. Effective Modern C++

Deduce Types

  • Item 1: Understand template type deduction
    • 形参声明的推导结果:
      • T -> 形参是值。可以绑定到任何实参,实参的引用、const/volatile都丢弃,发生拷贝或移动。
      • T& -> 形参是cv/non-cv左引用。不能绑定到右值(修改但不move右值是没有意义的),绑定到cv左值时,cv会推导在T中。
      • T const& -> 形参是const左引用。可以绑定到任何值。
      • T&& (universal reference) -> 形参是左值/右值引用,左值引用是T中有&。
    • 数组和函数
      • 形参为传值时,实参的数组和函数自动退化(decay)成元素指针和函数指针。
      • 形参为引用时,实参的数组和函数以引用方式传递。数组引用包括每个纬度的size
    • 结论
      • 就结果来说,实参的reference-ness会被忽略
  • Item 2: Understand auto type deduction
    • auto 用于类型推导时,相当于template type deduction中的T,因此auto/auto&/auto const&/auto&&分别被推导为值/可变左引用/不可变左引用/任何引用。
      • auto独有的规则,{}(braced initializer)作为初始化表达式时,auto被推导为initializer_list。(作为对比, int a = {1}auto a = {1} 完全不同,前者定义了个int,后者定义了个initializer_list<int>)
    • auto 用作返回值和lambda中的时候,完全相当于T。(auto私有的规则无效,具体来说,{} 作为返回值和lambda形参的初始化表达式是error)
  • Item 3: Understand decltype
    • decltype 以变量名为参数的时候,得到的就是变量的声明类型
    • decltype 以左值表达式为参数的时候,推导结果一定有左引用。(比如,int x; decltype(x) y; decltype((x)) z ,y是int,z是int&)
    • decltype(auto) 使得auto应用decltype的推导规则。(作为对比:auto -> 值;auto &-> 可变左引用; auto const & -> 不可变左引用; auto && -> 任意左/右引用; decltype(auto) -> 值或任意左/右引用)
  • Item 4: Know how to view deduced type
    • 可以在edit time, compile time, runtime 来inspect一个变量/表达式的类型
      • edit time: 通过ide。不一定准确,看ide中编译器前端实现是否标准。
      • compile time: 通过编译错误。不一定准确,看编译器是否标准。
        • template <typename T> class D; D<decltype(x)>(); 编译错误中看到x的类型
      • runtime: 通过boost::typeindex::type_id_with_cv<T>().pretty_name() 来看
        • typeid(T).name() 是不行的:
          1. name()不保证可读,不同编译器下的实现不同。clang/gcc可读性较差,msvc的可读性较好。
          2. 标准规定,typeid的形参声明相当于T,所以不能反映实参的类型。

Auto

  • Item 5: Prefer auto to explicit type declartions
    • auto 的优点:
      • 对重构友好
      • 必须初始化
      • 简化声明,比如迭代器。
      • 能够声明只有编译器知道的类型,比如lambda表达式
        • 使用auto来声明lambda表达式相比用function<xxx> 的优点:
          • 避免了function<xxx> 的多态开销,比如内部的堆分配
          • 每个lambda可以有独立的类型,传值给模板函数时,可以实例化出专有的函数实现,从而允许内联
          • 可以声明泛型lambda
      • 能够声明准确的类型,避免常见的类型陷阱
        • unsigned size = vec.size(),不可移植,64bit下溢出
        • for (const pair<string, int> &p : map),其实发生了隐式类型转换pair<string const, int> -> pair<string, int>,因此p其实引用的是一个右值
    • 关于auto影响可读性的讨论
      • auto不是强制的,你可以在有必要的时候显示声明
      • auto不是c++独创的,已经在静态函数式语言和动态语言中广泛的使用
      • 可以通过ide查看实际类型
    • auto 的缺点
      • {} 被返回值/lambda以外被推导成initializer_list,这和显示声明时结果不同
      • 会将hidden proxy暴露出来,导致undefined bahvior,需要用户自己去发现hidden proxy。比如bool x = getBoolVector()[0]; 是ok的,而auto x = getBoolVector()[0] 声明了一个bool的proxy,引用了一个右值的容器,非法。
  • Item 6: Use the explicitly typed initializer idiom when auto deduceds undesired types
    • 使用static_cast将hidden proxy实例转化成被代理的类型实例
      • auto x = static_cast<bool>(getBoolVector()[0]) 强调了这个转换的必要;而如果继续使用bool x = getBoolVector()[0]的话,一旦未来被人不小心refactor到auto,就break掉了,坑。

Moving to Modern C++

  • Item 7: Distinguish between () and {} when creating objects
    • 四种初始化方法:int x(0), int x = 0, int x{0}, int x = {0}
      • 对基本类型来说,以上四种声明等价。对用户自定义类型来说,可能是构造、拷贝构造、拷贝赋值等。
    • {}使用场合很广:
      • 初始化没有构造函数的结构体。圆括号不行。
      • 初始化容器。圆括号必须vector<int> v({1, 2, 3})
      • 类成员的默认值。例如,privat: int x{0};。赋值不行。
      • 初始化non-copyable对象。如atomic<int> x{0}。赋值不行。
    • {}的特有优点
      • 不允许implicit narrowing conversion。例如 int x = 3.0是合法的,int x = {3.0}是不合法的。显然隐式窄转换是不合理的,但是为兼容c又不能去掉,{} 初始化自身fix了这个问题。
      • 避免了the most vexing parse,即把一个变量的定义,parse成函数声明
        • Widget w2() 是函数声明,Wdiget w2{}是变量定义。
    • {}的缺点
      • 一旦有initializer_list的重载,即使有类型更匹配的其他重载,也选择initializer_list。例如 Widget(int, double)Wdiget(initializer_list<int>),对于Wdiget w{2, 3.0},选择后者。如果有operator float() const的话,Wdiget w2(w)也选择后者,而不是拷贝构造。
        • 这个问题经常遇到,因为vector<int> v{10, 0} 的结果是2元素容器而不是10元素容器
        • initilaizer_list隐藏其他重载的情况只发生在构造函数,因为,普通函数的调用语法是不同的:a.call(1, 2) vs a.call({1, 2}),不构成重载
      • 虽说,有initlaizer_list的重载的时候,{}总是选它;但也有例外,Widget w{}选择默认构造而不是initializer_list构造。
    • 当为一个类添加initialier_list构造函数重载的时候,小心break掉客户代码。
      • 如果用户全部用()初始化,没问题,因为已有代码中不会有Widget w({1,2,3})的call
      • 如果用户全部用{}初始化,那么,必须小心initializer_list是否和某个重载类型兼容,如果有,那么已有代码可能被resolve到这边。
    • 结论
      • 单独看initializer_list的构造,很好,增加了一种快捷方式vector<int> v({1, 2, 3})
      • 单独看uniform intialization,很好,统一了c-style的结构体初始化。Point p{x, y}
      • 两者合在一起,出事了,initializer_list 以外的构造被隐藏了。vector<int> v{100}vector<int> v{10, 0} 都只调用了initailizer_list的构造
    • () 和 {} 的选择 (个人)
      • 非模板
        • c-style struct用 {}
        • 普通类用 ()。
          • 优点:即使有initializer_list的构造,也不会误用,因为那要求传入({})
          • 缺点:implicit narrowing conversion, the most vexing parse
        • 用 {} 来以值序列初始化容器。此时其他构造被隐藏
      • 模板
        • 使用 ()
          • 优点:能够正确的调用各个构造函数重载
          • 缺点:不支持c-style struct
        • 对比
          • 假如用{}
            • 优点:支持c-sytle struct
            • 缺点:一旦类支持intializer_list构造,此时不能访问其他构造
          • make_shared/make_unique内部用的是()。试想:
            • 用():make_shard<vector<int>>(10, 0) 会构造10元素容器;make_shared<vector<int>>({10, 0})会构造2元素容器
            • 用{}: make_shared<vector<int>>(10, 0) 会构造2元素容器;make_shared<vector<int>>({10, 0}) 会构造2元素容器
  • Item 8: Prefer nullptr to 0 and NULL
    • 0和NULL的缺陷
      • 会在接受int和指针参数的函数重载间选择前者,surprise
      • 本质上0和NULL只是int literal,只是当出现在需要指针的上下文中,fallback成指针,类似3可以被当做char和double一样。所以,当0和NULL被用于类型推导的时候,会出现意外。推导的结果类型希望是指针,但其实是int。
    • nullptr 的类型是nullptr_t,它可以隐式的转换成任意指针
    • 避免同时提供指针和int的重载,尽管使用nullptr没有问题,但还是有用户或遗留代码在使用0/NULL。
  • Item 9: Prefer alias declarations to typedefs
    • 当用于模板别名的时候,typedef往往需要std::remove_cv<T>::type,这里的::type是一个dependent name,被用在模板定义当中时,需要加typename 前缀(否则,编译器没法判断这里的type是一个变量还是类型,因为针对参数T,用户可以特化使得type可以是任意类型和变量),这比template alias更冗长。
    • C++14提供了类型转换的template alias。例如std::remove_const_t<T>std::remove_const<T>::type的改进
  • Item 10: Prefer scoped enums to unscoped enums
    • scoped enums 相对于unscoped enums的优点
      • 避免了名空间污染
      • Color.red是强类型的,而不再是个int,它不能隐式转换成整数类型。
      • 前者可以前置声明,后者不行,减少了编译依赖
        • 之所以unscoped enums不行,是因为,即使一个函数没用到特定的枚举值,但函数仍然需要知道枚举类型的underlying type。c98下的编译器根据具体的枚举值来选择最小的整数类型,从而确定enum的size,这就要求所有的enumerator可见,于是不允许前置声明。而scoped enums的默认underlying type是整数,也可以在前置声明中指定底层类型如enum class Color : std::uint32_t,因此不要求所有的枚举值可见。
    • std::underlying_type_t<TE>可以得到枚举的底层类型
  • Item 11: Prefer deleted functions to private undefined ones
    • deleted function 比 private undefined function 的优点
      • compile time error vs compile/linking time error
        • 外部类访问后者,compile time error
        • 类的其他方法访问后者,linking time error
      • 前者可以是public的,相比起来后者通过private来禁止访问,是一种work around
    • deleted function 可以用来删除全局函数和模板函数实例
      • 可以删除普通全局函数
      • 删除多态函数的某个实例
        • ad hoc多态:删除重载的某个实例;删除模板特化的某个实例;删除类模板方法的某个实例
        • subtyping多态:在派生类的override方法中notsupported抛异常
  • Item 12: Declare overriding functions override
    • override要求
      • 基类方法是virtual的
      • 基类派生类方法名相同,参数类型和个数相同
      • const和reference qualify相同
      • 返回值和异常声明兼容
    • 没有override,很容易出错:
      • 派生类实际上没有覆盖基类方法
      • 基类重构过后,break掉派生类,使得派生类的覆盖失败
    • override, final都是contextual keywords,仅当出现在特定位置的时候才是关键字,因此他们可以被用作方法名。这是为了向后兼容
  • Item 13: Prefer const_iterators to iterators
    • const_iterator总是推荐的,它在c++98中难用的问题已经被修复:
      • 一个non-const对象返回的是iterator而不是const_iterator,哪怕他只用于读取
        • cbegin, cend 总是返回const_iterator
      • insert/erase等方法要求iterator
        • 这些方法现在接受const_iterator
    • 模板中应该使用非成员方法的begin,end,rbegin,rend,cbegin,cend,rcegin,rcend
  • Item 14: Declare functions noexcept if they won't emit exceptions
    • noxcept的优势
      • noxcept只有抛和不抛两种情况,是另一种trade-off。c++98的异常规范是指定抛出异常的类型,具体异常类型集合成为签名的一部分过后,被证明很容易break掉用户代码(实现变化)。
      • 编译器可以生成更优化的代码。当异常规范被打破时(在运行时),c++98规范要求unwind stack后再结束程序,而新规范不要求unwind stack允许直接结束程序。这就导致了,在call void func() throw()的函数时,也要为stack unwinding做准备(异常规范被打破),这就要求有类似finally的代码块来保证析构局部变量,这有维护代价;而void func() noexcept没这个要求,因为即使异常规范被打破,也不要求执行析构,这就没有了异常处理的overhead,所以更优化。
        • 从编译器维护异常处理的代价来说,void func() throw()其实是和void func()一样的;只有void func() noexcept真正有优化机会
      • 库代码可以实现得更优化。当函数(特别是指swap)被声明成noexcept或者throw()时,由于不会抛出异常(或者打破异常规范、直接结束程序),库可以更高容易做到strong exception safety,这可能允许更高效的实现。在vector<int>内部的enlarge_capacity中,需要将元素从旧数组拷贝到新数组,如果类型的move 构造不会抛出异常(往往意味着swap不抛出异常),那么可以直接move,否则必须按老方法copy + swap才能做到strong exception safety。
    • noexcept 的常见形式
      • void func() noexcept 等价于 void func() noexcept(true)
        • 析构函数在c++11中默认是noxcept的了,这意味着曾经的析构不抛建议,在新标准中是语言层面的默认行为,一但抛出异常直接结束程序,除非显示指定noexcept(false)或者某个类成员/基类的析构有noexcept(false)
        • operator deleteoperator delete[] 也是默认的noexcept
      • conditional noexcept。void swap(pair& p) noexcept(noexcept(swap(first, p.first)) && noxcept(swap(second, p.second)))void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(*a, *b))
        • 复合结构的noexcept往往是建立在基本单元的noexcept之上
    • noexcept 规范是接口的一部分,修改它将break用户代码
    • 区分wide contracts和narrow contracts,前者可以从任何地方调用,后者要求一定的前条件。一般后者不能被实现为noexcept的,因为至少要verify precondition,
    • 编译器不会在noexcept的函数体中检查异常抛出情况
      • 因为大量的c++98和c函数没有规范
      • 直接throw异常是可能被编译器警告的
    • 结论
      • 大部分的函数都是异常中立(exception-neutral)的
      • noexcept 给了编译器和库更多的优化机会
      • noexcept 对move操作、swap、delete/desetructor特别有意义
  • Item 15: Use constexpr whenever possible
    • constexpr 可以用来标记变量,于是该变量可以被用作模板参数和枚举定义
    • constexpr可以被用来标记函数
      • 函数可以在编译期和运行时调用。编译期调用要求传入字面值或者constexpr表达式
      • constexpr的函数可以递归,C++14以后允许语句
    • consexpr 可以用来标记类的方法,包括构造函数和getter/setter (后者也是C++14后加入)
      • 结果是,constexpr的对象可以用于编译期的函数计算
      • constexpr 是类接口的一部分。这意味着它可以被用于编译期计算,如果后期方法的constexpr被移除,这将导致客户代码中的编译期计算被break掉。
      • constexpr方法默认也是const的
  • Item 16: Make const member functions thread safe
    • 对于一个可能被用在并发环境中的类,确保它的const方法是thread-safe的
      • 这可能需要声明mutable的mutex或者atomic字段
  • Item 17: Understand special member function generation
    • 生成的copy函数,是memberwise copy;生成的move函数,是memberwise move,如果某字段没有move则copy
    • 按照以前的Rules of Three原则,一旦用户自定义一个析构/拷贝或move构造/赋值,意味着该类默认的memberwise copy/move不再适用,其他几个都应该被自定义,编译器不应该再自动生成。由于C++98中,拷贝构造/赋值和析构的自动生成互不影响,因此在不破坏已有代码的情况下,新的自动生成规则如下:
      • 默认构造函数:自定义任意一个构造函数,默认构造不再生成
      • copy构造/赋值:定义其中一个不影响另一个的生成(老规则),但是会导致不再自动生成move
      • move构造/赋值:定义其中一个另一个也不再生成,同时不再生成copy和destructor。因为只可能在c++11代码中声明move,所以导致的copy/destructor被delete这个结果,并不会break老代码
      • 析构函数:定义过后,不再生成move。
      • 模板copy/move函数及其实例化,不影响上面的函数生成
    • 注意!对于一个C++11类,添加自定义的copy/move/destructor,都会导致原本的move不再自动生成,这可能导致性能下降!
    • 为你的C++11类,明确声明constructor/copy/move/destructor的=default的行为

Smart Pointers

  • Item 18: Use std::unique_ptr for exclusive-ownership resource management
    • 裸指针的缺点
      • 没有说明指向的单元素还是数组
      • 没有说明你是否应该负责销毁指向的元素
      • 没有说明应该怎样销毁,delete还是destroy
      • 如果是delete,没有说明应该用delete还是delete[]
      • 很难保证销毁、并且仅销毁一次
      • 没法判断一个指针是否是悬空指针
    • 析构函数在很多情况下不保证被调用(智能指针也失效)
      • 异常没有被处理的时候(抛出main函数,此时不保证stack unwinding;相对的,此时c#的finally块儿保证被调用)
      • noexcept异常规范被破坏的时候
      • std::abort 或者 exit (std::_Exit, std::exit, std::quick_exit)函数被调用的时候
    • unique_ptr 几乎和裸指针性能一样,没有shared_ptr的那些性能惩罚(atomic ref count, control block allocation, virtual call of deleter, at least 2 word size)
    • 关于std::unique_ptr
      • deleter是type的一部分,所以声明unique_ptr变量的地方,要求deleter访问的对象是complete type
      • 因为deleter是type的一部分,所以实际的销毁是inline的,没有性能惩罚
      • 默认的deleter是delete,此时unique_ptr对象的大小是一个字
      • customer deleter如果是stateless的话,unique_ptr对象的大小仍然是一个字;否则会加上deleter的状态大小
    • unique_ptr 被广泛的用作工厂函数,尤其是他可以隐式转换成 shared_ptr
    • unique_ptr 被广泛的用于 pimpl idiom
    • unique_ptr 支持数组形式 (unique_ptr<T[]>),此时不能解引用(operator*operator->),但支持operator[];数组形式没有隐式的向下转型(因为数组的元素size不同)
      • 尽管此时用std::vector和std::array更好
  • Item 19: Use std::shared_ptr for shared-ownership resource management
    • 相对于GC的优势:generality and predictability
    • shared_ptr 的缺点
      • 对象是2个字大 (对象指针+控制块指针)
        • 控制块(control block)中,包括引用数、弱引用数、deleter数据
        • 控制块的实现中往往需要虚函数
      • 除非使用make_shared,控制块的内存需要一次额外分配
      • 增加和减小引用计数是原子的
        • 所以move运算比copy运算快,前者不需要修改引用数
    • deleter 不是类型的一部分,因此更灵活,但有运行时惩罚
      • 因此,声明shared_ptr变量的时候,不要求completed type,只在初始化的时候要求可见
      • deleter 的大小不影响 shared_ptr 对象大小
    • 有 shared_from_this() 需求的类型应该继承 enable_shared_from_this ,然后声明 private 构造,然后工厂函数返回 shared_ptr
      • 构造 shared_ptr 的时候,enable_shared_from_this 内部会记录 control block 的地址
    • 不应该用 shared_ptr 保存数组(T[])
      • 默认的delete不能用于数组
      • shared_ptr 允许从drived sp转换到base sp,这对数组是错的
    • 不应该通过裸指针构造shared_ptr,因为这个裸指针可能被多次用于构造sp
  • Item 20: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle
    • 需要通过 lock 来返回一个 shared_ptr 是因为分离的 expired() 判断和解引用有race condition
    • 在很多实现当中,control block 中的weak ref count是不等于weak_ptr对象数的。比如,可能用额外的1表示有shared_ptr存在
    • 典型应用
      • cache
      • observer list
      • prevention of shared_ptr cycles
  • Item 21: Prefer std::make_unique and std::make_shared to direct use of new
    • make_unique 和 make_shared 不支持custom allocator,后者有提供对应的 allocate_shared,它的第一个参数是allocator
    • make_xxx 的优点
      • make + auto 只需要写对象类型一次,而 new 需要两次(声明sp+new),避免了重复
      • exception safety。一旦new和ptr的构造之间有其他函数调用,并抛出了异常,会是资源泄露
        • 在c++17以前,函数各个实参求值之间彼此是unsequenced关系,即可用相互overlap,此时写符合表达式来初始化sp对象很容易导致泄露;c++17以后,求值顺序变成了indeterminately sequenced,稍好,但一样不可避免异常安全问题
        • 对策,应该以单独的语句来以空指针初始化sp (当然此句之前,上至new,都不应该有异常)
      • make_shared 有性能优势,因为 control block 可以和对象放在一个内存块上
        • 两次分配在性能和尺寸上都有惩罚
      • 以裸指针构造sp,可能导致同一个裸指针被用于构造多个sp
    • make_xxx 的缺点
      • 不支持 custom deleter。尤其是 customer deleter 往往也要求特殊的 allocator
      • make 内部使用的是 () 初始化而不是 {} 初始化
        • 无法初始化c-style struct
        • 无法完美转发 braced-initialization,所以make_shared<vector<int>>({1, 2, 3}) 是无效的,必须引入一个额外的initializer_list 变量
      • make_shared 不支持用户自定义operator new和operator delete,因为make_shared实际分配的内存是object size + control block size
      • make_shared 使得control block 和对象布局在同一个内存块上,虽然对象已经因为ref count归0而析构,但整块内存还要等到weak ref count归零才回收。如果weak_ptr生命期明显比较长,而对象尺寸又很大,那么这导致这块内存长时间不得释放。而直接构造shared_ptr会有两个内存块,对象内存块在ref count归0就回收了,weak ref count只会延迟control block的回收。
  • Item 22: When using the Pimpl Idiom, define special member functions in the implementation file
    • 使用 unique_ptr 来实现 Pimpl idiom 时,由于 deleter 是 unique_ptr 的一部分,所以需要在实现代码中明确的定义析构和move op甚至copy op
      • 多用 = default
    • 使用 shared_ptr 来实现 Pimpl idiom 时(往往是实现flyweight pattern的对象),只需要写构造即可,因为deleter不再是类型的一部分,只在构造shared_ptr的时候要求completed type

Rvalue References, Move semantics, Perfect forwarding

  • Item 23: Understand std::move and std::forward
    • move和forward不做移动等,只做类型转换
    • 由于命名的右值变量不再是临时值,所以他们会自动变成左值。比如T&&->T&T const&& -> T const &。正因为此,右值变量可以被多次引用而不用担心内容破坏,除非显示的move或者forward
      • const 右值一般只能绑定到const左值引用,导致copy而不是move
    • move 是将绑定的任意值(由于右值变量是左值,所以move的实参一般是左值)无条件转换成右值
      • 使用move的场合,其实也可以使用forward,但此时用move更能表达目的以及更简洁
      • 参考实现:
      template<typename T>
      decltype(auto) move(T&& v)
      {
          return static_caset<std::remove_reference_t<T>&&>(v);
      }
      
    • forward是将收到的任意值(一般是左值)根据显示模板参数转换回左值或者右值
      • 参考实现:
      template<typename T>
      T&& forward(T& v)
      {
          return static_cast<T&&>(v);
      }
      
  • Item 24: Distinguish universal references from lvalue references
    • T&& 出现在需要类型推导的场合,是universal reference,包括模板函数和auto;出现在具体类型的场合,是右值引用,包括模板类的成员和具体类
    • 注意在对泛型lambda的auto&&形参做转发时,应该用forward<decltype(v)>
      • 这里的形参也是universal reference
  • Item 25: Use std::move on rvalue references, std::forward on universal references
    • rvalue reference形参的最后一次访问(传参或者返回)用move,目的是move copy
      • 前几次访问右值引用变量会退化成左值引用
    • universal reference形参的最后一次访问用forward,目的是绑定到右值时move copy
      • 前几次访问变量将总是左值引用
    • 局部变量和传值形参直接传参或者return
      • 当局部变量和传值形参类型和返回值类型完全一样,并且return表达式就是变量名时,编译器被要求应用RVO(此时是NRVO),如果没有做这种优化,至少应该自动将变量视作右值而不是左值
      • RVO由于直接在函数返回值的地址上构造局部变量,所以省掉了move,性能最高
      • 此时用std::move反而导致RVO不能工作,性能可能变低
  • Item 26: Avoid overloading on universal references
    • 隐式转换的评分
      1. exact match: no conversion required, lvalue-to-rvalue conversion, qualification conversion, function pointer conversion, (since C++17) user-defined conversion of class type to the same class
      2. promotion: integral promotion, floating-point promotion
      3. conversion: integral conversion, floating-point conversion, floating-integral conversion, pointer conversion, pointer-to-member conversion, boolean conversion, user-defined conversion of a derived class to its base
    • 重载函数的匹配优先级
      • 所有参数exact match的非模板函数
      • 实例化后exact match的模板函数
      • 需要promotion或者conversion的非模板函数 (这种情况是模板函数不存在或者也需要转换或者遭遇SFINAE)
      • c语言变参函数
    • SFINAE (substitute failure is not an error)
      • 条件
        • 模板参数替换后导致ill-formed: A substitution failure is any situation when the type or expression above would be ill-formed (with a required diagnostic), if written using the substituted arguments.
        • 只有引用的ill-formed是SFINAE,如果需要生成其他类型,则是hard error: Only the failures in the types and expressions in the immediate context of the function type or its template parameter types are SFINAE errors. If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors.
      • alternatives
        • Where applicable, tag dispatch, static_assert, and, if available, concepts, are usually preferred over direct use of SFINAE.
    • 由于完美转发几乎总能造成exact match,所以在重载决议中出于极高的优先级(第2优先级),如果目标函数模板exact match而是需要转换,则会被完美转发异常。
      • 完美转发的贪婪性质,使得应该避免声明完美转发的重载
    • 声明完美转发的构造函数时,很容易隐藏const move和const copy构造函数,非常危险
  • Item 27: Familiarize yourself with with alternatives to overloading on universal references
    • 避免完美转发歧义的方法
      • 避免重载(abandon overloading):如果重载的函数是一般函数,那么专门为完美转发声明一个唯一的函数名
      • 使用tag dispatch: 声明一个完美转发的接口,然后利用tag来dispatch到各个实现(包括完美转发重载的实现)。该技巧不能用在构造函数上
      • 传const引用(pass by const T&):构造时使用c++98的老方法,缺点是实参是临时值时可能多一次拷贝
      • 传值(pass by value): 优点是传左值和右值都只有一次拷贝(或者构造)
      • 利用enable_if等SFINAE手段:该技巧可以应用在构造函数上,但是代码冗长,一般在有其他可选方案时优先考虑其他
        • std::decay_t 进行的类型转换相当于传值,即,数组函数都变指针,cv/ref都去掉
    • 完美转发的新更能比较好,但是一旦错误错误报告非常冗长。即,性能好,但可用性成问题。
  • Item 28: Understand reference collapsing
    • 编译器不允许声明引用的引用,但它会自动在合适的场合折叠引用
      • 规则:任意一个引用是左值引用,结果是左值引用;否则是右值引用
      • 引用折叠发生的场合
        • 模板函数的类型推导
        • auto
        • decltype
        • typedef
  • Item 29: Assume that move operations are not present, not cheap, and not used
    • 假设的原因
      • not present: 由于copy拷贝/赋值和析构会禁用move拷贝和赋值的生成,所以c++98写成的库中的很多类,直接用c++11编译,都不会从move中受益
      • not cheap: std::array以及支持SSO(small string optimization)的std::string实现的move都不是O(1)的
      • not used: 一些库实现只在move是noexcept或者throw()的时候进行move(即使用move_if_noexcept),其他时候进行copy,这可能是出于strong exception safety的考虑
    • 只需要在模板中进行假设,针对具体类型编程的时候尽管利用类型本身的move/copy性质
  • Item 30: Farmiliarize yourself with perfect forwarding failure cases
    • failure cases
      • braced initializer: forward({1, 2, 3})
        • work around: var list = {1, 2, 3}; forward(list);
      • 0 or NULL pointers
        • work around: use nullptr
    • declaration-only integral static const and constexpr data members
      • 因为完美转发是引用,要求变量地址,而没有定义的常量声明其实只是在利用编译器的constant propagation,一旦要求地址就不行了
      • work around: 在cpp中定义(但不用再声明初始值)
    • 重载函数和模板函数
      • work around: static_cast 或者声明显示类型的局部变量
    • Bitfields
      • 因为位域没有地址,无法传引用
      • work around: 声明局部变量

Lambda Expressions

  • Item 31: Avoid defualt capture models
    • default by-reference capture 可能导致dangling reference
    • default by-value capture 可能导致dangling pointer
      • 尤其危险的是隐式捕获了this
  • Item 32: Use init capture to move objects into closures
    • 通过init capture来move对象到closure中
    • 在c++11中,可以使用std::bind来达到同样的效果
  • Item 33: Use decltype on auto&& parameters to std::forward them
    • 在generic lambda内部forward形参时,用std::forward<decltype(v)>,这里的decltype(v)用来代替template deduction中的T
  • Item 34: Prefer lambdas to std::bind
    • lambda 可读性更强。
    • lambda 表达力更强。相比之下std::bind只能在创建closure的时候求值
    • lambda 更高效。
      • lambda 实例有特定的类型,可以inline;相比之下,bind其实接受的指针,有一个间接层
    • 在c++11中,std::bind可以用来创建template callable对象;lambda则必须要c++14的generic lambda

The Conrruency API

  • Item 35: Prefer task-based programming to thread-based
    • std::future 可以用来表示异步结果,可以转发异常
      • std::thread 中如果有unhandled exception,会terminate程序
    • std::async能够利用线程池,于是在系统线程不足的时候允许同步执行,允许load balancing(底层实现可能是OS level的),可以方便的移植到新系统
    • 在以下情况时使用std::thread是合适的:
      • 需要访问底层线程接口,std::thread允许通过native_handle访问底层线程句柄
      • 想要自己优化线程调度。比如自制线程池、优先级、core亲和性
  • Item 36: Specify std::launch::async if asynchornicity is essential
    • std::async 默认的标志允许同步或者异步执行,甚至可能永远不执行(同步并且没有call get的时候),这将导致以timeout的方式等待异步执行结束的代码出错,更可怕的是,可能只在work load较高的时候出错(此时实现认为线程不够异步执行)
    • 默认标志不确定同步/异步执行的结果是,应用层thread_local的使用可能出错
    • 建议自己在std:;async的基础上包装,显示要求异步执行(或者在提供一个显示同步的包装)
  • Item 37: Make std::threads unjoinable on all paths
    • std::thread 在析构的时候,如果发现对象仍然是joinable的(即从没有join或者detach),将断言
      • 如果默认行为总是join,则可能不必要的影响性能
      • 如果默认行为总是detach,那么线程方法体可能对局部变量的引用会导致dangling reference
  • Item 38: Be aware of varying thread handle destructor behavior
    • future (shared_future)和 promise 都持有一个对shared_state的引用
    • 最后一个future析构的时候,将导致释放shared_state,如果此时task是异步并且joinable,那么默认将join
      • 这仅限于std::async返回的future;因为packed_task返回的future,实际负责join的是委托的std::thread
  • Item 39: Consider void futures for one-shot event communication
    • condvar有两个问题
      • 如果notify 早于wait,此次notify将丢失,这可能导致hang
      • spurious wakeup: 没有notify,却可能因为OS实现而偶然的wakeup
    • 正确的condvar一般要求配合一个bool来使用:
      • 如果notify早于wait,bool可以用来表示状态,不至于丢失信号
      • spurious wakeup导致的唤醒,可以用bool来二次确认是否真的唤醒
    • 使用std::atomic<bool>也能做线程间通信,但他将导致busy waiting,不能真正的block住。超线程情况下影响更严重
    • promise + future 也可能完成通信的任务,但只能是单次唤醒
  • Item 40: Use std::atomic for concurency volatile for special memory
    • std::atomic 的作用:避免对多次load/多次save的优化;原子性;避免编译器乱序/避免cpu乱序(由于false sharing)。用于多线程
    • volatile的作用:每次访问都真正读写内存,避免优化。用于访问特殊内存
      • 主要是memory-mapped IO:此时,多次读其实是在从设备读取实时只,每次读的结果都不同;多次写可能是在对设备发送命令
      • 共享内存:多次读可能得到的是其他进程的多次副作用;多次写可能被其他线程用作不同目的
        • 多线程情况其实也类似

Tweaks

  • Item 41: Consider pass by value for copyable parametesr that are cheap to move and always copied
    • 一般有三种选择:
      • const左值引用、右值引用的重载: 缺点是维护两个函数
      • 完美转发:缺点是code size大,可用性不好
      • 传值
    • 传值建议仅在满足三个条件的情况下适用
      • 实参是copyable的类型。否则,如果只能move,那么简单声明右值引用版本即可
      • move廉价。比如对支持SSO的string实现,move和copy几乎一样昂贵,那么传值方案基本退回到c++98的情形,此时建议const引用
      • 一定需要拷贝。比如在构造函数中。如果不需要拷贝,那么const引用就足够了
    • 传值有slicing问题,因此不应该对基类做传值
  • Item 42: Consider emplacement instead of insertion
    • insert, push_back, push_froint,insert_after都有emplace版本,可以省掉至少一次构造和析构
    • 在这些情况下,emplace可能更快
      • 新值是在容器中被构造,而不是赋值
      • 实参类型和容器元素类型不同(也意味着要构造)
      • 容器不会否决掉这次添加(比如不是在往set中添加重复值)
  • emplace 实际是direct intialization而不是copy intialization,因此是explicit构造而不是隐式构造