C++11?

有的招聘岗位明确要求“熟练使用C++11或更高标准”。

比较好奇为什么会专门要求C++11呢?个人理解,C++最重要的是,面向对象的思想,和利用C++特性进行设计(如设计模式中的典型应用场景和对应的设计)。

很多文章介绍了C++11相对C++98的变化,本文主要顺着官网文章的思路,汇总相关内容,并在“备注”中附加了我的个人理解。

C++语言标准

从网页Current Status : Standard C++ (isocpp.org)可以看到C++标准的状态(见下图)。

C++这门编程语言的历史可以追溯至1979年,C++之父Bjarne Stroustrup开始从事“带类的C”编程语言的开发工作。1983年,“带类的C”正式被称为“C++”,其中“++”就取自C语言中的“++”运算符,这也从侧面表明了Stroustrup对于C++这门编程语言的定位。

1998年,C++标准委员会发布了第一版C++标准,命名为C++98标准。

2011年,C++11标准诞生,用于取代C++98标准;此标准还有一个别名为“C++0x”。

2014年,C++14标准发布,对C++11标准做了优化和更新。

2017年底,C++17标准正式颁布。

从图中看到,C++20(支持Reflection和协程Coroutines)和C++23也已发布,C++26和C++29也已经规划上了。

C++标准的演进过程中,不断改正原有的bug,不断吸收其他语言先进的地方。 虚函数、函数重载、引用、const 关键字,类中受保护成员(protected)和私有成员(private),多继承等,都是早期逐步加入的。所以,我们不能用一成不变的眼光看待一门编程语言,程序员也要与时俱进。

C++11标准算是一个大的版本。其他博文提到,在C++98的基础上修正了约600个C++语言中的缺陷,同时添加了约140个新特性,这些更新使得C++语言焕然一新。

C++标准库和编译器环境:

本节大部分内容来自C++标准库简介-CSDN博客

C++语言是跨平台的语言标准,C++语言标准的不断演进,必然要求编译器也不断演进,编译器和C++标准库基于不同平台有不同的实现。C++标准库(C++ Standard Library)是随着编译器一起提供的,是类库和函数的集合。C++编译器厂商根据C++标准委员会官方的ISO规范并将其转化为代码。C++编译器厂商实现的C++标准库,必须依赖不同操作系统提供的系统调用接口,因此每个平台都有自己的C++标准库实现。

C++标准库的特点如下:

1、C++标准库,由类库和函数库组成(见下图),不是C++语言标准的一部分,是C++语言标准的实现。

2、C++标准库中定义的类和对象都位于std命名空间中。

3、C++标准库的头文件都不带.h后缀。

4、C++标准库涵盖了C库的功能。

下图是C++编译环境的组成:

1,C++标准语法模块,针对C++语言标准规定的内容,如C++11标准的内容。

2,C++扩展语法模块, 定义了本编译器在本平台上扩展支持的独有特性。

3,不同的C++编译器有自己的编译器扩展库,也是本编译器在本平台上独有的。

4,为了便于软件开发,不同的C++编译器附带提供C语言兼容库,C语言兼容库头文件带.h后缀,如#include <stdio.h>#include <math.h>等。C++标准库中包含一个涵盖C库功能的子库,通常头文件以c开头,如#include <cmath>#include <cstring>等。

5,不同的C++编译器都有C++编译器厂商实现的遵循C++标准的C++标准库,C++标准库有相同的功能接口,但内部实现不同。

很多人嫌弃C++标准库,不够丰富强大,不像其他语言有强大的包管理和众多的库供选择。其实,C++中也有广泛使用的第三方库或第三方实现,如boost库,STLport等。

C++11特性:

https://isocpp.org/wiki/faq/cpp11

https://isocpp.org/wiki/faq/

C++11 Overview:

从这里Standard C++,可以获取C++11的文档,视频等学习资料。

C++11 Language Extensions — General Features:

Auto and decltype

“declare type”,译为“声明类型”。这两个关键字的语法格式分别为:

auto varname = value; //auto语法格式,编译器根据=右边的value推导出变量类型。

decltype(exp) varname [= value]; //decltype语法格式,编译器根据exp推导出变量类型。

C++中的变量必须有明确类型,故autodecltype(exp)只是占位符,编译器会推导出变量类型。C++98中,auto关键字用来指明变量的存储类型,和static关键字相对应,auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样。

//auto 的一个典型应用场景是用来定义 stl 的迭代器。
template<class T> void printall(const vector<T>& v)
{
    for (auto p = v.begin(); p!=v.end(); ++p)
        cout << *p << "\n";
}

template<class T, class U> void multiply(const vector<T>& vt, const vector<U>& vu)
{
    // ...
    auto tmp = vt[i]*vu[i];
    // ...
}

int x = 1;
decltype(x) y = x; // y 的类型为 int
decltype(10.8) z = 5.5; //z 的类型为 double
decltype(z + 100) a;  //a 的类型为 double

备注:书写简单的场景,最好写明确的类型,不用该特性。对复杂或者不好书写的类型用auto(如迭代器相关,模板相关)。

Range-for statement

所有的标准容器,如std:string, 初始化列表,数组,所有定义了begin()end()接口的容器,都可以作为range使用。

vector<double> v;
for (auto x : v) cout << x << '\n';
for (auto& x : v) ++x;  // using a reference to allow us to change the value
for (const auto x : { 1,2,3,5,8,13,21,34 }) cout << x << '\n';

备注:使代码更简洁明了,尽量使用。

Initializer lists & Uniform initialization syntax and semantics

C++11支持使用{}初始化列表完成所有形式的初始化。C++11支持初始化列表:std::initializer_list<T>

vector<double> v = { 1, 2, 3.456, 99.99 };
void f(initializer_list<int>);
f({1,2});
f({23,345,4567,56789});
X x1 = X{1,2}; 
X x2 = {1,2};   // the = is optional
X x3{1,2}; 
X* p = new X{1,2}; 

struct D : X {
    D(int x, int y) :X{x,y} { /* ... */ };
};

struct S {
    int a[3];
    S(int x, int y, int z) :a{x,y,z} { /* ... */ }; // solution to old problem
};

备注:推荐全面统一使用初始化列表。

Rvalue references and move semantics

引用:

C++ 发明人 Bjarne Stroustrup 说过,他在 C++ 中引入引用的直接目的是为了让代码的书写更加漂亮,尤其是在运算符重载中,不借助引用有时候会使得运算符的使用很麻烦。

引用可以认为是原数据的一个别名,通过这个别名和原名字都能够找到这份数据。引用的定义方式类似于指针,只是用&取代了*,语法格式为:type &name = data; 引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,即不能动态改变。这是由编译器对引用的处理决定的。如果读者不希望通过引用来修改原始的数据,那么可以在定义时添加 const 限制,形式为:const type &name = value; 或者 type const &name = value;

int a = 99;
int &r = a;
cout<<"a:address->" <<&a<< endl;
cout<<"r:address->" <<&r<< endl;

上面的代码显示:a r 的地址一样;或者说同一地址的内存有两个名字。其实引用只是对指针进行了简单的封装,它的底层依然是通过指针实现的,引用占用的内存和指针占用的内存长度一样,之所以不能获取引用的地址,是因为编译器进行了内部转换。也就是说,不是变量 r 不占用内存,而是编译器不让获取它的地址。 

以下面的语句为例:

int a = 99;
int &r = a;
int &rr = r;
r = 18;
rr=19;
cout<<&r<<endl;

编译器会做如下形式的转换:

int a = 99;
int *r = &a;
int *rr=r;
*r = 18;
*rr=19;
cout<<r<<endl;

 左值和右值:

 关于左值(英文简写为“lvalue”)和右值(英文简写为“rvalue”),lvalue究竟是left value还是locator value的简写,rvalue究竟是right value还是read value的简写,并不重要;可以认为两个意思都有。

左值,可用于赋值运算符的左边,同时存储在内存中、有明确存储地址(可寻址)的数据。

右值,可用于赋值运算符的右边,该数据不一定可以寻址(例如存储于寄存器中的数据);典型例子是,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等),它们没有名称,也无法获取其存储地址,所以属于右值。

有一种简单区分左值和右值的方法,对表达式取地址,如果编译器不报错就为左值,否则为右值。

左值引用和右值引用:

