简介

  • 在C++11中添加了原始字符串的字面量,定义方式为:R"xxx(原始字符串)xxx"。其中,()两边的字符串可以省略。
  • 原始字面量R可以直接表示字符串的实际含义,而不需要额外对字符串做转义或连接等操作
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      #include "test_raw_string.hpp"
    
      int test_raw_str()
      {
          std::cout << R"path(D:\user\desktop\aaa)path" << std::endl;
    
          std::cout << R"multi_lines(
              This
              is
              my
              world
              !!!
          )multi_lines" << std::endl;
    
          return 0;
      }
    

指针空值类型 nullptr

  • 在C++程序开发中,为了提高程序的健壮性,一般会在定义指针的同时完成初始化操作,或者在指针的指向尚未明确的情况下,都会给指针初始化为NULL,避免产生野指针(没有明确指向的指针,操作野指针可能导致程序发生异常)。
  • C++98和C++03标准中,将一个指针初始化为空指针的方式有两种:
    • char *ptr = 0;
    • char *ptr = NULL;
  • nullptr 无法隐式转换为整型,但是可以隐式匹配指针类型。在C++11标准下,相比NULL和0,使用nullptr初始化空指针可以令我们编写的程序更加健壮

constexpr

  • 在C++11之前只有const关键字,从功能上来说这个关键字有双重语义:变量只读,修饰常量。

  • constexpr,是用来修饰常量表达式或者常量函数
    • 常量函数:返回值是一个常量
    • 常量表达式:多个常量做运算,最终结果还是一个常量
  • C++程序从编写完毕到执行分为四个阶段:预处理,编译,汇编和链接四个阶段,之后得到可执行程序之后就可以运行了。
  • 需要额外强调的是,常量表达式和非常量表达式的计算时机不同
    • 非常量表达式只能在程序运行阶段计算出结果
    • 但是常量表达式的计算往往发生在程序的编译阶段
  • 这可极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间

  • 那么,编译器如何识别表达式是不是常量表达式呢?
    • 在C++11中添加了constexpr关键字之后,就可以在程序中使用它来修改常量表达式,用来提高程序的执行效率。
  • 在使用中建议,将constconstexpr的功能区分开
    • 凡是表达只读语义的场景都使用const
    • 凡是表达常量语义的场景都使用constexpr
  • 在定义常量时,constconstexpr是等价的,都可以在程序的编译阶段计算出结果
  • 对于C++内置类型的数据,可以直接用constexpr修饰,但如果是自定义的数据类型(用struct或者class实现),直接用constexpr修饰是不行的

  • 为了提高C++程序的执行效率,我们可以将程序中,值不需要发生变法的变量定义为常量,也可以使用constexpr修饰函数的返回值,这种函数被称作常量表达式函数,这些函数主要包括:普通函数/类成员函数,类的构造函数,模板函数

  • 修饰函数:
  • constexpr,并不能修改任意函数的返回值,使这些函数称为常量表达式函数,必须要满足以下几个条件
    • 函数必须要有返回值,并且return返回的表达式必须是常量表达式
    • 函数在使用之前,必须有对应的定义语句
    • 整个函数的函数体中,不能出现非常量表达式之外的语句(using指令,typedef语句以及static_assert断言,return语句除外)
  • 以上三条规则不仅对应普通函数适用,对应类的成员函数也是适用的
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      constexpr int test_constexpr();
    
      constexpr int test_constexpr()
      {
          using mytype = int;
          constexpr mytype a = 100;
          constexpr mytype b = 10;
          constexpr mytype c = a * b;
    
          return c - (a + b);
      }
    

  • 修饰模板函数
  • C++11语法中,constexpr可以修饰模板函数,但是由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。
  • 如果constexpr修饰的模板函数实例化结果不满足常量表达式的要求,则constexpr会被自动忽略,即该函数就等同于一个普通函数

auto

  • 可以适用auto自动推导变量的类型,还能够结合decltype来表示函数的返回值

