【C++】《C++ 并发编程实战 (第2版) 》笔记-ChapterA-C++11 精要:部分语言特性

附录A、C++11 精要:部分语言特性
  1. 右值引用

    int var = 42;
    int& ref = var; // 创建名为 ref 的引用,指向的模目标是变量 var
    
    int &i = 42;    // 无法编译
    int const& i = 42;  // 我们一般都能将右值绑定到 const 左值引用上
    
    int&& i = 42;
    int j = 42;
    int&& k = j;    // 编译失败
    
    • 术语右值来自 C 语言,指只能在赋值表达式等号右边出现的元素,如字面值和临时变量。
    • 左值引用只可以绑定左值,而无法与右值绑定。
    • C++11 标准采纳了右值引用这一新特性,它只与右值绑定,而不绑定左值。另外,其声明不再仅仅带有一个“&”,而改为两个“&”。
  2. 移动语义

    1. 右值往往是临时变量,故可以自由改变。
      void process_copy(std::vector<int> const& vec_)
      {
          std::vector<int> vec(vec_);
          vec.push_back(42);
      }
      
      void process_copy(std::vector<int> && vec)
      {
          vec.push_back(42);
      }
      
      • void process_copy(std::vector const& vec_) 这个函数接受左值和右值(此处的右值特指前文的绑定常量的引用,而非 C++11 新特性的右值引用)皆可,但都会强制进行复制。
      • 若我们预知原始数据能随意改动,即可重载该函数,编写一个接受右值引用的参数的版本 void process_copy(std::vector && vec),以此避免复制(这里为了讲解移动语义,刻意采用右值引用传参,但实际上,按传统的非 const 左值引用传参也能避免复制(直接引用原始数据,并不采用移动语义))。
    2. 具备移动构造函数的类
      class X
      {
      private:
          int* data;
      public:
          X()
           : data(new int[1000000])
          {}
          ~X()
          {
              delete [] data;
          }
          X(const X& other)
           : data(new int[1000000])
          {
              std::copy(other.data, other.data+1000000, data);
          }
          X(X&& other)
           : data(other.data)
          {
              other.data = nullptr;
          }
      }
      
      • 移动构造函数,它复制 data 指针,将源实例的 data 指针改为空指针,从而节约了一大块内存,还省去了复制数据本体的时间。
      • 某些类很有必要实现移动构造函数,强令它们实现拷贝构造函数反而不合理。以 std::unique_ptr<> 指针为例,其非空实例必须指向某对象,根据设计意图,它也肯定是指向该对象的唯一指针,故只许移动而不许复制,则拷贝构造函数没有存在的意义。
    3. 假如某个具名对象不再有任何用处,我们想将其移出,因而需要先把它转换成右值,这一操作可通过 static_cast<X&&> 转换或调用 std::move() 来完成。
      X x1;
      X x2 = std::move(x1);
      X x3 = static_cast<X&&>(x2);
      
    4. 尽管右值引用的形参与传入的右值实参绑定,但参数进入函数内部后即被当作左值处理。所以,当我们处理函数的参数的时候,可将其值移入函数局部变量或类的成员变量,从而避免复制整份数据。
      void do_stuff(X&& x_)
      {
          X a(x_);            // 复制构造
          X b(std::move(x_)); // 移动构造
      }
      do_stuff(X());  // 正确,X() 生成一个匿名临时对象,作为右值与右值引用绑定
      X x;
      do_stuff(x);    // 错误,具名对象 x 是左值,不能与右值引用绑定
      
    5. std::thread、std::unique_lock<>、std::future<>、std::promise<> 和 std::packaged_task<> 等类无法复制,但它们都含有移动构造函数,可以在其实例之间转移关联的资源,也能按转移的方式充当函数返回值。
    6. 按照良好的编程实践,类需确保其不变量的成立范围覆盖其“移出状态”。如果 std::thread 的实例作为移动操作的数据源,一旦发生了移动,它就等效于按默认方式构造的线程实例。(按默认方式构造的 std::thread 对象不含实际数据,也不管控或关联任何线程)
    7. 对于 std::string 类,C++ 标准仅要求移动操作在常数复杂度的时间内完成,却没有规定源数据上的实际效用如何。因此,要注意,移动语义可能通过不同方式实现,不一定真正窃取数据,也不一定搬空源对象。
  3. 右值引用和函数模板

    1. 假如函数的参数是右值引用,目标是模板参数,那么根据模板参数的自动类型推导机制,若我们给出左值作为函数参数,模板参数则会被推导为左值引用;若函数参数是右值,模板参数则会被推导为无修饰型别的普通引用。
      template<typename T>
      void foo(T&& t)
      {}
      
      foo(42);            // 调用 foo<int>(42)
      foo(3.14159);       // 调用 foo<double>(3.14159)
      foo(std::string()); // 调用 foo<std::string>(std::string())
      
      int i = 42;
      foo(i);             // 调用 foo<int&>(i)
      
      • 根据函数声明,其参数型别是 T&&,在本例的情形中会解释成“引用的引用”,所以发生引用折叠(左值的多重引用会引发折叠),编译器将它视为原有型别的普通引用。这里,foo<int&>() 的函数签名是 “void foo<int&>(int& t);”。
      • 利用该特性,同一个函数模板既能接收左值参数,又能接收右值参数。std::thread 的构造函数正是如此。若我们以左值形式提供可调用对象作为参数,它即被复制到相应线程的内部存储空间;若我们以右值形式提供参数,则它会按移动方式传递。
  4. 删除函数

    1. 要禁止某个类的复制行为,以前的标准处理手法是将拷贝构造函数和复制赋值操作符声明为私有,且不给出实现。假如有任何外部代码意图复制该类的实例,就会导致编译错误(因为调用私有函数);若其成员函数或友元函数试图复制它的实例,则会产生链接错误(因为没有提供实现)。
    2. 声明函数的语句只要追加“=delete”修饰,函数即被声明为“删除”。
    3. 若我们在实现某个类的时候,既删除拷贝构造函数和复制赋值操作符,又显式写出移动构造函数和移动赋值操作符,它便成了“只移型别”,该特性与 std::thread 和 std::unique_lock<> 的相似。
    4. 只移对象可以作为参数传入函数,也能充当函数返回值。然而,若要从某个左值移出数据,我们就必须使用 std::move() 或 static_cast<T&&> 显式表达该意图。
    5. 说明符“=delete”可修饰任何函数,而不局限于拷贝构造函数和赋值操作符,其可清楚注明目标函数无效。它还具备别的作用:如果某函数已声明为删除,却按普通方式参与重载解释并且被选定,就会导致编译错误。利用这一特性,我们即能移除特定的重载版本。例如,假如某函数接收 short 型参数,那它也允许传入 int 值,进而将 int 值强制向下转换成 short 值。若要严格杜绝这种情况,我们可以编写一个传入 int 类型参数的重载,并将它声明为删除。
  5. 默认函数

    1. 一旦将某函数标注为删除函数,我们就进行了显式声明:它不存在实现。但默认函数则完全相反:它们让我们得以明确指示编译器,按“默认”的实现方式生成目标函数。如果一个函数可以由编译器自动产生,那它才有资格被设为默认:默认构造函数、析构函数、拷贝构造函数、移动构造函数、复制赋值操作符和移动赋值操作符等。
    2. 一般来说,仅当用户自定义构造函数不存在时,编译器才会生成默认构造函数,针对这种情形,添加“=default”修饰即可保证其生成出来。
    3. 令析构函数成为虚拟函数,并托付给编译器生成。
    4. “默认构造函数”特指不接收任何参数的构造函数(或参数全都具备默认值)。
    5. 在同一个类中,若将某些成员函数交由编译器实现,它们便会具备一定的特殊性质,但是让我们自定义实现,这些性质就会丧失。两种实现方式的最大差异是,编译器有可能生成平实函数。
    6. 平实函数即 trivial function,其现实意义是默认构造函数和析构函数不执行任何操作;复制、赋值和移动操作仅仅涉及最简单、直接的按位进行内存复制/内存转移,而没有任何其他行为;若对象所含的默认函数全是平实函数,就可依照 Plain Old Data(POD)方式进行处理。
    7. constexpr 函数所用的字面值型别必须具备平实构造函数、平实拷贝构造函数和平实析构函数。
    8. 若要允许一个类能够被联合体(union)所包含,而后者已具备自定义的构造函数和析构函数,则这个类必须满足:其默认构造函数、拷贝构造函数、复制操作符和析构函数均为平实函数。
    9. 假定某个类充当了类模板 std::atomic<> 的模板参数,那它应当带有平实拷贝赋值操作符,才可能提供该类型值的原子操作。
    10. 一旦函数由用户自己动手显示编写而写,就肯定不是平实函数。
    11. 在同一个类中,某些特定的成员函数既能让编译器生成,又准许用户自行编写,我们继续分析两种实现方式的第二项差异:如果用户没有为某个类提供构造函数,那么它便得以充当聚合体,其初始化过程可依照聚合体初值表达式完成。
    12. 聚合体即 aggregate,是 C++ 11 引入的概念,它通常可以是数组、联合体、结构体或类(不得含有虚函数或自定义的构造函数,亦不得继承自父类的构造函数,还要服从其他限制),其涵盖范围随 C++ 标准的演化而正在扩大。
    13. 如果类 X 的实例在初始化时显示调用了默认构造函数,成员 a 即初始化为 0。
      struct X
      {
          int a;
      }
      X x1;           // x1.a 的值尚未确定
      X x2 = X();     // x2.a == 0 必然成立
      
      • 这个特殊性质还能扩展至基类及内部成员。假定某个类的默认构造函数由编译器产生,而它的每个数据成员与全部基类也同样如此,并且后者两者所含的成员都属于内建型别。那么,这个最外层的类是否显式调用该默认构造函数,就决定其成员是否初始化为尚未确定的值,抑或发生零值初始化。
      • 一旦我们手动实现默认构造函数,它就会丧失这个性质:要是指定了初值或显式地按默认方式构造,数据成员便肯定会进行初始化,否则初始化始终不会发生。
      • 一般情况下,若我们自行编写出任何别的构造函数,编译器就不会再生成默认构造函数。如果我们依然要保留它,就得自己手动编写,但其初始化行为会失去上述特性。然而,将目标构造函数显式声明成“默认”,我们便可强制编译器生成默认构造函数,并且维持该性质。
        X::X() = default;  // 默认初始化规则对成员 a 起作用
        
      • 原子类型正是利用了这个性质将自身的默认构造函数显式声明为“默认”。除去下列几种情况,原子类型的初值只能是未定义:它们具有静态生存期(因此静态初始化成零值);显式调用默认构造函数,以进行零值初始化;我们明确设定了初值。请注意,各种原子类型均具备一个构造函数,它们单独接受一个参数作为初值,而它们都声明成 constexpr 函数,以准许静态初始化发生。
  6. 常量表达式函数

    1. 常量表达式可用于创建常量,进而构建其他常量表达式。一些功能只能靠常量表达式实现。
      1、设定数组界限:
      int bounds = 99;
      int array[bounds];      // 错误,界限 bounds 不是常量表达式
      const int bounds2 = 99;
      int array2[bounds2];    // 正确,界限 bounds2 是常量表达式
      
      2、设定非类型模板参数的值:
      template<unsigned size>
      struct test
      {};
      test<bounds> ia;        // 错误,界限 bounds 不是常量表达式
      test<bounds2> ia2;      // 正确,界限 bounds2 是常量表达式
      
      3、在定义某个类时,充当静态常量整型数据成员的初始化表达式:
      class X
      {
          static const int the_answer = forty_two;  
      };
      
      4、对于能够进行静态初始化的内建型别和聚合体,我们可以将常量表达式作为其初始化表达式:
      struct my_aggregate
      {
          int a;
          int b;
      }
      static my_aggregate ma1 = {forty_two, 123};     // 静态初始化
      int dummy = 257;
      static my_aggregate ma2 = {dummy, dummy};       // 动态初始化
      
      5、只要采用本例示范的静态初始化方式,即可避免初始化的先后次序问题,从而防止条件竞争。
      
      • 静态数据成员 the_answer 由表达式 forty_two 初始化,所在的语句既是声明又是定义。作为静态数据成员,其只许枚举值和整型常量在类定义内部直接定义,而任意其他类型仅能声明,且必须在类定义外部给出定义。
    2. constexpr 关键字的主要功能是充当函数限定符。假如某函数的参数和返回值都满足一定要求,且函数体足够简单,那它就可以声明为 constexpr 函数,进而在常量表达式中使用。
      constexpr int square(int x)
      {
          return x*x;
      }
      int array[square(5)];
      
      int dummy = 4;
      int array[square(dummy)];   // 错误,dummy 不是常量表达式
      
  7. constexpr 关键字和用户自定义型别

    1. 若某个类要被划分为字面值型别,则下面条件必须全部成立:
      • 它必须具有平实拷贝构造函数。
      • 它必须具有平实析构函数。
      • 它的非静态数据成员和基类都属于平实型别。
      • 它必须具备平实默认构造函数或常量表达式构造函数(若具备后者,则不得进行拷贝/移动构造)。
    2. 字面值类型是 C++11 引入的新概念,是某些型别的集合,请注意与字面值区分,其是在代码中明确写出的值。
    3. 在 C++ 11 环境中,constexpr 函数的用途仅限于此,即 constexpr 函数只能调用其他 constexpr 函数。C++ 14 则放宽了限制,只要不在 constexpr 函数内部改动非局部变量,我们就几乎可以进行任意操作。
      class CX
      {
      private:
          int a;
          int b;
      public:
          CX() = default;
          constexpr CX(int a_, int b_)
              : a(a_), b(b_)
          {}
          constexpr int get_a() const
          {
              return a;
          }
          constexpr int get_b()
          {
              return b;
          }
          constexpr int foo()
          {
              return a+b;
          }
      }
      
      • 根据 C++11 标准,get_a() 上的 const 现在成了多余的修饰,因其限定作用已经为 constexpr 关键字所蕴含。
    4. 如果只有通过复杂的方法,才可求得某些数组界限或整型常量,那么凭借 constexpr 函数完成任务将省去大量运算。
    5. 一旦涉及用户自定义型别,常量表达式和 constexpr 函数带来的主要好处是:若依照常量表达式初始化字面值型别的对象,就会发生静态初始化,从而避免初始化的条件竞争和次序问题。构造函数同样遵守这条规则。假定构造函数声明成了 constexpr 函数,且它的参数都是常量表达式,那么所属的类就会进行常量初始化,该初始化行为会在程序静态化阶段发生。
    6. 在实践中,常量往往在编译期就完成计算,在运行期直接套用算好的值。
    7. 让用户自定义的构造函数担负起静态初始化工作,而在运行任何其他代码之前,静态初始化肯定已经完成,我们遂能避免任何牵涉初始化的条件竞争。
    8. 若 std::mutex 类的构造函数受条件竞争所累,其全局实例就无法发挥功效,因此我们将它的默认构造函数声明成 constexpr 函数,以确保其初始化总是在静态初始化阶段内完成。
  8. constexpr 对象

    1. constexpr 限定符会查验对象的初始化行为,核实其所依照的初值是常量表达式、constexpr 构造函数,或由常量表达式构成的聚合体初始化表达式。它还将对象声明为 const 常量。
      constexpr int i = 45;               // 正确
      constexpr std::string s("hello");   // 错误,std::string 不是字面值型别
      int foo(); 
      constexpr int j = foo();            // 错误,foo() 并未声明为 constexpr 函数
      
  9. constexpr 函数要符合的条件

    1. C++11 标准对 constexpr 函数的要求如下:
      • 所有参数都必须是字面值型别。
      • 返回值必须是字面值型别。
      • 整个函数体只有一条 return 语句。
      • return 语句返回的表达式必须是常量表达式。
      • 若 return 返回的表达式需要转换为某目标型别的值,涉及的构造函数或转换操作符必须是 constexpr 函数。
    2. C++14 标准大幅度放宽了要求,即 constexpr 函数仍是纯函数,不产生副作用,但其函数体能够包含的内容显著增加。
      • 准许存在多条 return 语句。
      • 函数中创建的对象可被修改。
      • 可以使用循环、条件分支和 switch 语句。
    3. 类所具有的 constexpr 成员函数则需符合更多要求。
      • constexpr 成员函数不能是虚函数。
      • constexpr 成员函数所属的类必须是字面值型别。
    4. constexpr 构造函数需遵守不同的规则:
      • 在 C++ 11 环境下,构造函数的函数体必须为空。而根据 C++ 14 和后来的标准,它必须满足其他要求才可以成为 constexpr 标准。
      • 必须初始化每一个基类。
      • 必须初始化全体非静态数据成员。
      • 在成员初始化列表中,每个表达式都必须是常量表达式。
      • 若数据成员和基类分别调用自身的构造函数进行初始化,则它们所选取执行的必须是 constexpr 构造函数。
      • 假设在构造数据成员和基类时,所依照的初始化表达式为进行类型转换而调用了相关的构造函数或转换操作符,那么执行的必须是 constexpr 函数。
    5. 平实拷贝构造函数是隐式的 constexpr 函数。
  10. constexpr 与模板

    1. 如果函数模板与类模板的成员函数加上 constexpr 修饰,而在模板的某个特定的具现化中,其参数和返回值却不属于字面值型别,则 constexpr 关键字会被忽略。该特性让我们可以写出一种函数模板,若选取了恰当的模板参数型别,它就具现化为 constexpr 函数,否则就具现化为普通的 inline 函数。
    2. 具现化的函数模板必须满足前文的全部要求,才可以成为 constexpr 函数。即便是函数模板,一旦它含有多条语句,我们就不能用关键字 constexpr 修饰其声明;这仍将导致编译错误。(此处特指 C++ 11 情形。在 C++ 14 中,constexpr() 函数模板可以合法含有多条语句,前提是符合前文所列要求)
  11. lambda 函数

    1. 如果 lambda 函数的函数体仅有一条返回语句,那么 lambda 函数的返回值型别就是表达式的型别。
    2. 假若 lambda 函数的函数体无法仅用一条 return 语句写成,这时就需要明确设定返回值型别。设定返回值型别的方法是在 lambda 函数的参数列表后附上箭头和目标型别。如果 lambda 函数不接收任何参数,而返回值型别却需显式设定,我们依然必须使之包含空参数列表。
      cond.wait(lk, []()->bool { return data_ready; });
      
    3. lambda 函数的真正厉害之处在于捕获本地变量。
    4. 要捕获本地作用域内的全体变量,最简单的方式是改用 lambda 引导符“[=]”。改用该引导符的 lambda 函数从创建开始,即可访问本地变量的副本。
      std::funtion<int(int)> make_offseter(int offset)
      {
          return [=](int j) { return offset+j; };
      }
      
    5. 还可以采用别的手段:按引用的形式捕获全部本地变量。照此处理,一旦 lambda 函数脱离生成函数或所属代码块的作用域,引用的变量即被销毁,若仍然调用 lambda 函数,就会导致未定义行为。
    6. 还有另一种做法:我们可将按引用捕获设定成默认行为,但以复制方式捕获某些特定变量。这种处理方式使用形如“[&]”的 lambda 引导符,并在“&”后面逐一列出需要复制的变量。
    7. 若要我们仅仅想要某几个具体变量,并按引用方式捕获,而非复制,就应该略去上述最开始的等号或“&”,且逐一列出目标变量,再为它们加上“&”前缀。
    8. 当 lambda 函数位于一个类的某成员函数内部时,我们在 lambda 函数中访问类成员时要务必注意。类的数据成员无法直接获取,若想从 lambda 函数内部访问类的数据成员,则须在捕获列表中加上 this 指针以捕获之。
    9. 从 C++ 14 开始,lambda 函数也能有泛型形式,其中的参数型别被声明成 auto,而非具体型别。这么一来,lambda 函数的调用操作符就是隐式模板,参数型别根据运行时外部提供的参数推导得出:
      auto f = [](auto x) { std::cout << "x=" << x << std::endl; };
      f(42);
      f("hello");
      
    10. C++ 14 还加入了广义捕获的概念,我们因此能够捕获表达式的运算结果,而不再限于直接复制或引用本地变量。该特性最常用于以移动方式捕获只移型别,从而避免以引用方式捕获。
      std::future<int> spawn_async_task()
      {
          std::promise<int> p;
          auto f = p.get_future();
          std::thread t([p = std::move(p)() { p.set_value(find_the_answer()); }]);
          t.detach();
          return f;
      }
      
      • 这里的 p = std::move§ 就是广义捕获行为,它将 promise 实例移入 lambda 函数,因此线程可以安全地分离,我们不必担心本地变量被销毁而形成悬空引用。Lambda 函数完成构建后,原来的实例 p 即进入“移出状态”,因此我们事先从它取得了关联的 future 实例。
  12. 变参模板

    1. 变参模板即参数数目可变的模板。
    2. 我们声明变参函数时,需令函数参数列表包含一个省略号(…)。变参模板与之相同,在其声明中,模板参数列表也需带有省略号:
      template<typename ...ParameterPack>
      class my_template
      {};
      
    3. 变参模板的另外两个特性。第一特性相对简单:普通模板参数(ReturnType)和可变参数(Args)能在同一声明内共存。所示的第二特性是,在 packaged_task 的特化版本中,其模板参数列表使用了组合标记“Args…”,当模板具现化时,Args 所含的各种型别均据此列出。这是个偏特化版本,因而它会进行模式匹配:在模板实例化的上下文中,出现的型别被全体捕获并打包成 Args。该可变参数 Args 叫作参数包,应用“Args…”还原参数列表则称为包展开。
      template<typename ReturnType, typename ...Args>
      class packaged_task<ReturnType(Args...)>;
      
    4. 我们可以依照某种模式创建元组,使得其中的成员型别都是普通指针,甚至都是 std::unique_ptr<> 指针,其目标型别对应参数包中的元素。
      template<typename ...Params>    // ①
      struct dummy3
      {
          std::tuple<Params* ...> pointers;   // ②
          std::tuple<std::unique_ptr<Params> ...> unique_pointers;    // ③
      }
      
      • ①处省略号是变参模板声明的语法成分,表示型别参数的数目可变,②③两处的省略号标示出展开模式。②处的模式是型别表达式 Params*,而③处的模式则是型别表达式 std::unique_ptr。
    5. 我们也可以用某种展开模式来声明函数参数,与前文按模式展开参数包的做法相似。例如,std::thread 类的构造函数正是采取了这种方法,按右值引用的形式接收全部函数参数:
      template<typename CallableType, typename ...Args>
      thread::thread(CallableType&& func, Args&& ...args);
      
    6. 借 std::forward<> 灵活保有函数参数的右值属性。
      template<typename ...ArgsTypes>
      void bar(ArgsTypes&& ...args)
      {
          foo(std::forward<ArgsTypes>(args)...);
      }
      
      • 利用 std::forward<> 完美转发:若是左值,传递之后仍是左值;若是右值,传递之后仍是右值。否则,一个右值引用参数作为函数的形参,在函数内部再转发该参数的时候它已经变成一个左值。
    7. 我们通过 sizeof… 运算符确定参数包大小,写法十分简单:sizeof…§ 即为参数包 p 所含元素的数目。sizeof… 运算符求得的值是常量表达式,这与普通的 sizeof 运算符一样,故其结果可用于设定数组长度,以及其他合适的场景中。
  13. 自动推导变量的型别

    1. 若变量在声明时即进行初始化,所依照的初值与自身型别相同,我们就能以 auto 关键字设定其类型。
  14. 线程局部变量

    1. 在程序中,若将变量声明为线程局部变量,则每个线程上都会存在其独立实例。在声明变量时,只要加入关键字 thread_local 标记,它即成为线程局部变量。
    2. 有 3 种数据能声明为线程局部变量:以名字空间为作用域的变量、类的静态数据成员和普通的局部变量。换言之,它们具有线程存储生存期。
      thread_local int x;     // 线程局部变量,它以名字空间为作用域
      class X
      {
          static thread_local std::string s;  // 类的静态数据成员,该语句用于声明
      }
      static thread_local std::string X::s;   // 该语句用于定义,类的静态数据成员应在外部另行定义
      void foo()
      {
          thread_local std::vector<int> v;    // 普通的局部变量
      }
      
    3. 实际上,在给定的翻译单元中,若所有线程局部变量从未被使用,就无法保证会把它们构造出来。这使得含有线程局部变量的模板得以动态加载,当给定线程初次指涉模板中的线程局部变量时,才进行动态加载,进而构造变量。
    4. 翻译单元,是有关 C++ 代码编译的术语,指当前代码所在的源文件,以及经过预处理后,全部有效包含的头文件和其他源文件。
    5. 对于函数内部声明的线程局部变量,在某个给定的线程上,当控制流程第一次经过其声明语句时,该变量就会初始化。假设某函数在给定的线程上从来都没有被调用,函数中却声明了线程局部变量,那么在该线程上它们均不会发生构造。这一行为规则与静态局部变量相同,但它对每个线程都单独起作用。
    6. 线程局部变量的其他性质与静态变量一致,它们先进行零值初始化,再进行其他变量初始化(如动态初始化)。如果线程局部变量的构造函数抛出异常,程序就会调用 std::terminate() 而完全终止。
    7. 动态初始化,指除非静态初始化(指零值初始化和常量初始化)以外的一切初始化行为。
    8. 给定一个线程,在其线程函数返回之际,该线程上构造的线程局部变量全都会发生析构,它们调用析构函数的次序与调用构造函数的次序相反。由于这些变量的初始化次序并不明确,因此必须保证它们的析构函数间没有相互依赖。若线程局部变量的析构函数因抛出异常而退出,程序则会调用 std::terminate(),与构造函数的情形一样。
    9. 如果线程通过调用 exit() 退出,或从 main() 自然退出(这等价于先取得 main() 的返回值,再以该值调用 std::exit()),那么线程局部变量也会被销毁。当应用程序退出时,如果有其他线程还在运行,则那些线程上的线程局部变量不会发生析构。
    10. 线程局部变量的地址因不同线程而异,但我们依然可以令一个普通指针指向该变量。假定该指针的值源于某线程所执行的取址操作,那么它指涉的目标对象就位于该线程上,其他线程也能通过这一指针访问那个对象。若线程在对象销毁后还试图访问它,将导致未定义行为(向来如此)。所以,若我们向另外一个线程传递指针,其目标是线程局部变量,那就需要确保在目标变量所属的线程结束后,该指针不会再被提取。
  15. 类模板的参数推导

    1. C++ 17 拓展了模板参数的自动推导型别的思想:如果我们通过一个模板声明一个对象,那么在大多数情况下,根据该对象的初始化表达式,能推导出模板参数的型别。
    2. 具体来说,若仅凭某个类模板的名字声明对象,却未设定模板参数列表,编译器就会根据对象的初始化表达式,指定调用类模板的某个构造函数,还借以推导模板参数,而函数模板也将发生普通的型别推导,这两个推导机制遵守相同的规则。
      std::mutex m;
      std::lock_guard guard(m);   // 将推导出 std::lock_guard<std::mutex>
      
      std::mutex m1;
      std::shard_mutex m2;
      std::scoped_lock guard(m1, m2); // 将推导出 std::scoped_lock<std::mutex, std::shard_mutex>
      
  16. C++11 增加的新特性包括静态断言(static assertion/static_assert)、强类型枚举(strongly typed enumeration/enum class)、委托构造(delegating constructor)函数、Unicode 编码支持、模板别名(template alias)和新式的统一初始化列表(uniform initialization sequence),以及许多相对细小的改变。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值