简介

  • Effective Modern C++ 中文版学习笔记,Scott Meyers著作,高博译

译者序

  • 现代C++在语言方面所进行的大刀阔斧,釜底抽薪式的变革,无需赘言。但是这些变革背后,更重要的反而是其保持不变者,即所谓C++语言的精神,或曰设计哲学。例如,由实际问题驱动,并立刻用于解决实际问题。现代C++中提供了并发API,在语言层面上支持并发程序设计,结束了在各种体系结构和操作系统之上存在很多互不兼容的第三方并发库的乱局,就是这种设计哲学的体现。程序员应该能够自由地选择自己的程序设计风格,而语言应该为该风格提供完备的支持
  • C++语言之难,主要还是在于众多语言特性之间的综合交叉。尤其对于新的语言特性,掌握其本身往往并不是很难,而要考虑到它与众多也已经存在的语言特性之发生的相互作用就不容易了。

绪论

  • 本书的写作目的并非对于C++11和C++14特性的泛泛介绍,而是为了揭示他们的高效应用。
  • 本书中的信息被分解成若干准则,称为条款。本书中的条款都是准则,而非规则,因为准则允许有例外。条款给出的建议并非最要紧的部分,建议背后的原理才是精华。只有掌握了原理,你才能判定,你的项目面临的具体情况是否真的违反了条款所指。本书的真正目标并不在于告诉你什么该做,什么不该做,而是想要传达对C++11和C++14运作原理的更深入理解