1.1 简介

  • 在C++11之前auto和static是对应的,表示自动变量是自动存储的,但是非static的局部变量默认都是自动存储的,因此这个关键字变得非常鸡肋
  • 在C++11中他们赋予了新的含义,使用这个关键字能够像别的语言一样自动推导出变量的实际类型

  • 推导规则:
    • C++11中auto并不代表一种实际的数据类型,只是一个类型声明的 占位符,auto 并不是万能的,在任意场景下都能够推导出变量的实际类型。
    • 使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型
    • 使用语法:
      • auto variable_name = variable_value;
    • auto还可以和指针,引用结合起来使用,也可以带上const, volatile限定符,在不同的场景下有对应的推导规则。
    • 推导规则内容如下:
      • 当变量不是指针或者引用类型时,推导的结果不会保留const,volatile关键字
      • 当变量是指针或者引用类型时,推导的结果会保留const,volatile关键字

1.2 auto的限制

  • auto关键字并不是万能的,在以下这些场景中是不能完成类型推导的:
    • 不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾
    • 不能用于类的非静态成员变量的初始化。因为非静态成员变量是属于类的对象的,而不是属于类的。
    • 不能使用auto关键字定义数组。定义一个数组,必须给定数组中元素的数据类型
    • 无法使用auto推导出模板参数

1.3 auto的应用

  • 下面列举几个比较常用的场景:
    • 用于STL的容器变量。
    • 用于泛型编程

decltype

  • 在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用C++11提供的decltype关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型
  • 语法格式如下:
    • decltype (表达式)
  • decltype, 是declare type的缩写,意思是 声明类型。
  • decltype的推导是在编译器完成的,它只是用于表达式类型的推导,并不会计算表达式的值

返回类型后置

  • 在泛型编程中,可能需要通过参数的运算来得到返回值的类型
  • 在C++11中增加了返回类型后置语法,说明白一点就是将decltype和auto结合起来完成返回类型的推导,其语法格式如下:
    • // 符号 -> 后边跟随的是函数返回值的类型
    • auto func(参数1, 参数2,...) -> decltype (参数表达式)
  • 通过对上述返回类型后置语法代码的分析,得到结论:auto 会追踪 decltype() 推导出的类型

final

  • C-C++03.md, final 部分

override

  • C-C++03.md,override

模板的右尖括号

  • 在范型编程中,模板实例化有一个非常繁琐的地方,那就是连续的两个右尖括号(»)会被编译器解析成右移操作符,而不是模板参数表的结束。
  • C++11改进了编译器的解析规则,尽可能地将多个右尖括号(>)解析成模板参数结束符

默认模板参数

  • 在C++11中添加了对函数模板参数的支持
  • 当默认模板参数和模板参数自动推导同时使用时(优先级从高到低)
    • 如果可以推导出参数类型则使用推导出的类型
    • 如果函数模板无法推导出参数类型,那么编译器会使用默认模板参数
    • 如果无法推导出模板参数类型并且没有设置默认模板参数,编译器就会报错

using

  • 在C++中using用于声明命名空间,使用命名空间也可以防止命名冲突,在程序中声明了命名空间之后,就可以直接使用命名空间中的定义的类了。在C++11中赋予了using新的功能,让C++变得更年轻,更灵活

1.1 定义别名

  • 在C++中可以通过typedef重新定义一个类型,语法格式如下:
    1
    2
    3
    
      // typedef 旧的类型名 新的类型名;
      // 使用举例
      typedef unsigned int uint_t;
    
  • 被重定义的类型并不是一个新的类型,仅仅只是原有的类型取了一个新的名字。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。
  • C++11中规定了一种新的方法,使用别名(alias declaration)来定义类型的别名,即使用using
  • 在使用的时候,关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。
  • 类型别名和类型的名字等价,只要是类型的名字能够出现的地方,就能使用类型别名。使用typedef定义的别名和使用using定义的别名在语义上是等效的。
  • 使用using定义别名的语法格式是这样的:
    1
    2
    3
    
      // using 新的类型 = 旧的类型;
      // 使用举例
      using uint_t = unsigned int;
    
  • 通过using和typedef的语法格式可以看到二者的使用没有太大的区别,假设我们定义一个函数指针,using的优势就能凸显出来了
  • 示例:
    1
    2
    3
    4
    5
    
      // 使用typedef定义函数指针
      typedef int(*func_ptr)(int, double);
    
      // 使用using定义函数指针
      using func_ptr1 = int(*)(int, double);
    
  • 如果不是特别熟悉函数指针与typedef,第一眼很难看出func_ptr其实是一个别名,其本质是一个函数指针,指向的函数返回类型是int,函数的参数有两个分别是int, double类型
  • 使用using定义函数指针别名的写法看起来就非常直观了,把别名的名字强制分离到了左边,而把别名对应的实际类型放在了右边,比较清晰,可读性比较好