C++11标准新引入了一种引用方式,称为右值引用,用 “&&” 表示。右值引用,与一个右值绑定,不能与左值绑定。基于右值引用引申出的 2 C++ 编程技巧,分别为移动语义和完美转发。

左值引用:type &引用名 = 左值表达式;就是对左值的引用,就是给左值取别名。

右值引用:type &&引用名 = 右值表达式;就是对右值的引用,就是给右值取别名。

下面是简单例子:

X a;
X f();
X& r1 = a;      // bind r1 to a (an lvalue)
X& r2 = f();    // error: f() is an rvalue; can't bind
X&& rr1 = f();  // fine: bind rr1 to temporary
X&& rr2 = a;    // error: bind a is an lvalue

引用折叠: 

所有右值引用折叠到右值引用上仍然是一个右值引用,即A&& &&变成A&&,具体见下面std::move的定义和说明。所有的其他引用类型之间的折叠都将变成左值引用, 即A& &变成A&,A& &&变成A&, A&& &变成A&)。

std::move()

std::move是为性能而设计的,能避免不必要的拷贝。std::move的作用在于将对象的状态转移,而非复制,提高效率。std::move并不能移动任何东西,它唯一的功能是将一个左值引用强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue)。通过std::move,可以将左值引用转换为右值引用,进而利用右值引用进行高效的操作,例如在向容器中插入元素时避免不必要的拷贝。 std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。 std::move函数的实现如下:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type &&>(t);
}

首先,函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配(可以传递左值或右值,这是std::move主要使用的两种场景)。简单来说,右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用。

 move(x) is just a cast that means “you can treat x as an rvalue”. Maybe it would have been better if move() had been called rval(), but by now move() has been used for years. The move() template function can be written in C++11 (see the “brief introduction) and uses rvalue references.

template<class T> 
void swap(T& a, T& b)   // "perfect swap" (almost)
{
   T tmp = move(a);    // could invalidate a
   a = move(b);        // could invalidate b
   b = move(tmp);      // could invalidate tmp
}

 移动语义:

所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

template<class T> class vector {
    // ...
    vector(const vector&);          // copy constructor
    vector(vector&&);           // move constructor
    vector& operator=(const vector&);   // copy assignment
    vector& operator=(vector&&);        // move assignment
};  // note: move constructor and move assignment takes non-const &&
    // they can, and usually do, write to their argument

C++11 standard library中,所有的容器都实现了move constructorsmove assignments, 操作如insert()push_back()等都实现了支持右值引用的版本。最终的结果就是,无需用户参与的情况下,由于拷贝更少,性能得到提升。

某些场景中,我们可能需要限制调用成员函数的对象的类型(左值还是右值),为此 C++11 新添加了引用限定符。所谓引用限定符,就是在成员函数的后面添加 “&” 或者 “&&”,从而限制调用者的类型(左值还是右值)。注意,引用限定符不适用于静态成员函数和友元函数。

const && 修饰类的成员函数时,调用它的对象只能是右值对象;当 const & 修饰类的成员函数时,调用它的对象既可以是左值对象,也可以是右值对象。无论是 const && 还是 const & 限定的成员函数,内部都不允许对当前对象做修改操作。

总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。

class demo{
    int get_num();   // 默认情况下,成员对象既可以被左值,又可以被右值调用
	int get_num()& ;  // &限制被调用的成员对象必须是左值
	int get_num()&& ;  //&&限制被调用的成员对象必须是右值
}

备注:在C++中,尽量使用引用替代指针;尽量使用移动语义提升效率。

Lambdas

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }。

[capture-list] : 捕捉列表,必须项,总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

(parameters):参数列表,同普通函数的参数列表,如果不需要参数,则可以连同()一起省略。

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时可省略。返回值类型明确时,也可省略,由编译器对返回类型进行推导。

{statement}:函数体,必须项,函数体内可以使用参数,还可以使用捕捉列表的变量。

综上,参数列表和返回值类型是可选部分,而捕捉列表和函数体可以为空。因此,C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

Lambda表达式用于指定一个函数对象。 “capture list”,指定函数能使用的本地变量,以及使用的方式(传值的方式,或者引用的方式),以下是具体的例子:

[&v]表示仅仅以引用的方式使用v。

[=v] or [v]表示以传值的方式使用v(仅仅v)。

[]表示不使用任何本地变量。

[&]表示所有本地变量以引用方式传递。

[=]表示所有本地变量以值方式传递。

例如:

// sort by absolute value:
std::sort(v.begin(), v.end(), [](int a, int b) { return abs(a)<abs(b); });

void f(vector<Record>& v)
{
    vector<int> indices(v.size());
    int count = 0;
    generate(indices.begin(),indices.end(),[&count](){ return count++; });
    // sort indices in the order determined by the name field of the records:
    std::sort(indices.begin(), indices.end(), [&](int a, int b) { return v[a].name<v[b].name; });
    // ...
}

noexcept to prevent exception propagation 

extern "C" double sqrt(double) noexcept;    // will never throw

// I'm not prepared to handle memory exhaustion
vector<double> my_computation(const vector<double>& v) noexcept 
{
    vector<double> res(v.size());   // might throw
    for(int i; i<v.size(); ++i) res[i] = sqrt(v[i]);
    return res;
}

备注:C++标准库大量使用noexcept来提升性能和标明需求;我们的代码,也应该这样使用。

Constexpr

C++11引入关键字constexprconstexpr定义常量并确保在编译时完成初始化。

constexpr int x1 = bad|eof; // ok
void f(Flags f3)
{
    constexpr int x2 = bad|f3;  // error: can't evaluate at compile time
    int x3 = bad|f3;        // ok
}

C++ 允许在一个函数前面直接加上constexpr关键字修饰。表示这个函数既可以在运行期调用,也可以在编译期调用,而函数本身的内容几乎不需要任何改变。

相比于利用模板进行编译期计算,constexpr函数的编译速度会快很多 。如果你好奇编译期是如何实现这一强大的特性的,可以认为,C++编译器内部内嵌了一个小的解释器,这样遇到constexpr函数的时候用这个解释器解释一下,再把计算结果返回就行了。

C++11标准中,const用于为修饰的变量添加只读属性;而constexpr 关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译时将其结果计算出来,而无需等到程序运行阶段,这样能提高了程序的执行效率。

备注:constexpr可用于自定义类型,确保在编译时完成初始化。注意这里的编译器计算概念。 

nullptr – a null pointer literal