术语和惯例

  • C++98缺乏并发支持(仅对C++98和C++03成立)
  • C++11支持lambda表达式(对C++11和C++14成立)
  • C++14提供了广义返回值性别推导(仅对C++14成立)

  • C++11被最广泛接受的特性可能莫过于移动语义,而移动语义的基础在于区分左值表达式和右值表达式。因为,一个对象是右值意味着能够对其实施移动语义,而左值则一般不然。从概念上说(实践上并不总是成立),右值对应的是函数返回的临时对象,而左值对应的是可指涉的对象,而指涉的途径则无论通过名字,指针,还是左值引用皆可。
  • 有一种甄别表达式是否左值的使用方法富有启发性,那就是检查能否取得该表达式的地址。如果可以取得,那么该表达式基本上可以断定是左值。如果不可以,则其通常是右值。这种方法之所以说富有启发性,是因为它让你记得,表达式的型别与它是左值还是右值没有关系。换言之,给定一型别T,则既有T型别的左值,也有T型别的右值。这一点在处理右值引用型别的形参时尤其要注意,因为该形参本身是个左值。
    1
    2
    3
    4
    5
    
    class Widget
    {
    public:
      Widget(Widget&& rhs);   // rhs是个左值,尽管它具有右值引用型别
    };
    
  • 在Widget的移动构造函数内部对rhs取址完全没有问题,所以rhs是个左值,尽管它的型别属于右值引用(基于类似的理由,我们可以得知,任何形参都是左值)

  • 若某对象是依据同一型别的另一对象初始化出来的,则该新对象称为提供初始化依据的对象的一个副本,即使该副本是由移动构造函数创建的。这样称呼情有可原,因为C++中并无术语用以区分某对象到底是经由复制否早函数创建的副本,还是经由移动构造函数创建的副本。
    1
    2
    3
    4
    
    void someFunc(Widget w);    // someFunc的形参w按值传递
    Widget wid;                 // wid是Widget型别的某个对象
    someFunc(wid);              // 在这个对someFunc的调用中,w是wid经由复制构造函数创建的副本
    someFunc(std::move(wid));   // 在这个对someFunc的调用中,w是wid经由移动构造函数创建的副本
    
  • 右值的副本经常经由移动构造函数创建,而左值的副本通常经由复制构造函数创建。这也就是说,如果你仅仅了解到某个对象是另一个对象的副本,则还不能判断构造这个副本要花费多少成本。

  • 在函数调用中,调用方的表达式,称为函数的实参。实参的用处,是初始化函数的形参。
  • 在上面someFunc的第一次调用中,实参是wid。而在第二次调用中,实参则是std::move(wid)。在两次调用中,形参都是w。
  • 实参和形参有着重大的区别,因为形参都是左值,而用来作为其初始化依据的实参,则极可能是右值,也可能是左值。这一点在完美转发(perfect forwarding)的过程中尤其关系重大,在这样的一个过程中,传递给某个函数的实参会被传递给另一个函数,并保持其右值性(rvalueness)或左值性(lvalueness)

  • 设计良好的函数都是异常安全的,这意味着它们至少会提供基本异常安全保证(即基本保证)。提供了基本保证的函数能够向调用者确保即使有异常抛出,程序的不变量不会受到影响(即不会有数据结构被破坏),且不会发生资源泄露。而提供了强异常安全保证(即强保证)的函数则能够通过向调用者确保即使有异常抛出,程序状态会在调用前后保持不变。
  • 当提及函数对象时,我通常意指某个对象,其型别支持operator()成员函数。换言之,就是说该对象表现得像个函数。进一步泛化这一术语的话,就涵盖了指涉到成员函数的指针,从而得到了所谓的可调用物。一般情况下,你可以不用关心这些含义之前的细微差别,函数指针也好,可调用物也罢,你只需要知道他们在C++中表示某种函数调用语法加以调用就行了。

  • 经由lambda表达式创建的函数对象称为闭包,将lambda表达式和他们创建的闭包区分开来,意义不大,所以我经常把他们统称为lambda式。
  • 相似的,我也很少区分函数模板(即用以生成函数的模板)和模板函数(即从函数模板生成的函数)。类模板和模板类的情形同上。

  • C++中有很多事物能够加以声明和定义。声明的作用是引入名字和型别,而不给出细节,例如存储位置或具体实现
    1
    2
    3
    4
    
    extern int x;               // 对象生命
    class Widget;               // 类声明
    bool func(const Widget& w); // 函数声明
    enum class Color;           // 限定作用域的枚举声明
    
  • 定义则会给出存储位置和具体实现的细节。
  • 定义同时也可以当声明用。所以,除非某些场合非给出定义不可,我倾向于只使用声明。
  • 我把函数声明的形参型别和返回值型别这部分定义为函数的签名,而函数名字和形参名字则不属于签名的组成部分。
  • 函数声明除形参型别和返回值型别的其他组成元素(即可能存在的noexcept或constexpr)则被排除在外(noexcept和constexpr)
  • 签名的官方定义与我给出的稍有不同,但是在本书中,我们定义的更加使用(官方定义有时会省区返回值型别)

  • 标准有时会把某个操作的结果说成是未定义行为。意思是,其运行期行为不可预测,你当然会对这样的不确定性敬而远之。未定义行为的例子有,在方括号([])内使用越界值作为std::vector的下表,未初始化的迭代器实施提领操作,或者进入数据竞险(即两个或更多线程同时访问同一内存位置,且其中至少有一个执行写操作的情形)

  • 我将内建的指针,就是new表达式返回的那些指针,称为萝指针。而与裸指针形成对照的,则是智能指针。智能指针通常都重载了指针提领运算符(operator->和operator*)

第一张 型别推导

  • C++98仅有一套型别推导规则,用于函数模板。C++11对这套规则进行了一些改动,并且增加了两套规则,一套用于auto,另一套用于decltype。后来,C++14又扩展了能够运用auto和decltype的语境。型别推导应用返回的不断普及,使得人们不惜再去写下那些不言自明或是完全冗余的型别。
  • 想要使用现代C++高效编程,就离不开对型别推导操作的坚实理解。型别推导设计的语境实在不胜枚举: 在函数模板的调用中,在auto现身的大多数场景中,在decltype表达式中,特别是在C++14中那个神秘莫测的decltype(auto)结构中
  • 本章解释了模板型别推导如何运作,auto的型别推导如何构建在此运作规则之上,以及decltype独特的型别推导规则。

条款一:理解模板型别推导

  • 如果一个复杂系统的用户对于该系统的运作方式一无所知,然而却对其提供的服务表示满意,这就充分说明系统设计得好。
  • 模板的型别推导,是现代C++最广泛应用的特性之一–auto的基础。