1.2 模板的别名

  • 使用 typedef 重定义类似很方便,但是它有一点限制,比如无法重定义一个模板,比如我们需要一个固定以 int 类型为 key 的 map,它可以和很多类型的 value 值进行映射,如果使用 typedef 这样直接定义就非常麻烦:
    1
    2
    3
    
      typedef map<int, string> m1;
      typedef map<int, int> m2;
      typedef map<int, double> m3;
    
  • 在 C++11 中,新增了一个特性就是可以通过使用 using 来为一个模板定义别名,对于上面的需求可以写成这样:
    1
    2
    
      template <typename T>
      using mymap = map<int, T>;
    
  • 上面的例子中通过使用 using 给模板指定别名,就可以基于别名非常方便的给 value 指定相应的类型,这样使编写的程序变得更加灵活,看起来也更加简洁一些。

  • 最后在强调一点:using 语法和 typedef 一样,并不会创建出新的类型,它们只是给某些类型定义了新的别名。using 相较于 typedef 的优势在于定义函数指针别名时看起来更加直观,并且可以给模板定义别名

委托构造函数和继承构造函数

1.1 委托构造函数

  • 委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化

1.2 继承构造函数

  • C++11 中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写

列表初始化

基于范围的for循环

1.1 for循环新语法

  • 我们在遍历的过程中需要给出容器的两端:开头(begin)和结尾(end),因为这种遍历方式不是基于范围来设计的。
  • 在基于范围的for循环中,不需要再传递容器的两端,循环会自动以容器为范围展开,并且循环中也屏蔽掉了迭代器的遍历细节,直接抽取容器中的元素进行运算,使用这种方式进行循环遍历会让编码和维护变得更加简便

  • C++98/03中普通的for循环,语法格式:
    1
    2
    3
    4
    
      for(表达式 1; 表达式 2; 表达式 3)
      {
          // 循环体
      }
    
  • C++11 基于范围的 for 循环,语法格式:
    1
    2
    3
    4
    
      for (declaration : expression)
      {
          // 循环体
      }
    
  • 在上面的语法格式中
    • declaration 表示遍历声明,在遍历过程中,当前被遍历到的元素会被存储到声明的变量中。
    • expression 是要遍历的对象,它可以是表达式、容器、数组、初始化列表等
  • 使用基于范围的 for 循环遍历容器,示例代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      #include <iostream>
      #include <vector>
      using namespace std;
    
      int main(void)
      {
          vector<int> t{ 1,2,3,4,5,6 };
          for (auto value : t)
          {
              cout << value << " ";
          }
          cout << endl;
    
          return 0;
      }
    
  • 在上面的例子中,是将容器中遍历的当前元素拷贝到了声明的变量 value 中,因此无法对容器中的元素进行写操作,如果需要在遍历过程中修改元素的值,需要使用引用
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
      #include <iostream>
      #include <vector>
      using namespace std;
    
      int main(void)
      {
          vector<int> t{ 1,2,3,4,5,6 };
          cout << "遍历修改之前的容器: ";
          for (auto &value : t)
          {
              cout << value++ << " ";
          }
          cout << endl << "遍历修改之后的容器: ";
    
          for (auto &value : t)
          {
              cout << value << " ";
          }
          cout << endl;
    
          return 0;
      }
    
  • 对容器的遍历过程中,如果只是读数据,不允许修改元素的值,可以使用 const 定义保存元素数据的变量,在定义的时候建议使用 const auto &,这样相对于 const auto 效率要更高一些