C++11标准中引入一个新关键字nullptr,表示空指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。nullptr是nullptr_t类型的右值常量,专用于初始化空类型指针。也就是说,nullptr仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。nullptr 可以被隐式转换成任意的指针类型,nullptr不是整数;NULL实际为整数0(#define NULL 0)。

C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:

int *p = 0;
int *p = NULL; //推荐使用

备注:C++中,用nullptr全面替代以前的NULL。

Copying and rethrowing exceptions

略。

Inline namespaces 

原文的意思,inline namespaces提供了对库的一种类似版本管理的机制,感觉有点勉强,其实namespace本身的作用域形式,就可以认为是一种版本管理;Inline namespace,提供了缺省版本的定义。

// file V99.h:
inline namespace V99 {
    void f(int);    // does something better than the V98 version
    void f(double); // new feature
    // ...
}
    
// file V98.h:
namespace V98 {
    void f(int);    // does something
    // ...
}
    
// file Mine.h:
namespace Mine {
#include "V99.h"
#include "V98.h"
}

#include "Mine.h"
using namespace Mine;
// ...
V98::f(1);  // old version
V99::f(1);  // new version
f(1);       // default version,来自inline namespace中

备注:指定缺省版本的场合下,可以使用。

User-defined literals 

 针对内置数据类型,C++支持:

1.2     // double
1.2F    // float
'a'     // char 
0xD0    // hexadecimal unsigned
"as"    // string
10LL    // long long
10ULL   // unsigned long long
10      // int

long类型整数需明确标注 “L” 或者 “l” 后缀。对于有符号long long整形,后缀用 “LL” 或者 “ll” 标识。对于无符号long long整形,后缀用 “ULL”、“ull”、“Ull” 或者 “uLL” 标识。如果不添加任何标识,则所有的整数都会默认为int类型。

C++11支持“用户定义的文本”,通过文本操作符的概念,将具有给定后缀的文本映射到所需的类型中。在一串文本之后,编译器检查是否有支持的后缀,并根据后缀定义的文本操作符进行处理。程序员也可以自定义后缀,例如,用后缀s表示string,i表示imaginary, m表示meter, x表示extended。下面是自定义的文本操作符例子:

constexpr complex<double> operator "" i(long double d)  // imaginary literal
{
    return {0,d};   // complex is a literal type
}
std::string operator""s (const char* p, size_t n)   // std::string literal
{
    return string(p,n); // requires free store allocation
}

 备注:对缺省数据类型,确保变量类型和值完全对应。新的“user-defined literal”也用起来。

C++11 Language Extensions – Classes

=default and =delete 

class X {
    // ...
    X& operator=(const X&) = delete;    // Disallow copying
    X(const X&) = delete;    //禁止用类对象初始化
};

struct Z {
    // ...
    Z(long long);     // can initialize with a long long
    Z(long) = delete; // but not anything smaller,禁止类型转换
};

class Y {
    // ...
    Y& operator=(const Y&) = default;   // default copy semantics
    Y(const Y&) = default;
};

备注:简单暴力,直接明了。使用=default要注意,一般编译器自动生成的缺省函数更有效率。

Control of default move and copy 

 C++11中,类默认支持以下5个操作(即如果程序员不显式定义的话,编译器会自动生成):

copy assignment,copy constructor,move assignment,move constructor,destructor。

这5个操作是有关联的整体,如果显式定义了其中之一,也应显式定义全部。如果显式定义(声明,定义,=default,=delete都算)了任一个copy,move,destructor操作,编译器就不会再自动生成move操作。

备注可以不用太关注上述理论,以实测为准。

Delegating constructors

class X {
    int a;
public:
    X(int x) { if (0<x && x<=max) a=x; else throw bad_X(x); }
    X() :X{42} { }
    X(string s) :X{lexical_cast<int>(s)} { }
    // ...
};

备注类中的构造函数调用另一个构造函数,本质没区别,但效率应该会高点。

In-class member initializers 

//In C++98, only static const members of integral types could be initialized in-class, and //the initializer has to be a constant expression. Example in C++98:
int var = 7;
class X {
    static const int m1 = 7;        // ok
    const int m2 = 7;                   // error: not static
    static int m3 = 7;              // error: not const
    static const int m4 = var;          // error: initializer not constant expression
    static const string m5 = "odd"; // error: not integral type
    // ...
};

//The basic idea for C++11 was to allow a non-static data member to be initialized where it //is declared (in its class). A constructor can then use the initializer when run-time //initialization is needed. The following two code is equivalent.
class A {
public:
    int a = 7;
};

//This is equivalent to:
class A {
public:
    int a;
    A() : a(7) {}
};

 备注给类成员变量提供一个缺省值,可以避免漏掉初始化的情况。

Inherited constructors:

class Derived : public Base { 
public: 
    using Base::f;    // lift Base's f into Derived's scope -- works in C++98
    void f(char);     // provide a new f 
    void f(int);      // prefer this f to Base::f(int) 
    using Base::Base; // lift Base constructors Derived's scope -- new in C++11
    Derived(char);    // provide a new constructor 
    Derived(int);     // prefer this constructor to Base::Base(int) 
    // ...
};

备注注意成员函数在父类和子类中作用域的不同。

 Override controls: override

//C++98
struct B {
    virtual void f();
    virtual void g() const;
    virtual void h(char);
    void k();   // not virtual
};
    
struct D : B {
    void f();   // overrides B::f()
    void g();   // doesn't override B::g() (wrong type)
    virtual void h(char);   // overrides B::h()
    void k();   // doesn't override B::k() (B::k() is not virtual)
};

//C++11
struct D : B {
    void f() override;  // OK: overrides B::f()
    void g() override;  // error: wrong type
    virtual void h(char);   // overrides B::h(); likely warning
    void k() override;  // error: B::k() is not virtual
};

//override is only a contextual keyword, so you can still use it as an identifier:
int override = 7;   // valid, not recommended

备注建议使用,因为表达更明确更具体,能让编译器帮忙检查是否符合设计意图。

Override controls: final

struct B {
    virtual void f() const final;   // do not override
    virtual void g();
};
    
struct D : B {
    void f() const;     // error: D::f attempts to override final B::f
    void g();       // OK
};

//final is only a contextual keyword, so you can still use it as an identifier:
int final = 7;  // not recommended

 备注:final声明一个虚拟成员函数不能被overridden。final断了再被overridden的可能,即断了再被优化的可能。所以设计时一定要预先准确定义好virtual和override和final的使用。

Explicit conversion operators

C++98支持隐式和显式构造函数,C++98不支持显式类型转换操作符;而C++11支持显式类型转换操作符。

struct S { S(int); };   // "ordinary constructor" defines implicit conversion
    
S s1(1);        // ok 
S s2 = 1;   // ok
void f(S);
f(1);       // ok (but that's often a bad surprise -- what if S was vector?)

struct E { explicit E(int); };  // explicit constructor,C++98支持
E e1(1);        // ok
E e2 = 1;   // error (but that's often a surprise)
void f(E);

f(1); //error (protects against surprises--e.g.std::vector's constructor from int is explicit)

struct S { S(int) { } };
struct SS {
    int m;
    SS(int x) :m(x) { }
    explicit operator S() { return S(m); } //because S don't have S(SS),C++11支持
};

SS ss(1);
S s1 = ss;  // error; like an explicit constructor
S s2(ss);   // ok ; like an explicit constructor
void f(S); 
f(ss);      // error; like an explicit constructor

 备注explicit指定更明确的调用方式,需要时建议使用。

C++11 Language Extensions — Other Types

 enum class

C++11中enum class的使用,不再把枚举成员隐式转换为int,限定了枚举成员的范围(不再暴露在外围)。下面是一些例子:

enum class Color { red, blue };   // C++11, scoped and strongly typed enum
// no export of enumerator names into enclosing scope
// no implicit conversion to int
enum class TrafficLight { red, yellow, green };
Alert a = 7;              // error (as ever in C++)
Color c = 7;              // error: no int->Color conversion
int a2 = red;             // ok: Alert->int conversion
int a3 = Alert::red;      // error in C++98; ok in C++11
int a4 = blue;            // error: blue not in scope
int a5 = Color::blue;     // error: not Color->int conversion
Color a6 = Color::blue;   // ok

enum class Color : char { red, blue };  // compact representation
enum class TrafficLight { red, yellow, green };  //by default, the underlying type is int
enum E { E1 = 1, E2 = 2, Ebig = 0xFFFFFFF0U };   // how big is an E?
// (whatever the old rules say;
// i.e. "implementation defined")
enum EE : unsigned long { EE1 = 1, EE2 = 2, EEbig = 0xFFFFFFF0U }; //now we can be specific


enum class Color_code : char;     // (forward) declaration
void foobar(Color_code* p);       // use of forward declaration
// ...
enum class Color_code : char { red, yellow, green, blue }; // definition

 备注建议全面使用,好处明显:强类型,范围明确,并可以指定基础类型。

Generalized unions

C++98不支持union成员有构造函数/析构函数/赋值函数;而C++11支持,同时构造函数和析构函数会被去掉。

union U1 {
    int m1;
    complex<double> m2; // ok
};

union U2 {
    int m1;
    string m3;  // ok
};

U1 u;           // ok
u.m2 = {1,2};   // ok: assign to the complex member
U2 u2;      //error: the string destructor caused the U destructor to be deleted
U2 u3 = u2; //error: the string copy constructor caused the U copy constructor to be deleted

 备注原文信息比较难理解,应该是C++11支持Union包含自定义类型的成员,且不受其构造函数/析构函数等的影响(被去掉了)。

Generalized PODs

POD(“Plain Old Data”)对象,实质就是可以将对象视为一块内存,可以用memcpy()和memset()对整块内存进行处理。POD在文中是以递归的方式进行定义的。文中英文表述为:A POD (“Plain Old Data”) is something that can be manipulated like a C struct, e.g. bitwise copyable with memcpy(), bitwise initializable with memset(), etc. 

// S is a POD
struct S { 
    int a; 
};

// SS was not a POD in C++98, is a POD in C++11
struct SS { 
    int a;
    SS(int aa) : a(abs(aa)) { assert(a>=0); }
};

// Definitely not POD
struct SSS {
    virtual void f(); /* ... */
};

备注从对象占用的内存来理解,POD不能有指针(无论虚指针,还是别的),可以认为C++11才更接近其本来意义。

C++11 Language Extensions – Templates

About template: 

模板最核心的功能应该是代码生成。所谓泛型 (Generic) 编程,就是为不同的数据类型(或者数据对象)编写相同的代码,实现代码复用。在引入模板之前,只能通过宏来模拟泛型。

下面是模板的例子:

template <typename T>
T add(T a, T b) {
    return a + b;
}

template int add<>(int, int);  // explicit instantiation

int main() {
    add(1, 2);         // auto deduce T
    add(1.0f, 2.0f);   // implicit instantiation
    add<float>(1, 2);  // explicitly specify T
}

//下面例子中,constexpr声明的函数,可以编译时调用,也可以运行时调用:
constexpr std::size_t factorial(std::size_t N) {
    std::size_t result = 1;
    for(std::size_t i = 1; i <= N; ++i) {
        result *= i;
    }
    return result;
}

int main() {
    constexpr auto a = factorial(5);  // compile-time
    std::size_t& n = *new std::size_t(6);
    auto b = factorial(n);  // run-time
}
//对类型进行判断,C++中不允许第一种写法,支持第二种写法:
template <typename T>
void test() {
    if(T == int) {
        /* ... */
    }
}

template <typename T>
void test() {
    if constexpr(std::is_same_v<T, int>) {
        /* ... */
    }
}

 模板类型用typename还是class修饰?一般情况下, template<typename T>与template<class T>通用,但有一个特例,当T是一个类,而它又有子类(名为 innerClass) 时,应该用:

typename T::innerClass myInnerObject;

这里的typename告诉编译器,T::innerClass 是一个类,程序要声明一个T::innerClass 类的对象,而不是声明 T 的静态成员,而 typename 如果换成 class 则语法错误。依据的C++规则是,除了使用typename修饰之外,template内的任何标识符号都被视为一个值而不是一个类别(对象)。

Extern templates 

 #include "MyVector.h"
 extern template class MyVector<int>; // Suppresses implicit instantiation below --
 // MyVector<int> will be explicitly instantiated elsewhere
    
void foo(MyVector<int>& v)
{
    // use the vector in here
}

#include "MyVector.h"
template class MyVector<int>; //Make MyVector available to clients (e.g., of the shared library

备注extern声明能避免模板的隐式实例化,减少编译器和链接器的重复工作。

Template aliases

template<class T>
using Vec = std::vector<T,My_alloc<T>>;  //alias, standard vector using my allocator
Vec<int> fib = { 1, 2, 3, 5, 8, 13 }; // allocates elements using My_alloc


typedef void (*PFD)(double);      // C style
using PF = void (*)(double);      // using plus C-style type
using P = auto (*)(double)->void; // using plus suffix return type

备注C++中通过using定义模板别名,也可以定义函数类型。使用using的方式更容易理解。

Variadic templates

template<class ... Types> 
void f(Types ... args); // variadic template function
// (i.e. a function that can take an arbitrary number of arguments of arbitrary types)
   
f();        // OK: args contains no arguments
f(1);       // OK: args contains one argument: int
f(2, 1.0);  // OK: args contains two arguments: int and double

备注:模板参数可变,应该函数参数可变的情况

Local types as template arguments

void f(vector<X>& v)
{
    struct Less {
        bool operator()(const X& a, const X& b) { return a.v<b.v; }
    };
    sort(v.begin(), v.end(), Less());   // C++98: error: Less is local  // C++11: ok
}

void f(vector<X>& v)
{
    sort(v.begin(), v.end(), 
              [] (const X& a, const X& b) { return a.v<b.v; }); // C++11 
}

template<typename T> void foo(T const& t){};
enum X { x };
enum { y };

int main()
{
    foo(x);     // C++98: ok; C++11: ok
    foo(y);     // C++98: error; C++11: ok
    enum Z { z };
    foo(z);     // C++98: error; C++11: ok 
}

 备注C++11中更合理,对编程限制更少。

C++11 Language Extensions — Miscellaneous Language Features

What is the value of __cplusplus for C++11? 

In C++11 the macro __cplusplus is set to the value 201103L. (Before C++11, it was 199711L.)

Suffix return type syntax

template<class T, class U>
    ??? mul(T x, U y)
{
    return x*y;
}

//函数的返回值类型表示方法:
template<class T, class U>
   auto mul(T x, U y) -> decltype(x*y)   
   //the notation auto to mean “return type to be deduced or specified later.”
{
    return x*y;
}

 备注只有如此,不然无法支持这种情况。

Preventing narrowing

C++11, {} initialization doesn’t narrow。

int x = 7.3;    // Ouch! implicitly Truncated!!
void f(int);
f(7.3);         // Ouch! implicitly Truncated!!

int x0 {7.3};   // error: narrowing
int x1 = {7.3}; // error: narrowing
double d = 7;
int x2{d};      // error: narrowing (double to int)
char x3{7};     // ok: even though 7 is an int, this is not narrowing
vector<int> vi = { 1, 2.3, 4, 5.6 };    // error: double to int narrowing

char c1{7};      // OK: 7 is an int, but it fits in a char
char c2{77777};  // error: narrowing (assuming 8-bit chars)
int x=7.0; //error:Note that floating-point to integer conversions are always considered narrowing – even 7.0 to 7.

备注初始化列表提供了更精确的方式,更能避免隐式转换或者截断带来的错误。

Right-angle brackets

list<vector<string>> lvs;  //compiles in C++11 and fails in C++98

备注跟编译器的font-end处理过程有关,front-end处理包括词法分析,语法分析,类型检查等;即跟编译器的实现有关,个人理解,更改以前的编译器实现,应该也可以支持。

static_assert compile-time assertions 

C++11新引入static_assert用于编译时,运行时继续使用assert宏(只在debug版本有效)。

static_assert(expression,string);

assert(expression);

这两个宏的实现代码如下:

//运行时,根据条件,调用printf,和中止程序
#define assert(x)		\
	do { if (!(x)) printf("assertion failed: %s", #x); } while (0)

//编译时,两个相同case值0,导致编译报错
#define static_assert(a, b) \
	do { switch (0) case 0: case (a): ; } while (0)

下面是使用实例:

static_assert(sizeof(long)>=8, "64-bit code generation required for this library.");

struct S { X m1; Y m2; };
static_assert(sizeof(S)==sizeof(X)+sizeof(Y),"unexpected padding in S");

assert(p==0 && "p is not null");

备注static_assert丰富了程序员的工具,理论上在C++11之前和C中,也可以自己手动添加这一功能,甚至采用不同的实现。

Raw string literals

//Consider how to write the pattern representing two words separated by a backslash (\w\\\w):
string s = "\\w\\\\\\w";    // I hope I got that right
string s = R"(\w\\\w)"; // I'm pretty sure I got that right
R"("quoted string")"    // the string is "quoted string"

备注C++11借鉴了其他语言的一些做法,也许是互相学习。 

Attributes

属性定义的格式为:[ [ … ] ]。

C++11标准定义属性有:noreturn,carries_dependency。属性是辅助功能,noreturn属性能帮助检测错误,carries_dependency属性能帮助优化。

void f [ [ noreturn ] ] ()  // f() will never return
{
    throw "error";  // OK
}

struct foo* f [ [ carries_dependency ] ] (int i);   // hint to optimizer
int* g(int* x, int* y [ [ carries_dependency ] ] );

备注有好处,当然要充分利用。网上可以搜索C++支持的属性列表,另外,编译器厂商也可以自定义属性,GNU的[[hot]]、[[always_inline]],微软的[[declspec(xxx)]] 等。 

Alignment 

C++11 标准引入了两个关键的特性来支持内存对齐:alignof和alignas。这两个特性提供了对内存对齐的直接控制。

alignof是一个操作符,用于查询类型或变量的对齐要求。它返回一个std::size_t类型的值,表示类型或变量的对齐字节数。

alignas是一个对齐说明符,用于指定变量或类型的最小对齐要求。alignas可以用于变量声明或类型定义中,以确保所声明的变量或类型实例具有特定的对齐。alignas指定的对齐要求不能低于类型的自然对齐要求。

下面是例子: 

alignas(double) unsigned char c[1024];  //array of characters, suitably aligned for doubles
alignas(16) char[100];                  // align on 16 byte boundary

struct alignas(16) AlignedStruct {
    int i;
};

constexpr int n = alignof(int);     // ints are aligned on n byte boundaries

备注内存对齐,与数据在内存中的布局有关,目的是提高访问效率。具体是指,将数据存储在地址能被特定大小(如4、8、16等)整除的内存位置,这样的数据访问通常能得到硬件层面的优化,提高程序运行效率。

C99 features 

支持C99特性:如long long, __func__宏,_ _VA_ARGS_ _等。

#define report(test, ...) ((test)?puts(#test):printf(_ _VA_ARGS_ _))

备注可搜索C99相关内容。后面可专门学习一下C语言标准和标准的演进情况。

C++11 Standard Library Extensions — General Libraries

unique_ptr & shared_ptr & weak_ptr

C++98支持auto_ptr;C++11支持unique_ptr,shared_ptr,weak_ptr。

C++11中的unique_ptr具有以下特性:

1,unique_ptr对象对它指向的对象拥有所有权,管理其生命周期,负责其内存的申请和释放。

2,不支持拷贝构造和拷贝赋值,只支持move构造和move赋值。

3,unique_ptr对象自动摧毁时,同时释放它拥有的对象。

另外,C++11有std::make_shared,却没有std::make_unique, 在C++14已经加上。

例子如下: 

X* f()
{
    unique_ptr<X> p(new X);     // or {new X} but not = new X
    // do something -- maybe throw an exception
    return p.release();
}

unique_ptr<X> f()
{
    unique_ptr<X> p(new X);     // or {new X} but not = new X
    // do something -- maybe throw an exception
    return p;   // the ownership is transferred out of f()
}

void g()
{
    unique_ptr<X> q = f();       // move using move constructor
    q->memfct(2);                // use q
    X x = *q;                    // copy the object pointed to
    // ...
}   // q and the object it owns is destroyed on exit

C++11中的shared_ptr表示对其中指向的对象共享所有权,对指向的对象同时维护了一个引用计数,当引用计数为0时,释放所指向的对象。Shared_ptr适用于垃圾回收和文件句柄,如多个应用同时打开同一个文件,或者同时打开多个应用窗口的场景。注意:不要随意使用shared_ptr管理对象的所有权和生命周期,设计时要清楚对象的所有权和生命周期,根据需要依次选择放在栈上,或者作为类对象成员(对象再根据需要进行合理分配),或者作为unique_ptr管理的堆对象,或者make_shared的堆对象。

可以认为,每个unique_ptr指针指向的堆内存空间的引用计数,都只能为1,一旦该unique_ptr指针放弃对所指堆内存空间的所有权,则该空间会被立即释放回收。

C++11标准提供了std::make_shared模板函数,用于初始化shared_ptr智能指针。代码示例如下:

std::shared_ptr<int> p3(new int(10));
std::shared_ptr<int> p3 = std::make_shared<int>(10);

void test()
{
    shared_ptr<int> p1(new int);    // count is 1
    {
        shared_ptr<int> p2(p1); // count is 2
        {
            shared_ptr<int> p3(p1); // count is 3
        }   // count goes back down to 2
    } // count goes back down to 1
}   // here the count goes to 0 and the int is deleted.

原文对weak_ptr解释不够明确,weak_ptr是智能指针,但其中所指向的对象是弱引用,weak_ptr对象不具有所有权。使用场景:一切应该不具有对象所有权,又想安全访问对象的情况。std::weak_ptr的lock函数是一个原子操作。 

auto sp = wp.lock();
if (sp) 
{
    sp->DoSomething();
}

 备注避免内存泄漏,你值得拥有。

 Garbage collection ABI

 垃圾回收,指自动回收不再被使用的内存。C++中不是强制实现,C++从来没有公开支持过垃圾回收机制,但C++11提供了相关功能接口定义。

垃圾回收机制得到了诸多编程语言的支持,例如Java、Python、C#、PHP等。C++98/03标准中,支持使用auto_ptr智能指针来实现堆内存的自动回收;C++11标准废弃auto_ptr同时,新增unique_ptr、shared_ptr和weak_ptr这3个智能指针来实现堆内存的自动回收。

C++智能指针底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。实际上,每种智能指针都是以类模板的方式实现的。

备注垃圾回收,在C++中没有强制实现;如果需要,可以按照ABI来实现。

Tuple

元组,有2元组,3元组,4元组等等;其中用得最多的是2元组,即数据对,英文pair。

元组的成员的类型,可以显式指定,也可以编译器自动推导(调用make_tuple()),元素也可以通过get()接口,基于0开始的索引进行访问。相关代码示例如下:

tuple<string,int> t2("Kylling",123);  
auto t = make_tuple(string("Herring"),10, 1.23);    // t will be of type tuple<string,int,double>
string s = get<0>(t);
int x = get<1>(t);
double d = get<2>(t);

备注二元组与pair一定上可通用,多一个选择。注意三元组和多元组的可能应用场景。

function and bind

C++11中的function是C++98 standard library中 mem_fun_t, pointer_to_unary_function等的替代;bind()是bind1st()和bind2nd()的替代。下面是使用举例:

int f(int,char,double);

// auto to deduce return type
// The _1 is a placeholder object for the first argument of ff if called through ff.
// the 2nd and 3rd parameter is already spcified here
auto ff = bind(f,_1,'c',1.2);   
int x = ff(7);          //call f(7,'c',1.2);

// equivalent with lambdas
auto ff2 = [](int i){ f(i,'c',1.2); };  // deduce return type
int x2 = ff2(7);        //call  f(7,'c',1.2);

int f(int,char,double);
auto frev = bind(f,_3,_2,_1);   // reverse argument order
int x = frev(1.2,'c',7);    // f(7,'c',1.2);
// equivalent with lambdas
auto frev2 = [](double d, char c, int i){ f(i,c,d); };  // reverse argument order
int x2 = frev2(1.2,'c',7);  // f(7,'c',1.2);


int g(int);
double g(double);   // g() is overloaded
auto g1 = bind(g,_1);               // error: which g()?
auto g2 = bind((double(*)(double))g,_1);    // ok (but ugly)
// equivalent with C++11 lambdas, which handle this naturally
auto g3 = [](double d){ g(d); };    // ok in C++11
// both shorter and more powerful with C++14 lambdas
auto g4 = [](auto x){ g(x); };  // ok in C++14, and gives full access to the overload set

auto f2 = bind<int>(f,7,'c',_1);    // explicit return type
int x = f2(1.2);            // f(7,'c',1.2);


function<float (int x, int y)> f;   // make a function object
struct int_div {            // take something you can call using ()
     float operator()(int x, int y) const { return ((float)x)/y; };
};
f = int_div();              // assign
cout << f(5, 3) << endl;        // call through the function object
std::accumulate(b,e,1,f);       // passes beautifully

struct X {
    int foo(int);
};
function<int (X*, int)> f;
f = &X::foo;        // pointer to member
X x;
int v = f(&x, 5);   // call X::foo() for x with 5
function<int (int)> ff = std::bind(f,&x,_1);    // first argument for f is &x
v=ff(5);        // call x.foo(5)

备注注意例子中的函数对象,bind()的不同用法。

Time utilities

 C++11标准库提供了时间相关的丰富功能,如time_point,now(),system_clock, steady_clock,high_resolution_clock等。代码示例如下:

steady_clock::time_point t = steady_clock::now();
// do something
steady_clock::duration d = steady_clock::now() - t;
// something took d time units

//A clock returns a time_point, 
//a duration is the difference between two time_points from the same clock. 
//As usual, if you are not interested in details, auto is your friend:
auto t = steady_clock::now();
// do something
auto d = steady_clock::now() - t;
// something took d time units

//Here are some examples of values using standard duration types as defined in <chrono>:
microseconds mms = 12345;
milliseconds ms = 123;
seconds s = 10;
minutes m = 30;
hours h = 34;
auto x = std::chrono::hours(3);         // being explicit about namespaces
auto x = hours(2)+minutes(35)+seconds(9);   // assuming suitable "using"

//a duration is interpreted as a number of “ticks.”
duration<long> d0 = 5;                  // seconds (by default)
duration<long,kilo> d1 = 99;            // kiloseconds!
duration<long,ratio<1000,1>> d2 = 100;  // d1 and d2 have the same type ("kilo" means "*1000")


auto t = steady_clock::now();
// do something
nanoseconds d = steady_clock::now() - t;    // we want the result in nanoseconds
cout << "something took " << d << "nanoseconds\n";

备注使用标准库的东西,可以保证平台可移植性。

 Random number generation

C++11标准库提供了随机数生成接口。随机数生成器包括两部分:一是产生随机或者伪随机序列的引擎;二是在一个数学区间内进行分发。示例代码如下:

//To get a random number, you call a distribution with an engine:
uniform_int_distribution<int> one_to_six {1,6};  // distribution that maps to the ints 1..6
default_random_engine re {};                     // the default engine
int x = one_to_six(re); // x becomes a value in [1:6]

auto dice {bind(one_to_six,re)};   // make a generator
int x = dice(); // roll the dice: x becomes a value in [1:6]

int rand_int(int low, int high);    // generate a random number from a uniform distribution in [low:high]

备注真随机,还是伪随机,跟底层实现有关;甚至真随机需要底层硬件支持。

 Scoped allocators

template<class T> class Simple_alloc {  // C++98 style
    // no data
    // usual allocator stuff
};

class Arena {
    void* p;
    int s;
public:
    Arena(void* pp, int ss);
    // allocate from p[0..ss-1]
};

template<class T> struct My_alloc {
    Arena& a;
    My_alloc(Arena& aa) : a(aa) { }
    // usual allocator stuff
};

Arena my_arena1(new char[100000],100000);
Arena my_arena2(new char[1000000],1000000);
vector<int> v0;                                         // allocate using default allocator
vector<int,My_alloc<int>> v1(My_alloc<int>{my_arena1}); // allocate from my_arena1
vector<int,My_alloc<int>> v2(My_alloc<int>{my_arena2}); // allocate from my_arena2
vector<int,Simple_alloc<int>> v3;                       // allocate using Simple_alloc

// vector and string use their own (the default) allocator:
using svec0 = vector<string>;
svec0 v0;

// vector (only) uses My_alloc and string uses its own (the default) allocator:
using svec1 = vector<string,My_alloc<string>>;
svec1 v1(My_alloc<string>{my_arena1});
// vector and string use My_alloc (as above):
using xstring = basic_string<char, char_traits<char>, My_alloc<char>>;
using svec2 = vector<xstring,scoped_allocator_adaptor<My_alloc<xstring>>>;
svec2 v2(scoped_allocator_adaptor<My_alloc<xstring>>{my_arena1});
// vector uses My_alloc and string uses My_string_alloc:
using xstring2 = basic_string<char, char_traits<char>, My_string_alloc<char>>;
using svec3 = vector<xstring2,scoped_allocator_adaptor<My_alloc<xstring>, My_string_alloc<char>>>;  
svec3 v3(scoped_allocator_adaptor<My_alloc<xstring2>, My_string_alloc<char>>{my_arena1,my_string_arena});

备注涉及内存管理,指定从哪里分配内存;应该标准库本身也用到了吧。

C++11 Standard Library Extensions — Containers and Algorithms

Algorithms improvements 

The standard library algorithms are improved partly by simple addition of new algorithms, partly by improved implementations made possible by new language features, and partly by new language features enabling easier use。

For example, move-based std::sort() and std::set::insert() have been measured to be 15 times faster than copy based versions.

备注:由于移动语义的引入,部分已有算法性能已经大幅提升。

Container improvements

First, of course we got a few new ones: array (a fixed-sized container), forward_list (a singly-linked list), and unordered containers (the hash tables). Next, new features, such as initializer lists, rvalue references, variadic templates, and constexpr[cpp11-constexpr] were put to use. Consider std::vector.

vector<string> vs = { "Hello", ", ", "World!", "\n" };
for (auto s : vs ) cout << s;

vector<int> make_random(int n)
{
    vector<int> ref(n);
    for(auto& x : ref) x = rand_int(0,255); // some random number generator
    return ref;
}
    
vector<int> v = make_random(10000);
for (auto x : make_random(1000000)) cout << x << '\n';

vector<pair<string,int>> vp;
string s;
int i;
while(cin>>s>>i) vp.push_back({s,i});

vector<pair<string,int>> vp;
string s;
int i;
while(cin>>s>>i) vp.emplace_back(s,i);

备注:新引入了一些container类型,程序员有更多选择,但也需要深入理解他们的差别。

unordered_* containers 

An unordered_* container is implemented using a hash table. C++11 offers four standard ones:

unordered_map

unordered_set

unordered_multimap

unordered_multiset

//map vs unordered_map
map<string,int> m {
    {"Dijkstra",1972}, {"Scott",1976}, {"Wilkes",1967}, {"Hamming",1968}
};
m["Ritchie"] = 1983;
for(auto& x : m) cout << '{' << x.first << ',' << x.second << '}';

unordered_map<string,int> um {
    {"Dijkstra",1972}, {"Scott",1976}, {"Wilkes",1967}, {"Hamming",1968}
};    
um["Ritchie"] = 1983;
for(auto& x : um) cout << '{' << x.first << ',' << x.second << '}';

备注:基于hash表,所以unordered,甚至更高效。

std::array

array<int,6> a = { 1, 2, 3 };
a[3]=4;
int x = a[5];       // x becomes 0 because default elements are zero initialized
int* p1 = a;        // error: std::array doesn't implicitly convert to a pointer
int* p2 = a.data(); // ok: get pointer to first element

array<int> a3 = { 1, 2, 3 };    // error: size unknown/missing
array<int,0> a0;        // ok: no elements
int* p = a0.data();     // unspecified; don't try it

template<class C> typename C::value_type sum(const C& a)
{
    return accumulate(a.begin(),a.end(),0);
}
array<int,10> a10;
array<double,1000> a1000;
vector<int> v;
// ...
int x1 = sum(a10);
double x2 = sum(a1000);
int x3 = sum(v);

 The standard container array is a fixed-sized random-access sequence of elements defined in <array>.

备注:跟之前的版本的std::array有什么区别?之前版本没有吗?

forward_list 

The standard container forward_list, defined in <forward_list>, is basically a singly-linked list. It supports forward iteration (only) and guarantees that elements don’t move if you insert or erase one. It occupies minimal space (an empty list is likely to be one word) and does not provide a size() operation (so that it does not have to store a size member):

template <ValueType T, Allocator Alloc = allocator<T> >
// requires NothrowDestructible<T>
class forward_list {
public:
    // the usual container stuff
    // no size()
    // no reverse iteration
    // no back() or push_back()
};

备注:一个新的特殊的容器forward_list,不支持随机访问,特别适合于需要在列表前端进行频繁插入和删除操作的场景。

C++11 Standard Library Extensions — Concurrency

对各小结在原文中的顺序进行了简单调整,便于理解。

Thread-local storage

In C++11 you can use the storage class thread_local to define a variable that should be instantiated once per thread. Note that using thread_local storage requires care, and in particular does not work well with most parallel algorithms.

备注使用中注意,thread_local对象的生命周期和范围。

Mutual exclusion

一个mutex可视为一个资源,也可视为一个基本对象,使用mutex的线程都可访问它;作为资源,它不能拷贝和移动。下面是mutex,recursive_mutex,timed_mutex等的基本用法:

// basic use: lock()
std::mutex m;
int sh; // shared data
// ...
m.lock();
// manipulate shared data:
sh+=1;
m.unlock();

//try_lock() to try to get into the critical region without the risk of getting blocked:
std::mutex m;
int sh; // shared data
// ...
if (m.try_lock()) {
    // manipulate shared data:
    sh+=1;
    m.unlock();
else {
    // maybe do something else
}

//use recursive_mutex that can be acquired more than once by a thread:
std::recursive_mutex m;
int sh; // shared data
// ...
void f(int i)
{
    // ...
    m.lock();
    // manipulate shared data:
    sh+=1;
    if (--i>0) f(i);
    m.unlock();
    // ...
}

//use timed_mutex to try_lock() with an associated time limit:
std::timed_mutex m;
int sh; // shared data
// ...
if (m.try_lock_for(std::chrono::seconds(10))) {
    // manipulate shared data:
    sh+=1;
    m.unlock();
}
else {
    // we didn't get the mutex; do something else
}

//There is of course also a recursive_timed_mutex.

备注:注意掌握recursive_mutex和timed_mutex等的使用。 

Locks

//A lock can be moved (a lock is to represent local ownership of a non-local resource), //but not copied (which copy would own the resource/mutex?).


std::mutex m;
int sh; // shared data
// ...
void f()
{
    // ...
    std::unique_lock lck(m);
    // manipulate shared data: lock will be released even if code throws an exception
    sh+=1;
}

//use try_lock
std::mutex m;
int sh; // shared data
// ...
void f()
{
    // ...
    std::unique_lock lck(m,std::defer_lock);  // make a lock, but don't acquire the mutex
    // ...
    if (lck.try_lock()) {
        // manipulate shared data:
        sh+=1;
    }
    else {
        // maybe do something else
    }
}

//(safely) trying to acquire two or more locks:
void f()
{
    // ...
    std::unique_lock lck1(m1,std::defer_lock);  // make locks but don't yet try to acquire the mutexes
    std::unique_lock lck2(m2,std::defer_lock);
    std::unique_lock lck3(m3,std::defer_lock);

    lock(lck1,lck2,lck3); //try_lock also could be used
    // manipulate shared data
}

备注:Lock用于辅助管理mutex,lock对象拥有一个mutex对象的引用,可以在构造时对mutex加锁,在析构时解锁,类似于智能指针对堆内存的管理,更好应对抛异常的场景。

Condition variables

条件变量(Condition Variable)是一种线程同步机制,通常与互斥锁(Mutex)一起使用。条件变量提供了一种线程间的通信机制,允许一个线程等待另一个线程满足某个条件后再继续执行。

条件变量是利用线程间共享的全局变量进行同步的一种机制,这个全局变量就是“条件”,同时还用到std::condition_variable和std::mutex。常用于生产者消费者场景,消费者线程等待全局变量达到某个条件,不满足条件则消费者线程阻塞;生产者线程使条件成立,然后调用notify接口唤醒消费者线程。生产者和消费者修改全局条件变量时,都必须持有mutex;在条件变量中只能使用std::unique_lock<std::mutex>。std::condition_variable的wait接口,实际依次完成3个动作:释放互斥锁,等待在条件变量上,再次获得互斥锁。

std::deque<int> q;  //全局条件变量
std::mutex mu;
std::condition_variable cond; 

void function_1() //生产者
{
    int count = 10;
    while (count > 0) 
    {
        std::unique_lock<std::mutex> locker(mu);
        q.push_front(count);
        locker.unlock();
        cond.notify_one();  // Notify one waiting thread, if there is one.
        std::this_thread::sleep_for(std::chrono::seconds(1));
        count--;
    }
}

void function_2() //消费者
{
    int data = 0;
    while (data != 1) 
    {
        std::unique_lock<std::mutex> locker(mu);
        while (q.empty())
            cond.wait(locker); // Unlock mu and wait to be notified
        data = q.back();
        q.pop_back();
        locker.unlock();
        std::cout << "t2 got a value from t1: " << data << std::endl;
    }
}

int main() 
{
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();
    return 0;
}

注意:在使用条件变量时,必须确保与互斥锁一起使用,以避免竞态条件的发生。 

Atomics

c++11引入了std::atomic<T>, 将某个变量声明为std::atomic<T>后, 通过std::atomic<T>的相关接口即可实现原子性的读写操作。

// 原子性的写入值
std::atomic<T>::store(T val, memory_order sync = memory_order_seq_cst);

// 原子性的读取值
std::atomic<T>::load(memory_order sync = memory_order_seq_cst);

// 原子性的增加,counter.fetch_add(1)等价于++counter
std::atomic<T>::fetch_add(T val, memory_order sync = memory_order_seq_cst);

// 原子性的减少,counter.fetch_sub(1)等价于--counter
std::atomic<T>::fetch_sub(T val, memory_order sync = memory_order_seq_cst);

// 原子性的按位与,counter.fetch_and(1)等价于counter &= 1
std::atomic<T>::fetch_and(T val, memory_order sync = memory_order_seq_cst);

// 原子性的按位或,counter.fetch_or(1)等价于counter |= 1
std::atomic<T>::fetch_or(T val, memory_order sync = memory_order_seq_cst);

// 原子性的按位异或,counter.fetch_xor(1)等价于counter ^= 1
std::atomic<T>::fetch_xor(T val, memory_order sync = memory_order_seq_cst);

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<bool> ready{false};
std::atomic<int> data{0};

void producer() 
{
    data.store(42, std::memory_order_relaxed); //原子性的更新data, 但是不保证内存顺序
    ready.store(true, std::memory_order_released); //保证data的更新先于ready的更新
}

void consumer() {
    // 保证先读取ready的值, 再读取data的值
    while (!ready.load(memory_order_acquire)) {   
        std::this_thread::yield(); // 啥也不做, 只是让出CPU时间片
    }

    // 当ready为true时, 再原子性的读取data的值
    std::cout << data.load(memory_order_relaxed);  // 4. 消费者线程使用数据
}

int main() {
    // launch一个生产者线程
    std::thread t1(producer); 
    // launch一个消费者线程
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

备注:atomic本身就是一种锁,它本身就实现了线程间同步。对C++中6个memory order的详细介绍见下一节。 

Concurrency memory model

 原文比较难以理解,可能因为本身就难一点吧,可从以下链接学习相关内容:

大白话C++之:一文搞懂C++多线程内存模型(Memory Order)_c++ memory order-CSDN博客

现代 C++ 对多线程/并发的支持(上) -- 节选自 C++ 之父的 《A Tour of C++》 - Zijian/TENG - 博客园 (cnblogs.com)

https://www.zhihu.com/question/24301047/answer/1193956492

这里涉及到CPU架构,缓存和内存体系,编译器对代码顺序的处理,执行时对代码顺序的处理,所以理解上有一点难度。下图时多核处理器中缓存和主存架构:

多线程中,线程之间的执行顺序一定不是确定的;只能在某些点同步它们,但是在任何其它的地方是没有办法保证绝对的执行顺序。

一个线程中的代码顺序,首先程序员编码有一个顺序,然后编译器根据优化等考虑生成一个二进制代码的顺序,CPU执行时由于多条指令并行,其实也有一个顺序。首先,编译器对代码可能进行指令重排;编译器编译之后(特别是开了优化之后)的代码执行顺序,不一定严格按照写代码的顺序。另外,当代CPU内部也有指令重排,CPU执行指令的顺序,也不见得是完全严格按照机器码的顺序。代CPU不仅是多核心,而且每个核心还是多任务(多指令)并行的,当代CPU的IPC(每时钟执行指令数)一般都远大于1,也就是所谓的多发射,很多命令都是同时执行的。

多线程之间,由于同步的引入,线程之间的代码相对同步点,形成了一定的先后顺序。

C++11所规定的6种模式,其实并不是限制(或者规定)两个线程该怎样同步执行,而是在规定一个线程内的指令该怎样执行,其实讨论的是单线程的问题。更为准确地说,是在讨论单线程内的指令执行顺序对于多线程的影响的问题。

C++ 提供了适当的内存模型(memory model)和一组原子操作(atomic operation),以支持在同一地址空间内并发执行多个线程。原子操作使得无锁编程成为可能。内存模型保证了在避免数据竞争(data races,不受控地同时访问可变数据)的前提下,一切按照预期工作。

所谓的memory order,其实就是限制编译器以及CPU对单线程当中的指令执行顺序进行重排的程度(此外还包括对cache的控制方法)。这种限制,决定了以atom操作为基准点(边界),对其之前的内存访问命令,以及之后的内存访问命令,能够在多大的范围内自由重排(或者反过来,需要施加多大的保序限制)。从而形成了C++11中的6种模式。它本身与多线程无关,是限制单一线程当中指令执行顺序。但是(合理的)指令执行顺序的重排在单线程环境下不会造成逻辑错误而在多线程环境下会,所以这个问题的目的是为了解决多线程环境下出现的问题。

内存顺序是指在并发编程中, 对内存读写操作的执行顺序。这个顺序可以被编译器和处理器进行优化, 可能会与代码中的顺序不同, 这被称为指令重排。

memory_order_relaxed: 确保操作原子性,多个原子操作是有序的,不对内存顺序做任何保证。

memory_order_consume这个内存序非常特殊,主流编译器直接将它当成memory_order_acquire处理。“C++ Concurrency In Action”(第二版,2019)一书作者认为memory_order_consume不应该出现在实际的代码中,即使在C++17中也不推荐使用。

memory_order_release: 用于写操作, 比如std::atomic::store(T, memory_order_release), 会在写操作之前插入一个StoreStore屏障, 确保屏障之前的所有操作不会重排到屏障之后。

memory_order_acquire: 用于读操作, 比如std::atomic::load(memory_order_acquire), 会在读操作之后插入一个LoadLoad屏障, 确保屏障之后的所有操作不会重排到屏障之前.

memory_order_acq_rel: 等效于memory_order_acquire和memory_order_release的组合, 同时插入一个StoreStore屏障与LoadLoad屏障. 用于读写操作, 比如std::atomic::fetch_add(T, memory_order_acq_rel).

memory_order_seq_cst: 最严格的内存顺序, 在memory_order_acq_rel的基础上, 保证所有线程看到的内存操作顺序是一致的。memory_order_seq_cst会带来最大的性能开销了, 相比其他的memory_order来说, 因为相当于禁用了CPU Cache. memory_order_seq_cst常用于multi producer - multi consumer的场景。

备注:cache和多核的概念对理解上述内容很重要,对内存序memory order,很多人以为它们是限制多线程之间的执行顺序,其实不是,只是限制单个线程内的执行顺序和执行到cache还是memory,先跳出多线程语境来理解,再看对多线程的影响。 

Threads

//A thread is launched by constructing a std::thread 
//with a function or a function object (incl. a lambda):
#include <thread>
   
void f();
struct F {
    void operator()();
};

int main()
{
    std::thread t1{f};  // f() executes in separate thread
    std::thread t2{F()};    // F()() executes in separate thread
    t1.join();  // wait for t1
    t2.join();  // wait for t2
}

void f(vector<double>&);
struct F {
    vector<double>& v;
    F(vector<double>& vv) :v{vv} { }
    void operator()();
};

int main()
{
    std::thread t1{std::bind(f,some_vec)};  // f(some_vec) executes in separate thread
    std::thread t2{F(some_vec)};        // F(some_vec)() executes in separate thread
    t1.join();
    t2.join();
}

void f(vector<double>&, double* res);   // place result in res
struct F {
    vector<double>& v;
    double* res;
    F(vector<double>& vv, double* p) :v{vv}, res{p} { }
    void operator()();  // place result in res
};

int main()
{
    double res1;
    double res2;
    std::thread t1{std::bind(f,some_vec,&res1)}; //f(some_vec,&res1) executes in separate thread
    std::thread t2{F(some_vec,&res2)}; //F(some_vec,&res2)() executes in separate thread
    t1.join();
    t2.join();
    std::cout << res1 << ' ' << res2 << '\n';
}

 Futures and promises

异步调用,就是当前线程将一个任务交给另外一个线程来进行执行,当前线程会接着执行当前任务,不需要等待这个交付给其他线程执行的任务的结果,直到其在未来的某一个时刻需要使用这个任务执行结果数据的时候。

创建一个异步调用,然后其他线程执行这个异步调用,异步调用执行完成之后会返回一个结果,然后异步调用创建方在需要使用这个异步调用结果的时候,通过某种方式来获取这个异步调用结果。C++针对上述需求,提供了一些类对象来完成上面的过程。

异步调用创建的时候,会返回一个std::future对象实例给异步调用创建方。异步调用执行方持有std::promise对象实例。双方持有的std::promise对象实例和std::future对象实例分别连接一个共享对象,这个共享对象在异步调用创建方和异步调用执行方之间构建了一个信息同步的通道(channel),双方通过这个通道进行异步调用执行情况的信息交互。

//C++11 provides three kinds of futures, future for most simple uses, and shared_future and atomic_future for some trickier cases.
//If we have a future<X> called f, we can get() a value of type X from it:
X v = f.get();  // if necessary wait for the value to get computed

//We might not want to wait for a result, so we can ask the future if a result has arrived:
if (f.wait_for(0)) {    // there is a value to get()
    // do something
}
else {
    // do something else
}

//the main purpose of future is to provide that simple get(). 
//The main purpose of promise is to provide a simple set() to match future’s get().

//The packaged_task type is provided to simplify launching a thread to execute a task. 
//In particular, it takes care of setting up a future connected to a promise and 
//to provides the wrapper code to put the return value or exception from the task into the promise. 
//For example:
double comp(vector<double>& v)
{
    // package the tasks:
    // (the task here is the standard accumulate() for an array of doubles):
    packaged_task<double(double*,double*,double)> pt0{std::accumulate<double*,double*,double>};

    packaged_task<double(double*,double*,double)> pt1{std::accumulate<double*,double*,double>};

    auto f0 = pt0.get_future(); // get hold of the futures
    auto f1 = pt1.get_future();
    pt0(&v[0],&v[v.size()/2],0);    // start the threads
    pt1(&v[v.size()/2],&v[size()],0);
    return f0.get()+f1.get();   // get the results
}

std::promise 对象可以保存某一类型T的值,该值可被future对象读取(可能在另外一个线程中)。可以通过 get_future 来获取与该 std::promise 对象相关联的 std::future 对象,调用该函数之后,两个对象共享相同的共享状态。std::promise 对象是异步 Provider,它可以在某一时刻设置共享状态的值。std::future 对象可以异步返回共享状态的值,或者在必要的情况下阻塞调用者并等待共享状态标志变为 ready,然后才能获取共享状态的值。 

//------------------ example 1 ------------------ 
#include <iostream>       
#include <functional>     
#include <thread>        
#include <future>     // std::promise, std::future

void print_int(std::future<int>& fut) {
    int x = fut.get();                    // 获取共享状态的值.
    std::cout << "value: " << x << '\n';  // 打印 value: 10.
}

int main ()
{
    std::promise<int> prom;                    // 生成一个 std::promise<int> 对象.
    std::future<int> fut = prom.get_future();  // 和 future 关联.
    std::thread t(print_int, std::ref(fut));   // 将 future 交给另外一个线程t.
    prom.set_value(10);                        // 设置共享状态的值, 此处和线程t保持同步.
    t.join();
    return 0;
}

//------------------ example 2 ------------------ 
#include <iostream>      
#include <functional>    
#include <thread>        
#include <future>         
#include <exception>    // std::exception, std::current_exception

void get_int(std::promise<int>& prom) {
    int x;
    std::cout << "Please, enter an integer value: ";
    std::cin.exceptions (std::ios::failbit);   // throw on failbit
    try {
        std::cin >> x;                         // sets failbit if input is not int
        prom.set_value(x);
    } catch (std::exception&) {
        prom.set_exception(std::current_exception());
    }
}

void print_int(std::future<int>& fut) {
    try {
        int x = fut.get();
        std::cout << "value: " << x << '\n';
    } catch (std::exception& e) {
        std::cout << "[exception caught: " << e.what() << "]\n";
    }
}

int main ()
{
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    std::thread th1(get_int, std::ref(prom));
    std::thread th2(print_int, std::ref(fut));

    th1.join();
    th2.join();

    return 0;
}

备注:实际编程中,尽量不要直接使用future和promise,使用下面的Async更好(但有限制)。 

Async 

template<class T, class V> struct Accum  {  // simple accumulator function object
    T* b;
    T* e;
    V val;
    Accum(T* bb, T* ee, const V& v) : b{bb}, e{ee}, val{vv} {}
    V operator() () { return std::accumulate(b,e,val); }
};
    
double comp(vector<double>& v)    // spawn many tasks if v is large enough
{
    if (v.size()<10000) return std::accumulate(v.begin(),v.end(),0.0);
    auto f0 {async(Accum{&v[0],&v[v.size()/4],0.0})};
    auto f1 {async(Accum{&v[v.size()/4],&v[v.size()/2],0.0})};
    auto f2 {async(Accum{&v[v.size()/2],&v[v.size()*3/4],0.0})};
    auto f3 {async(Accum{&v[v.size()*3/4],&v[v.size()],0.0})};
    return f0.get()+f1.get()+f2.get()+f3.get();
}

备注:使用了共享资源、需要上锁的任务无法使用 async()。async()完成线程创建和join等操作,async()的返回值是一个future,future的get()接口会等待线程结束。C#等语言中也有类似功能的async()接口。

总结和感受

1,C++/C#/Java等语言在一定程度上是相通的,相互学习,相互促进。

2,编程语言在与时俱进,程序员也要跟上,积极使用一些新的特性。

3,文中处处可见编程语言与编译器密切相关,深入理解编译链接等原理,有助于学好用好语言。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值