1.2 使用细节

  • 使用基于范围的 for 循环有一些需要注意的细节,先来看一下对关系型容器 map 的遍历:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
      #include <iostream>
      #include <string>
      #include <map>
      using namespace std;
    
      int main(void)
      {
          map<int, string> m{
              {1, "lucy"},{2, "lily"},{3, "tom"}
          };
    
          // 基于范围的for循环方式
          for (auto& it : m)
          {
              cout << "id: " << it.first << ", name: " << it.second << endl;
          }
    
          // 普通的for循环方式
          for (auto it = m.begin(); it != m.end(); ++it)
          {
              cout << "id: " << it->first << ", name: " << it->second << endl;
          }
    
          return 0;
      }
    
  • 在上面的例子中使用两种方式对 map 进行了遍历,通过对比有两点需要注意的事项:
    • 使用普通的 for 循环方式(基于迭代器)遍历关联性容器, auto 自动推导出的是一个迭代器类型,需要使用迭代器的方式取出元素中的键值对(和指针的操作方法相同):
      • it->first
      • it->second
    • 使用基于访问的 for 循环遍历关联性容器,auto 自动推导出的类型是容器中的 value_type,相当于一个对组(std::pair)对象,提取键值对的方式如下:
      • it.first
      • it.second
  • 元素只读
    • 通过对基于范围的 for 循环语法的介绍可以得知,在 for 循环内部声明一个变量的引用就可以修改遍历的表达式中的元素的值,但是这并不适用于所有的情况,对应 set 容器来说,内部元素都是只读的,这是由容器的特性决定的,因此在 for 循环中 auto & 会被视为 const auto & 
    • 除此之外,在遍历关联型容器时也会出现同样的问题,基于范围的for循环中,虽然可以得到一个std::pair引用,但是我们是不能修改里边的first值的,也就是key值。
  • 访问次数:
    • 基于范围的 for 循环遍历的对象可以是一个表达式或者容器 / 数组等。假设我们对一个容器进行遍历,在遍历过程中 for 循环对这个容器的访问频率是一次还是多次呢?
    • 从上面的结果中可以看到,不论基于范围的 for 循环迭代了多少次,函数 getRange () 只在第一次迭代之前被调用,得到这个容器对象之后就不会再去重新获取这个对象了
    • 对应基于范围的 for 循环来说,冒号后边的表达式只会被执行一次。在得到遍历对象之后会先确定好迭代的范围,基于这个范围直接进行遍历。如果是普通的 for 循环,在每次迭代的时候都需要判断是否已经到了结束边界

可调用对象包装器,绑定器

1.1 可调用对象

共享智能指针

  • 在 C++ 中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。
  • 智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

  • C++11 中提供了三种智能指针,使用这些智能指针时需要引用头文件 <memory>
    • std::shared_ptr:共享的智能指针
    • std::unique_ptr:独占的智能指针
    • std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视 shared_ptr 的

1.2 shared_ptr 的初始化

  • 共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针 shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared 辅助函数以及 reset 方法。
  • 共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数 use_count,函数原型如下
    1
    2
    
      // 管理当前对象的 shared_ptr 实例数量,或若无被管理对象则为 0。
      long use_count() const noexcept;
    
  • 通过构造函数初始化:
    1
    2
    
      // shared_ptr<T> 类模板中,提供了多种实用的构造函数, 语法格式如下:
      std::shared_ptr<T> 智能指针名字(创建堆内存);
    
  • 如果智能指针被初始化了一块有效内存,那么这块内存的引用计数 + 1,如果智能指针没有被初始化或者被初始化为 nullptr 空指针,引用计数不会 + 1。另外,不要使用一个原始指针初始化多个 shared_ptr

  • 通过拷贝和移动构造函数初始化
    • 当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了
    • 如果使用拷贝的方式初始化共享智能指针对象,这两个对象会同时管理同一块堆内存,堆内存对应的引用计数也会增加;如果使用移动的方式初始智能指针对象,只是转让了内存的所有权,管理内存的对象并不会增加,因此内存的引用计数不会变化。
  • 通过 std::make_shared 初始化
    • 通过C++提供的std::make_shared()就可以完成内存对象的创建并将其初始化给智能指针,函数原型如下:
      1
      2
      
      template< class T, class... Args >
      shared_ptr<T> make_shared( Args&&... args );
      
    • 参数:
      • T:模板参数的数据类型
      • Args&&... args : 要初始化的数据,如果是通过make_shared创建对象,需要按照构造函数的参数列表指定
  • 使用std::make_shared()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可以完成地址的初始化;如果要创建一个类对象,函数的()内存需要指定构造对象需要的参数,也就是类构造函数的参数

  • 通过 reset 方法初始化
    • 共享智能指针类提供的 std::shared_ptr::reset 方法函数原型如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
        void reset() noexcept;
      
        template< class Y >
        void reset( Y* ptr );
      
        template< class Y, class Deleter >
        void reset( Y* ptr, Deleter d );
      
        template< class Y, class Deleter, class Alloc >
        void reset( Y* ptr, Deleter d, Alloc alloc );
      
    • ptr:指向要取得所有权的对象的指针
    • d:指向要取得所有权的对象的指针
    • aloc:内部存储所用的分配器
    • 对于一个未初始化的共享智能指针,可以通过 reset 方法来初始化,当智能指针中有值的时候,调用 reset 会使引用计数减 1
  • 获取原始指针:
    • 对应基础数据类型来说,通过操作智能指针和操作智能指针管理的内存效果是一样的,可以直接完成数据的读写。但是如果共享智能指针管理的是一个对象,那么就需要取出原始内存的地址再操作,可以调用共享智能指针类提供的 get () 方法得到原始地址,其函数原型如下:
      • T* get() const noexcept;
  • 指定删除器
    • 当智能指针管理的内存对应的引用计数变为 0 的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的

原子类型 std::atomic

  • C++11 提供了一个原子类型 std::atomic<T>,通过这个原子类型管理的内部变量就可以称之为原子变量,我们可以给原子类型指定 bool、char、int、long、指针等类型作为模板参数(不支持浮点类型和复合类型)
  • 原子指的是一系列不可被 CPU 上下文交换的机器指令,这些指令组合在一起就形成了原子操作。在多核 CPU 下,当某个 CPU 核心开始运行原子操作时,会先暂停其它 CPU 内核对内存的操作,以保证原子操作不会被其它 CPU 内核所干扰。

  • 由于原子操作是通过指令提供的支持,因此它的性能相比锁和消息传递会好很多。相比较于锁而言,原子类型不需要开发者处理加锁和释放锁的问题,同时支持修改,读取等操作,还具备较高的并发性能,几乎所有的语言都支持原子类型
  • 可以看出原子类型是无锁类型,但是无锁不代表无需等待,因为原子类型内部使用了 CAS 循环,当大量的冲突发生时,该等待还是得等待!但是总归比锁要好

  • C++11 内置了整形的原子变量,这样就可以更方便的使用原子变量了。在多线程操作中,使用原子变量之后就不需要再使用互斥量来保护该变量了,用起来更简洁。因为对原子变量进行的操作只能是一个原子操作(atomic operation),原子操作指的是不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何的上下文切换。多线程同时访问共享资源造成数据混乱的原因就是因为 CPU 的上下文切换导致的,使用原子变量解决了这个问题,因此互斥锁的使用也就不再需要了

  • CAS 全称是 Compare and swap, 它通过一条指令读取指定的内存地址,然后判断其中的值是否等于给定的前置值,如果相等,则将其修改为新的值

1.1 atomic类成员

  • 类定义:
    1
    2
    3
    
      // 定义于头文件 <atomic>
      template< class T >
      struct atomic;
    
  • 通过定义可得知:在使用这个模板类的时候,一定要指定模板类型

  • 构造函数:
    1
    2
    3
    4
    5
    6
    
      // ①
      atomic() noexcept = default;
      // ②
      constexpr atomic( T desired ) noexcept;
      // ③
      atomic( const atomic& ) = delete;
    
  • 构造函数①:默认无参构造函数
  • 构造函数②:使用 desired 初始化原子变量的值
  • 构造函数③:使用 =delete 显示删除拷贝构造函数,不允许进行对象之间的拷贝

  • 公共成员函数:
    • 原子类型在类内部重载了 = 操作符,并且不允许在类的外部使用 = 进行对象的拷贝
      1
      2
      3
      4
      5
      
        T operator=( T desired ) noexcept;
        T operator=( T desired ) volatile noexcept;
      
        atomic& operator=( const atomic& ) = delete;
        atomic& operator=( const atomic& ) volatile = delete;
      
    • 原子地以 desired 替换当前值。按照 order 的值影响内存
      1
      2
      
        void store( T desired, std::memory_order order = std::memory_order_seq_cst ) noexcept;
        void store( T desired, std::memory_order order = std::memory_order_seq_cst ) volatile noexcept;
      
    • desired:存储到原子变量中的值
    • order:强制的内存顺序
    • 原子地加载并返回原子变量的当前值。按照order的值影响内存。直接访问原子对象也可以得到原子变量的当前值
      1
      2
      
        T load( std::memory_order order = std::memory_order_seq_cst ) const noexcept;
        T load( std::memory_order order = std::memory_order_seq_cst ) const volatile noexcept;
      
  • 特化成员函数
    • 复合赋值运算符重载
  • 内存顺序约束:
    • 通过上面的 API 函数我们可以看出,在调用 atomic 类提供的 API 函数的时候,需要指定原子顺序,在 C++11 给我们提供的 API 中使用枚举用作执行原子操作的函数的实参,以指定如何同步不同线程上的其他操作
    • 定义如下:
      1
      2
      3
      4
      5
      6
      7
      8
      
        typedef enum memory_order {
            memory_order_relaxed,   // relaxed
            memory_order_consume,   // consume
            memory_order_acquire,   // acquire
            memory_order_release,   // release
            memory_order_acq_rel,   // acquire/release
            memory_order_seq_cst    // sequentially consistent
        } memory_order;
      
    • memory_order_relaxed, 这是最宽松的规则,它对编译器和 CPU 不做任何限制,可以乱序
    • memory_order_release 释放,设定内存屏障 (Memory barrier),保证它之前的操作永远在它之前,但是它后面的操作可能被重排到它前面
    • memory_order_acquire 获取, 设定内存屏障,保证在它之后的访问永远在它之后,但是它之前的操作却有可能被重排到它后面,往往和 Release 在不同线程中联合使用
    • memory_order_consume:改进版的 memory_order_acquire ,开销更小
    • memory_order_acq_rel,它是 Acquire 和 Release 的结合,同时拥有它们俩提供的保证。比如你要对一个 atomic 自增 1,同时希望该操作之前和之后的读取或写入操作不会被重新排序
    • memory_order_seq_cst 顺序一致性, memory_order_seq_cst 就像是 memory_order_acq_rel 的加强版,它不管原子操作是属于读取还是写入的操作,只要某个线程有用到 memory_order_seq_cst 的原子操作,线程中该 memory_order_seq_cst 操作前的数据操作绝对不会被重新排在该 memory_order_seq_cst 操作之后,且该 memory_order_seq_cst 操作后的数据操作也绝对不会被重新排在 memory_order_seq_cst 操作前

1.2 原子变量的使用

  • 假设我们要制作一个多线程交替数数的计数器,我们使用互斥锁和原子变量的方式分别进行实现

  • 互斥锁版本:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
      #include <iostream>
      #include <thread>
      #include <mutex>
      #include <atomic>
      #include <functional>
      using namespace std;
    
      struct Counter
      {
          void increment()
          {
              for (int i = 0; i < 10; ++i)
              {
                  lock_guard<mutex> locker(m_mutex);
                  m_value++;
                  cout << "increment number: " << m_value 
                      << ", theadID: " << this_thread::get_id() << endl;
                  this_thread::sleep_for(chrono::milliseconds(100));
              }
          }
    
          void decrement()
          {
              for (int i = 0; i < 10; ++i)
              {
                  lock_guard<mutex> locker(m_mutex);
                  m_value--;
                  cout << "decrement number: " << m_value 
                      << ", theadID: " << this_thread::get_id() << endl;
                  this_thread::sleep_for(chrono::milliseconds(100));
              }
          }
    
          int m_value = 0;
          mutex m_mutex;
      };
    
      int main()
      {
          Counter c;
          auto increment = bind(&Counter::increment, &c);
          auto decrement = bind(&Counter::decrement, &c);
          thread t1(increment);
          thread t2(decrement);
    
          t1.join();
          t2.join();
    
          return 0;
      }
    
  • 原子变量版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
      #include <iostream>
      #include <thread>
      #include <atomic>
      #include <functional>
      using namespace std;
    
      struct Counter
      {
          void increment()
          {
              for (int i = 0; i < 10; ++i)
              {
                  m_value++;
                  cout << "increment number: " << m_value
                      << ", theadID: " << this_thread::get_id() << endl;
                  this_thread::sleep_for(chrono::milliseconds(500));
              }
          }
    
          void decrement()
          {
              for (int i = 0; i < 10; ++i)
              {
                  m_value--;
                  cout << "decrement number: " << m_value
                      << ", theadID: " << this_thread::get_id() << endl;
                  this_thread::sleep_for(chrono::milliseconds(500));
              }
          }
          // atomic<int> == atomic_int
          atomic_int m_value = 0;
      };
    
      int main()
      {
          Counter c;
          auto increment = bind(&Counter::increment, &c);
          auto decrement = bind(&Counter::decrement, &c);
          thread t1(increment);
          thread t2(decrement);
    
          t1.join();
          t2.join();
    
          return 0;
      }
    
  • 通过代码的对比可以看出,使用了原子变量之后,就不需要再定义互斥量了,在使用上更加简便,并且这两种方式都能保证在多线程操作过程中数据的正确性,不会出现数据的混乱。
  • 原子类型 atomic<T> 可以封装原始数据最终得到一个原子变量对象,操作原子对象能够得到和操作原始数据一样的效果,当然也可以通过 store() 和 load() 来读写原子对象内部的原始数据。

1.3 std::atomic::store

  • 功能:修改包含值,使用val替换被包含的值。该操作是原子的,并遵循由sync指定的内存顺序
  • 声明:
    • void store (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
    • void store (T val, memory_order sync = memory_order_seq_cst) noexcept;
  • 参数:
    • val – 复制到包含对象的值。Tatomic的模板参数(包含值的类型)
    • sync – 操作的同步模式。这将是枚举类型memory_order的可能值之一:
      • memory_order_relaxed – Relaxed – No synchronization of side effects
      • memory_order_release – Release – Synchronizes side effects with the next consume or acquire operation
      • memory_order_seq_cst – Sequentially consitent – Synchronizes all visible side effects with the other sequentially consistent operations,following a single total order.
  • 返回值:无

1.4 std::atomic::load

  • 功能:读取包含值。返回这个包含的值。该操作是原子的,并遵循由sync指定的内存顺序
  • 声明:
    • T load (memory_order sync = memory_order_seq_cst) const volatile noexcept;
    • T load (memory sync = memory_order_seq_cst) const noexcept;
  • 参数:
    • sync – 操作的同步模式,这将是枚举类型memory_order的可能值之一:
      • memory_order_relaxed – Relaxed – No synchronization of side effects
      • memory_order_release – Release – Synchronizes side effects with the next consume or acquire operation
      • memory_order_seq_cst – Sequentially consitent – Synchronizes all visible side effects with the other sequentially consistent operations,following a single total order.
  • 返回值:
    • 返回包含的值
    • t是包含值的类型(原子模板参数)

1.5 std::atomic_load

  • 功能:读取包含值。返回obj中包含的值。这个操作是原子的,并使用顺序一致性(memory_order_seq_cst)。访问不同的memory ordering的值,看atomic_load_explicit
  • 声明:
    • template <class T> T atomic_load (const volatile atomic<T>* obj) noexcept;
    • template <class T> T atomic_load (const atomic<T>* obj) noexcept;
    • T atomic_load (const volatile A* obj) noexcept;
    • T atomic_load (const A* obj) noexcept;
  • 参数:
    • obj – 一个指向atomic对象的指针
    • A类型代表其他过载的原子类型(如果库未实现C风格的原子类型作为原子的实例)
  • 返回值:
    • 返回包含的值
    • t是包含值的类型(原子模板参数)

1.6 C风格API

  • 针对C++的atomic提案,C也有一份对应提案,它应该提供相同语义,但是不使用诸如template, reference和member function等C++特性。整个atomic接口有一个C-style对等品,称为C standard的一份扩充。
  • 可以声明atomic_bool取代atomic,并替换store()load(),改为global函数,后者接受一个pointer指向对象
  • C另有一个接口,采用_Atomic_Atomic(),因此C-style接口一般只用于“需要在C和C++之间保持兼容”的代码身上

1.7 C风格数据类型

  • 在C++中使用C-style atomic类型并不罕见,为了兼容新的C标准(C11),C++支持定义原子整型类型。
  • 这些类型都与std::atomic<T>特化类相对应,或是用同一接口特化的一个基本类型。

  • std::atomic_itype原子类型:
    • atomic_bool
    • atomic_char
    • atomic_schar
    • atomic_uchar
    • atomic_int
    • atomic_uint
    • atomic_short
    • atomic_ushort
    • atomic_long
    • atomic_ulong
    • atomic_llong
    • atomic_ullong
    • atomic_wchar_t
    • atomic_char16_t
    • atomic_char32_t
  • std::atomic<>相关特化类:
    • atomic <bool>
    • atomic <char>
    • atomic <signed char>
    • atomic <unsigned char>
    • atomic <int>
    • atomic <unsigned int>
    • atomic <short>
    • atomic <unsigned short>
    • atomic <long>
    • atomic <unsigned long>
    • atomic <long long>
    • atomic <unsigned long long>
    • atomic <wchar_t>
    • atomic <char16_t>
    • atomic <char32_t>

1.8 小结

  • 虽然atomic原子类型使用上较为简单,但是其函数接口(原子操作)却可以有不同的内存顺序。
  • C++11从各种不同的平台上抽象出了一个软件的内存模型,并以内存顺序进行描述,以使得向进一步挖掘并行系统性能的程序员有足够简单的手段来完成以往只能通过内联汇编来完成的工作

std::round, std::roundf, std::roundl, std::lround, std::lroundf, std::lroundl, std::llround, std::llroundf

  • 声明
    1. float round (float arg);
    2. float roundf (float arg);
    3. double round (double arg);
    4. long double round (long double arg);
    5. long double roundl (long double arg);
    6. double round (IntegralType arg);
    7. long lround (float arg);
    8. long lroundf (float arg);
    9. long lround (double arg);
    10. long lround (long double arg);
    11. long lroundl (long double arg);
    12. long lround (IntegralType arg);
    13. long long llround (float arg);
    14. long long llroundf (float arg);
    15. long long llround (double arg);
    16. long long llround (long double arg);
    17. long long llroundl (long double arg);
    18. long long llround (IntegralType arg);
  • (1-3) 计算 arg 的最接近整数值(以浮点格式),中点情况舍入为远离零,无关乎当前舍入模式。
  • (5-7, 9-11) 计算 arg 的最接近整数值(以整数格式),中点情况舍入为远离零,无关乎当前舍入模式
  • (4,8,12) 接受任何整数类型参数的重载集或函数模板。分别等价于 2) 、 6) 或 10) (将参数转型为 double )

  • 参数:
    • arg – 浮点值
  • 返回值:
    • 若不出现错误,则返回 arg 的最接近整数值,中点情况为远离零者。
    • 若出现定义域错误,则返回实现定义值。