文章

C++11新特性

C++11新特性

该文记录《深入理解C++11:C++11新特性解析与应用》读后记录。

C++11新特性

类作者 库作者 所有人 部分人

1 新标准的诞生

1.1 C++11标准的诞生

2011年11月C++11标准被C++标准委员会批准通过。2011年也通过了C11标准。

  • WG14: C标准委员会
  • WG21: C++标准委员会

2 保证稳定性和兼容性

2.1 保持与C99兼容

部分人

C++11将对以下C99特性的支持也都纳入了新标准中:

  • C99中的预定义宏
  • __func__预定义标识符
  • _Pragma_操作符
  • 不定参数宏定义以及__VA_ARGS__
  • 宽窄字符串连接
宏名称功能描述
__STDC_HOSTED__如果编译器的目标系统环境中包含完整的标准C库,那么这个宏就定义为1,否则值为0
__STDC__C编译器通常用这个宏的值来表示编译器的实现是否和C标准一致。C++11标准中这 个宏是否定义以及定成什么值由编译器来决定
__STDC_VERSION__C编译器通常用这个宏来表示所支持的C标准的版本,比如1999mmL。C++11标准中 这个宏是否定义以及定成什么值将由编译器来决定
__STDC_ISO_10646__这个宏通常定义为一个yyyymmL格式的整数常最,例如199712L,用来表示C++编 译环境符合某个版本的ISO/IEC 10646标准
宏名称功能描述
__FILE__文件名称
__LINE__当前行数
__func__函数名称

在C++11中,标准定义了与预处理指令 #pragma 功能相同的操作符 _Pragmao,使用方式_Pragma (字符串字面量),示例 _Pragma("once");

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

#define LOG(...)                                              \
    {                                                         \
        fprintf(stderr, "%s: Line%d:\t", __FILE__, __LINE__); \
        fprintf(stderr, __VA_ARGS__);                         \
        fprintf(stderr, "\n");                                \
    }

int main()
{
    int x = 3;
    LOG("x = %d", x); // 2-1-5.cpp: Line 13:    x = 3
}
// 编译选项:g++ -std=c++11 2-1-5.cpp

2.2 long long 整型

部分人

标准要求 long long 不同平台不同长度,但至少有64位。

long long 等价 signed long longlong long intsinged long long int

unsigned long long 等价 unsigned long long int

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <climits>
#include <iostream>
using namespace std;

int main()
{
    long long          ll  = -9LL;
    unsigned long long ull = -9ULL;

    printf("min of long long: %lld\n", LLONG_MIN);           // -9223372036854775808
    printf("max of long long: %lld\n", LLONG_MAX);           // 9223372036854775807
    printf("max of unsigned long long: %llu\n", ULLONG_MAX); // 18446744073709551615

    return 0;
}

2.3 扩展的整型

部分人

UINT__int16u64int64_u 等都是编译器的自行扩展整型。

1
2
3
4
5
6
// C++11 标准有符号整型,无符号具有相同存储空间大小
signed char
short int
int
long int
long long int

当运算、传参等类型不匹配的时候,整型间会发生隐式的转换,这个过程通常被称为整型的提升(Integral promotion),原则:

  • 长度越大的整型等级越高,比如 long long int 的等级会高于 int

  • 长度相同的情况下,标准整型的等级高于扩展类型,比如 long long int_int64 如果都是64位长度,则 long long int 类型的等级更高。

  • 相同大小的有符号类型和无符号类型的等级相同,long long intunsigned long long int 的等级就相同。

2.4 宏_cplusplus

部分人

__cplusplus 宏通常被定义为一个整型值,而且随着标准变化,__cplusplus 一般会是一个比以往标准更大的值。

C++ 标准_cplusplus
C++98 pre1L
C++98199711L
C++03199711L
C++11201103L
C++14201402L
C++17201703L

2.5 静态断言

库作者

2.5.1 断言:运行时与预处理时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cassert> // <assert.h>
#include <iostream>
using namespace std;

void Div(int a, const int b)
{
    assert(b > 0);
    printf("%d / %d = %d", a, b, a / b);
}

int main()
{
    Div(2, 0);
    return 0;
}

编译运行后就会报错结果如下,只有在运行时才能显示错误。

1
2
a.out: format.cpp:7: int Div(int, int): Assertion `b > 0' failed.
Aborted 

2.5.2 静态断言

static_assert(常量表达式,"提示字符串")

  • 使用范围:static_assert 可以用在全局作用域中,命名空间中,类作用域中,函数作用域中,几乎可以不受限制的使用。

  • 常量表达式:static_assert 的断言表达式的结果必须是在编译时期可以计算的表达式,即必须是常量表达式。
  • 模板参数:编译器在遇到一个 static_assert 语句时,通常立刻将其第一个参数作为常量表达式进行演算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <cassert> // <assert.h>
#include <iostream>
using namespace std;

// void Div(int a, const int b)
// {
//     static_assert(b > 0, "b must >0"); // 运行时调用,编译不过
//     printf("%d / %d = %d", a, b, a / b);
// }

template <class T, int Size>
class Vector
{
    static_assert(Size > 3, "Vector size is too small!");
    T m_values[Size];
};

int main()
{
    Vector<int, 4> v1;
    Vector<int, 2> v2;
    return 0;
}

编译时就会报错结果如下。

1
2
3
4
format.cpp: In instantiation of ‘class Vector<int, 2>’:
format.cpp:21:20:   required from here
format.cpp:14:24: error: static assertion failed: Vector size is too small!
   14 |     static_assert(Size > 3, "Vector size is too small!");

2.6 noexcept

库作者

使用 noexcept 表明函数或操作不会发生异常,会给编译器更大的优化空间。以下情形鼓励使用 noexcept

  • 移动构造函数(move constructor)
  • 移动分配函数(move assignment)
  • 析构函数(destructor)。析构函数是默认加上关键字 noexcept(true) 的。
  • 叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。
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
#include <iostream>
using namespace std;

struct A
{
    ~A() { throw 1; }
};
struct B
{
    ~B() noexcept(false) { throw 2; }
};
struct C
{
    B b;
};

int funA() { A a; }
int funB() { B b; }
int funC() { C c; }

int main()
{
    try
    {
        funA(); // terminate called after throwing an instance of 'int'
    }
    catch (...)
    {
        cout << "caught funA." << endl;
    }

    try
    {
        funB();
    }
    catch (...)
    {
        cout << "caught funB." << endl; // caught funB.
    }

    try
    {
        funC();
    }
    catch (...)
    {
        cout << "caught funC." << endl; // caught funC.
    }
}

2.7 快速初始化成员变量

部分人

C++98 中必须是静态且常量性成员才能初始化,且只能是整型或枚举。否则必须在构造函数中初始化。

C++11 对常见类型可以直接初始化,对于封装类型需要使用列表初始化。

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
#include <iostream>
using namespace std;

class Mem
{
private:
    int m;

public:
    Mem(int i) : m(i){};
};

class Group
{
private:
    int    data = 1;
    Mem    mem{0};			// 列表初始化
    string name{"Group"};

public:
    Group() {}					// 内部自动初始化
    Group(int a) : data(a) {}
    Group(Mem m) : mem(m) {}
    Group(int a, Mem m, string n) : data(a), mem(m), name(n) {}
};

int main()
{
    Group g1;
    Group g2(1);
    Group g3(Mem(1));
    Group g4(1, Mem(1), "1");
    return 0;
}

2.8 非静态成员的sizeof

部分人
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

struct People
{
public:
    int            hand;
    static People* all;
};

int main()
{
    People p;
    std::cout << sizeof(p.hand) << std::endl;       // C++98中通过,C++11中通过
    std::cout << sizeof(People::all) << std::endl;  // C++98中通过,C++11中通过
    std::cout << sizeof(People::hand) << std::endl; // C++98中错误,C++11中通过
    return 0;
}

C++98 实现同等功能,强制转换0为一个People类的指针,继而通过指针的解引用获得其成员变量, 并用 sizeof 求得该成员变量的大小。

1
sizeof(((People*)0)->hand);

2.9 扩展的friend

部分人

friend 关键字用于声明类的友元,友元可以无视类中成员的属性。无论成员是 public, protected 或是private 的,友元类或友元函数都可 以访问,这就完全破坏了面向对象编程中封装性的概念。

C++11 友元不需要 class 且能够实现类模板声明友元。

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
#include <iostream>
using namespace std;

template <typename T>
class People
{
    friend T;   // 注释掉编译不通过

private:
    int    age  = 18;
    string name = "none";
};

class Test
{
public:
    void show(People<Test>& pt)
    {
        std::cout << pt.age << std::endl;
        std::cout << pt.name << std::endl;
    }
};

int main()
{
    People<Test> pt;
    Test         t;
    t.show(pt);
    return 0;
}

2.10 final/override

部分人

一个类A中声明的虚函数 fun 在其派生类B中再次被定义,且B中的函数 fun 跟 A中 fun 的原型一样(函数名、参数列表等一样),那么我们就称B重载(overload ) 了A的 fun 函数。

  • final 关键字的作用是使派生类不可覆盖它所修饰的虚函数。
  • override 关键字使用后,该函数必须重载其基类中的同名函数,否则代码无法通过编译。
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
#include <iostream>
using namespace std;

class Base
{
public:
    virtual void fun()  = 0;
    virtual void fun2() = 0;
    virtual void fun3() const;
    virtual void fun4(double d) = 0;
    void         fun5();
};

class A : public Base
{
public:
    void fun() final;          // 声明为 final
    void fun2() override;      // 声明为 override
    void fun3() override;      // 无法通过编译,常量性不一致
    void fun4(int i) override; // 无法通过编译,参数不一致
    void fun5() override;      // 无法通过编译,非虚函数重载
};

class B : public A
{
public:
    // void fun(); // 无法通过编译 virtual function ‘virtual void B::fun()’ overriding final function
};

int main()
{
    return 0;
}

2.11 模板函数的默认参数

所有人

模板函数可以拥有默认参数,类模板在为多个默认模板参数声明指定默认值时必须遵守 “从右往左”的规则指定,函数模板可以不用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <typeinfo>
using namespace std;

template <typename T, typename U = double>
void fun(T t = 0, U u = 0)
{
    std::cout << typeid(t).name() << " " << typeid(u).name() << std::endl;
}

int main()
{
    fun(1, 'c');      // fun<int, char>(1, 'c')
    fun(1);           // fun<int, double>(1, 0),使用默认模板参数 double
    // fun();         // 无法推导
    fun<int>();       // fun<int, double>(0, 0),使用默认模板参数 double
    fun<int, char>(); // fun<int, char>(0, 0)
    return 0;
}

2.12 外部模板

部分人

同一模板函数在多个文件中实例化就会导致生成多份链接导致失败。

使用 extern 关键字,可以避免同一模板多次实例化。

image-20220529135723920

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <typeinfo>
using namespace std;

template <typename T>
void fun(T t)
{
    std::cout << t << std::endl;
}

// 外部模板声明
extern template 
void fun<int>(int i);

int main()
{
    return 0;
}

2.13 局部和匿名类型作为模板实参

部分人

C++11 可以将局部内型和匿名的类作为模板类的实参。

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
#include <iostream>
using namespace std;

template <typename T>
class X
{};

template <typename T>
void TempFun(T t){};

struct A
{
} a;
struct
{
    int i;
} b; // b 是匿名类型变量
typedef struct
{
    int i;
} B; // B 是匿名类型

int main()
{
    struct C
    {
    } c; // c 是局部变量

    X<A> x1;    // C++98 通过,C++11通过
    X<B> x2;    // C++98 错误,C++11通过
    X<C> x3;    // C++98 错误,C++11通过
    TempFun(a); // C++98 通过,C++11通过
    TempFun(b); // C++98 错误,C++11通过
    TempFun(c); // C++98 错误,C++11通过

    return 0;
}

3 通用为本,专用为末

3.1 继承构造函数

类作者

类具有可派生性,派生类可以自动获得基类的成员变量和接口(虚函数和纯虚函数,指 public 派生),基类的非虚函数则无法再被派生类使用。

可以通过 using 声明透传构造函数。将基类中的构造函数全继承到派生类中,这是隐式声明继承的。

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
#include <iostream>
using namespace std;

struct Base
{
    int    a;
    double b;
    string c;
    Base(int i) : a(i) {}
    Base(int i, double d) : a(i), b(d) {}
    Base(int i, double d, const char* c) : a(i), b(d), c(c) {}
    //...等等系列的构造函数版本号
};
struct Derive : Base
{
    using Base::Base; // 继承构造函数
    int d{0};
};

int main()
{
    Derive d(1, 2, "3");
    std::cout << d.a << std::endl; // 1
    std::cout << d.b << std::endl; // 2
    std::cout << d.c << std::endl; // 3
    std::cout << d.d << std::endl; // 0
    return 0;
}
  • 对于继承构造函数来说,參数的默认值是不会被继承的,并且默认值会导致基类产生多个构造函数版本号(即參数从后一直往前面减。直到包括无參构造函数,当然假设是默认复制构造函数也包括在内),这些函数版本号都会被派生类继承。
  • 继承构造函数中的冲突处理:当派生类拥有多个基类时,多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名。一个解决的办法就是显式的继承类的冲突构造函数。阻止隐式生成对应的继承构造函数,以免发生冲突。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct A
{
    A(int) {}
};
struct B
{
    B(int) {}
};
struct C : A, B
{
    using A::A;
    using B::B;
    C(int) {}
};
  • 假设基类的构造函数被声明为私有构造函数或者派生类是从基类虚继承的,那么就不能在派生类中声明继承构造函数。
  • 假设一旦使用了继承构造函数,编译器就不会为派生类生成默认构造函数。

3.2 委派构造函数

类作者

委托构造函数(Delegating Constructor)由C++11引入,是对C++构造函数的改进,允许构造函数通过初始化列表调用同一个类的其他构造函数,目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。

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>
using namespace std;

class Time
{
public:
    int hours   = 0;
    int minutes = 0;
    int seconds = 0;

public:
    Time() {}
    Time(int hour) : hours(hour) {}
    Time(int hour, int minute) : Time(hour) { minutes = minute; }
    Time(int hour, int minute, int second) : Time(hour, minute) { seconds = second; }
};

int main()
{
    Time t(1, 2, 3);
    std::cout << t.hours << std::endl;   // 1
    std::cout << t.minutes << std::endl; // 2
    std::cout << t.seconds << std::endl; // 3
    return 0;
}
  • 委托构造的链状关系不能形成委托环(delegation cycle)
  • 委托构造函数不能在后面追加其他成员初始化表达式,需要写到大括号中初始化

3.3 右值应用:移动语义和完美转发

类作者

3.3.1 指针成员与拷贝构造

浅拷贝深拷贝

3.3.2 移动构造

“偷走”临时变量中资源的构造函数,就被称为“移动构造函数”(移动语义,move semantics),白话文“移为己用”。

img

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>
using namespace std;

class HasPtrMem
{
public:
    int*       d;
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
    static int n_mvtr;

public:
    HasPtrMem() : d(new int(3)) { std::cout << "Construct: " << ++n_cstr << std::endl; }
    HasPtrMem(const HasPtrMem& h) : d(new int(*h.d)) { std::cout << "Copy construct: " << ++n_cptr << std::endl; }
    HasPtrMem(HasPtrMem&& h) : d(h.d)
    {
        h.d = nullptr; // 将临时值的指针成员置空
        std::cout << "Move construct: " << ++n_mvtr << std::endl;
    }
    ~HasPtrMem()
    {
        delete d;
        std::cout << "Destruct: " << ++n_dstr << std::endl;
    }
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;

HasPtrMem GetTemp()
{
    HasPtrMem h;
    std::cout << "Resource from " << __func__ << ": " << h.d << std::endl;
    return h;
}

int main()
{
    HasPtrMem a = GetTemp();
    std::cout << "Resource from " << __func__ << ": " << a.d << std::endl;
    return 0;
}
// g++ format.cpp -std=c++11 -fno-elide-constructors

3.3.3 左值、右值与右值引用

1
2
3
4
5
6
7
8
9
1 分类1
int a = b + c;
左值:出现在等号左边
右值:出现在等号右边

2 分类2(C++11)
左值
将亡值
纯右值

image-20220529202048892

3.3.4 std::move 强制转化为右值

<utility> 中提供 std::move,唯一功能是将一个左值强制转换为右值引用,继而通过右值引用使用该值宜用于移动语义,等同于 static_cast<T&&>(lvalue)

3.3.6 完美转发 std::forward

模板函数转发到目标函数时,常量左值虽然万能,但是目标函数无法接受常量左值引用作为参数。

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
#include <iostream>
using namespace std;

void RunCode(int&& m) { std::cout << "rvalue ref" << std::endl; }
void RunCode(int& m) { std::cout << "lvalue ref" << std::endl; }
void RunCode(const int&& m) { std::cout << "const rvalue ref" << std::endl; }
void RunCode(const int& m) { std::cout << "const lvalue ref" << std::endl; }

template <typename T>
void PerfectForward(T&& t)
{
    RunCode(forward<T>(t));
}

int main()
{
    int       a = 1;
    int       b = 2;
    const int c = 3;
    const int d = 4;
    PerfectForward(a);       // lvalue ref
    PerfectForward(move(b)); // rvalue ref
    PerfectForward(c);       // const lvalue ref
    PerfectForward(move(d)); // const rvalue ref
    return 0;
}

3.4 显示转换操作符

库作者

C++11 显示类型转换 explicit 的使用范围扩展到了自定义的类型转换操作符上。

struct B 将不能使用拷贝构造初始化。

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
#include <iostream>
using namespace std;

struct A
{
    A(int) { }      // converting constructor
    A(int, int) { } // converting constructor (C++11)
    operator bool() const { return true; }
};
 
struct B
{
    explicit B(int) { }
    explicit B(int, int) { }
    explicit operator bool() const { return true; }
};
 
int main()
{
    A a1 = 1;      // OK: copy-initialization selects A::A(int)
    A a2(2);       // OK: direct-initialization selects A::A(int)
    A a3 {4, 5};   // OK: direct-list-initialization selects A::A(int, int)
    A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)
    A a5 = (A)1;   // OK: explicit cast performs static_cast
    if (a1) ;      // OK: A::operator bool()
    bool na1 = a1; // OK: copy-initialization selects A::operator bool()
    bool na2 = static_cast<bool>(a1); // OK: static_cast performs direct-initialization
 
//  B b1 = 1;      // error: copy-initialization does not consider B::B(int)
    B b2(2);       // OK: direct-initialization selects B::B(int)
    B b3 {4, 5};   // OK: direct-list-initialization selects B::B(int, int)
//  B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
    B b5 = (B)1;   // OK: explicit cast performs static_cast
    if (b2) ;      // OK: B::operator bool()
//  bool nb1 = b2; // error: copy-initialization does not consider B::operator bool()
    bool nb2 = static_cast<bool>(b2); // OK: static_cast performs direct-initialization
}

3.5 列表初始化

所有人

3.5.1 初始化列表

  • C++11 能够对类成员快速就地初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <map>
#include <vector>
using namespace std;

int main()
{
    int              a[] = {1, 2, 3};                    // C++98通过,C++11通过
    int              b[]{4, 5, 6};                       // C++98失败,C++11通过
    vector<int>      c{1, 2, 3};                         // C++98失败,C++11通过
    map<int, double> d = {
        {1, 1.1}, 
        {2, 2.2}, 
        {3, 3.3}
    }; // C++98失败,C++11通过
}
  • C++11 能够自定义类使用初始化列表,构造函数使用 <initializer_list>
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
#include <initializer_list>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

enum Gender
{
    MALE,
    FEMALE
};
class People
{
private:
    vector<pair<string, Gender>> data;

public:
    People(initializer_list<pair<string, Gender>> list)
    {
        auto it = list.begin();
        for (; it != list.end(); it++)
        {
            data.push_back(*it);
            std::cout << "name: " << it->first << "\t gender: " << it->second << std::endl;
        }
    }
};

int main()
{
    People peo = {
        {"Marry", MALE}, 
        {"Tom", MALE}, 
        {"Judy", FEMALE}
    };
    return 0;
}

3.5.2 防止类型收窄

使用列表初始化其中优势之一可以防止类型收窄(narrowing)。类型收窄一般是指一些可以使得数据变化或者精度丢失的隐式类型转换。可能导致类型收窄的典型情况如下:

  • 从浮点数隐式地转化为整型数。比如:int a= 1.2 这里a实际保存的值为整数1,可 以视为类型收窄。
  • 从高精度的浮点数转为低精度的浮点数,比如从 long double 隐式地转化为 double 或从 double 转为 float。如果这些转换导致精度降低,都可以视为类型收窄。
  • 从整型(或者非强类型的枚举)转化为浮点型,如果整型数大到浮点数无法精确地表 示,则也可以视为类型收窄。
  • 从整型(或者非强类型的枚举)转化为较低长度的整型,比如:unsigned char = 1024, 1024明显不能被一般长度为8位的 unsigned char 所容纳,所以也可以视为类型收窄。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int main()
{
    const int     x = 1024;
    const int     y = 10;
    char          a = x;               // 收窄,但可以通过编译
    char*         b = new char(1024);  // 收窄,但可以通过编译
    // char          c = {x};             // 收窄,无法通过编译
    char          d = {y};             // 可以通过编译
    // unsigned char e{-1};               // 收窄,无法通过编译
    float         f{7};                // 可以通过编译
    // int           g{2.0};              // 收窄,无法通过编译
    // float*        h = new float{1e48}; // 收窄,无法通过编译
    float         i = 1.21;            // 可以通过编译
}

3.6 POD类型

部分人

POD类型是C++中常见的概念,用来说明类/结构体的属性,具体来说它是指没有使用面相对象的思想来设计的类/结构体。

POD类型在C++中有两个独立的特性:

  • 支持静态初始化(static initialization)
  • 拥有和C语言一样的内存布局(memory layout)

POD 的全称是Plain Old DataPlain表明它是一个普通的类型,没有虚函数虚继承等特性;Old表明它与C兼容。C++11将 POD 划分为两个基本概念的合集,即:平凡的(trivial classes )和 标准布局(standard-layout)。

3.6.1 平凡

  1. 拥有平凡的默认构造函数和析构函数:不定义类的构造函数,编译器会为我们生成一个平凡的默认构造函数,而一旦定义即使构造函数什么也没声明,构造函数都不是平凡的;析构函数同理。
1
2
// 第七章,使用 =default 关键字显示声明缺省版本的构造函数,从而使得类型恢复“平凡化”
struct NoTrivial { NoTrivial(); };
  1. 拥有平凡的拷贝构造函数和移动构造函数:平凡拷贝构造函数等同于 memcpy 进行类型的构造,同上述。
  2. 拥有平凡的拷贝赋值运算符和移动赋值运算符:同上述。
  3. 不包含虚函数以及虚基类。

类模板 is_trivial 的成员 value 可用于判断 T 的类型是否是一个平凡的类型。除了类和结构体还可以对内置的标量类型数据及数组类型进行判断。

1
template <typename T> struct std::is_trivial;
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <type_traits>
using namespace std;

struct Trivial1
{};
struct Trivial2
{
public:
    int a;

private:
    int b;
};
struct Trivial3
{
    Trivial1 a;
    Trivial2 b;
};
struct Trivial4
{
    Trivial2 a[23];
};
struct Trivial5
{
    int        x;
    static int y;
};

struct NoTrivial1
{
    NoTrivial1() : z(42) {}
    int z;
};
struct NoTrivial2
{
    NoTrivial2();
    int w;
};
NoTrivial2::NoTrivial2() = default;
struct Trivial22
{
    Trivial22() = default;
    int w;
};
struct NoTrivial3
{
    Trivial5     c;
    virtual void f();
};

int main()
{
    std::cout << std::is_trivial<Trivial1>::value << std::endl;   // 1
    std::cout << std::is_trivial<Trivial2>::value << std::endl;   // 1
    std::cout << std::is_trivial<Trivial3>::value << std::endl;   // 1
    std::cout << std::is_trivial<Trivial4>::value << std::endl;   // 1
    std::cout << std::is_trivial<Trivial5>::value << std::endl;   // 1
    std::cout << std::is_trivial<NoTrivial1>::value << std::endl; // 0
    std::cout << std::is_trivial<NoTrivial2>::value << std::endl; // 0
    std::cout << std::is_trivial<Trivial22>::value << std::endl;  // 1
    std::cout << std::is_trivial<NoTrivial3>::value << std::endl; // 0
    return 0;
}

3.6.2 标准布局

  1. 所有非静态成员有相同的访问权限。
  2. 在类或者结构体继承时:
    1. 派生类中有非静态成员,且只有一个仅包含静态成员的基类。
    2. 基类成员有非静态成员,而派生类没有非静态成员。
  3. 类中第一个非静态成员的类别与其基类不同。
  4. 没有虚函数和虚基类
  5. 所有非静态数据成员均符合标准布局类型,其基类也符合标准布局。

同理,类模板 is_standard_layout 的成员 value 可以打印出类型的标准布局属性。

1
template <typename T> struct std::is_standard_layout;
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <type_traits>
using namespace std;

struct SLayout1
{};

struct SLayout2
{
private:
    int x;
    int y;
};

struct SLayout3 : SLayout1
{
    int  x;
    int  y;
    void f();
};

struct SLayout4 : SLayout1
{
    int      x;
    SLayout1 y;
};

struct SLayout5
    : SLayout1
    , SLayout2
{};

struct SLayout6
{
    static int y;
};

struct SLayout7 : SLayout6
{
    int x;
};

struct NonSLayout1 : SLayout1
{
    SLayout1 x;
    int      i;
};

struct NonSLayout2 : SLayout2
{
    int z;
};

struct NonSLayout3 : NonSLayout2
{};

struct NonSLayout4
{
public:
    int x;

private:
    int y;
};

int main()
{
    std::cout << std::is_standard_layout<SLayout1>::value << std::endl;    // 1
    std::cout << std::is_standard_layout<SLayout2>::value << std::endl;    // 1
    std::cout << std::is_standard_layout<SLayout3>::value << std::endl;    // 1
    std::cout << std::is_standard_layout<SLayout4>::value << std::endl;    // 1
    std::cout << std::is_standard_layout<SLayout5>::value << std::endl;    // 1
    std::cout << std::is_standard_layout<SLayout6>::value << std::endl;    // 1
    std::cout << std::is_standard_layout<SLayout7>::value << std::endl;    // 1
    std::cout << std::is_standard_layout<NonSLayout1>::value << std::endl; // 0
    std::cout << std::is_standard_layout<NonSLayout2>::value << std::endl; // 0
    std::cout << std::is_standard_layout<NonSLayout3>::value << std::endl; // 0
    std::cout << std::is_standard_layout<NonSLayout4>::value << std::endl; // 0
    return 0;
}

3.6.3 POD判断

同理,类模板 is_pod 的成员 value 可以判断一个类型是否是 POD

1
template <typename T> struct std::is_pod;
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 <type_traits>
using namespace std;

union U
{};
union U1
{
    U1() {}
};
enum E
{};
typedef double* DA;
typedef void (*PF)(int, double);

int main()
{
    std::cout << std::is_pod<U>::value << std::endl;   // 1
    std::cout << std::is_pod<U1>::value << std::endl;  // 0
    std::cout << std::is_pod<E>::value << std::endl;   // 1
    std::cout << std::is_pod<int>::value << std::endl; // 1
    std::cout << std::is_pod<DA>::value << std::endl;  // 1
    std::cout << std::is_pod<PF>::value << std::endl;  // 1
    return 0;
}

3.6.4 POD好处

  1. 字节赋值:代码中可以安全的使用 memsetmemcpyPOD 类型进行初始化和拷贝等操作。
  2. 提供对 C 内存布局兼容:C++ 程序可以与 C 函数进行相互操作,因为 POD 类型的数据在 C 和 C++ 间操作总是安全的。
  3. 保证静态初始化的安全有效:静态初始化在很多时候能提高程序的性能,而 POD 类型的对象初始化往往更加简单。(静态初始化:指的是在编译时期就讲某一些对象进行了初始化;动态初始化:运行的时候才去进行初始化)

3.7 非受限联合体

部分人

C/C++ 中联合体(Union)是一种构造类型的数据结构。一个联合体内可以定义多种不同的数据类型,共享相同的内存空间,只能使用其中一个类型。

非受限联合体(Unrestricted Union):任何非引用类型都可以称为联合体的数据成员。

  • 联合拥有非 POD 类型需要由程序员自己为非受限联合体定义构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

union T
{
    string s;
    int    n;

public:
    // 自定义构造函数和析构函数
    T() { new (&s) string; }
    ~T() { s.~string(); }
};

int main()
{
    T t; //构造析构成功
    return 0;
}

3.8 用户自定义字面量

部分人

C++11 可以通过一个后缀标识的操作符,将声明了该后缀标识的字面量转化为需要的类型。

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
51
52
53
54
55
56
57
#include <iostream>
using namespace std;

typedef unsigned char uint8;

struct RGBA
{
    uint8 r;
    uint8 g;
    uint8 b;
    uint8 a;
    RGBA(uint8 R, uint8 G, uint8 B, uint8 A = 0) : r(R), g(G), b(B), a(A) {}
};

RGBA operator"" _C(const char* col, size_t n)
{
    const char* p   = col;
    const char* end = col + n;
    const char *r, *g, *b, *a;
    r = g = b = a = nullptr;
    for (; p != end; ++p) {
        if (*p == 'r')
            r = p;
        else if (*p == 'g')
            g = p;
        else if (*p == 'b')
            b = p;
        else if (*p == 'a')
            a = p;
    }
    if ((nullptr == r) || (nullptr == g) || (nullptr == b)) {
        throw;
    }
    else if (nullptr == a)
        return RGBA(atoi(r + 1), atoi(g + 1), atoi(b + 1));
    else
        return RGBA(atoi(r + 1), atoi(g + 1), atoi(b + 1), atoi(a + 1));
}

ostream& operator<<(ostream& out, RGBA& col)
{
    return out << "r: " << (int)col.r << ", g: " << (int)col.g << ", b: " << (int)col.b << ", a: " << (int)col.a
               << endl;
}

void blend(RGBA&& col1, RGBA&& col2) { cout << "blend " << endl << col1 << col2 << endl; }

int main()
{
    /**
     * blend 
     * r: 255, g: 240, b: 155, a: 0
     * r: 15, g: 255, b: 10, a: 7
     */
    blend("r255 g240 b155"_C, "r15 g255 b10 a7"_C);
    return 0;
}

C++11字面量具体规则:

  • 如果字面量为整型数,那么字面量操作符函数只可接受unsigned long long或者 const char*为其参数。当unsigned long long无法容纳该字面量的时候,编译器会自动将该字面量转化为以 \0 为结束符的字符串,并调用以 const char* 为参数的版本进行处理。
  • 如果字面量为浮点型数,则字面量操作符函数只可接受long double或者 const char* 为参数。const char* 版本的调用规则同整型的一样(过长则使用 const char* 版本)。
  • 如果字面量为字符串,则字面量操作符函数函数只可接受 const char* , size_t 为参数 (已知长度的字符串)。
  • 如果字面量为字符,则字面量操作符函数只可接受一个char为参数。

注意事项:

  • 在字面量操作符函数的声明中,operator""与用户自定义后缀之间必须有空格。
  • 后缀建议以下划线开始。不宜使用非下划线后缀的用户自定义字符串常量,否则会被编译器警告。和标准自带的字面量(201203L)混淆。

3.9 内联名字空间

部分人

C++98 标准不允许在不同的名字空间(namespace)中对模板进行特化。

C++11 可以通过关键字 inline namespace 声明一个内联的名字空间,允许程序员在父名字空间定义或特化子名字空间的模板。

ADL(Argument Dependent name Lookup,参数关联名称查找),一定程度上破坏了 namespace 的封装性。最好还是打开名字空间或者使用 :: 列出变量、函数完整的名字空间。

3.10 模板的别名

部分人

C++11 中 usingtypedef 都可以定义类型别名。模板编程 using 更加灵活。

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 <map>
#include <type_traits> // 判断类型
#include <typeinfo>    // 获取类型名称
#include <cxxabi.h>    // 获取类型名称
using namespace std;

// 起别名
using uint = unsigned int;
typedef unsigned int UINT;
using sint = int;

// 模板使用
template <typename T>
using MapString = std::map<T, char*>;

int main()
{
    std::cout << std::is_same<uint, UINT>::value << std::endl; // 1
    MapString<int> numberedString;                             // std::map<int, char*>
    std::cout << typeid(numberedString).name() << " => "
              << abi::__cxa_demangle(typeid(numberedString).name(), NULL, NULL, NULL) << std::endl; // 1

    return 0;
}

3.11 一般化的 SFINAE 规则

库作者

C++模板规则 SFINAE(Substitution failure is not an error,匹配失败不是错误)。这条规则表示的是对重载的模板的参数进行展开的时候,如果展开导致了一些类型不匹配,编译器并不会报错。

SFINAE 最主要的作用,是保证编译器在泛型函数、偏特化、及一般重载函数中遴选函数原型的候选列表时不被打断。除此之外,它还有一个很重要的元编程作用就是实现部分的编译期自省和反射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Test {
  typedef int foo;
};

template <typename T>
void f(typename T::foo) {}  // Definition #1

template <typename T>
void f(T) {}  // Definition #2

int main() {
  f<Test>(10);  // Call #1.
  f<int>(10);   // Call #2. 并无编译错误(即使没有 int::foo)
                // thanks to SFINAE.
}

4 新手易学,老兵易用

4.1 右尖括号 > 的改进

所有人

C++98 中会将 >> 两个尖括号优先解析为右移,C++11 则会优先解析为模板参数界定符。

当模板确定优先需要右移,建议使用园括号括起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

template <int i>
class X
{};

template <class T>
class Y
{};

int main()
{
    Y<X<1> > x1;   // C++98成功,C++11成功
    Y<X<1>> x2;    // C++98失败,C++11成功
    X<1 >> 5> x;   // C++98成功,C++11失败
    X<(1 >> 5)> x; // C++98成功,C++11成功
    return 0;
}

4.2 auto类型推导

所有人

4.2.1 静态类型、动态类型与类型推导

  • 静态类型:类型检查发生在编译阶段,确定变量类型。
  • 动态类型:类型检查发生在运行阶段。需要用到类型推导。

auto 声明变量的类型必须由编译器在编译时期推导而得。

  • auto 声明的变量必须被初始化,使编译器能够从其初始化表达式中推导出其类型。

4.2.2 auto的优势

  1. 能够在初始化表达式的负载类型变量声明时简化代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
#include <vector>
using namespace std;

void loopover(std::vector<std::string>& vs)
{
    std::vector<std::string>::iterator i = vs.begin(); //使用 iterator,往往需要书写大量代码
    for (; i < vs.end(); i++)
    {
        /* code */
    }

    for (auto i = vs.begin(); i < vs.end(); i++)    // 简洁代码
    {
        /* code */
    }
}

int main() { return 0; }
  1. auto 自适应可以免除在一些类型声明时的麻烦,或者避免一些在类型声明时的错误。
  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
#include <iostream>
using namespace std;

template <typename T1, typename T2>
double Sum(T1& t1, T2& t2)
{
    auto s = t1 + t2;
    return s;
}

#define MAX(a, b)            \
    ({                       \
        auto _a = (a);       \
        auto _b = (b);       \
        (_a > _b) ? _a : _b; \
    })

int main()
{
    int a = 3;
    long b = 5;
    float c = 1.0f;
    auto s = Sum<int, long>(a, b);
    auto x = Sum<int, float>(a, c);
    auto m = MAX(1 * 2 * 3 * 4, 5 * 6 * 7 * 8);
    return 0;
}

4.2.3 auto的使用细则

  1. auto 可以与指针和引用结合起来使用。
  2. auto 变量不能从其初始化表达式中“带走” cv限制符。volatileconst 代表了变量的两种不同属性:易失的和常量的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

double foo();
float* bar();

int main()
{
    const auto     a = foo(); // const double
    const auto&    b = foo(); // const double
    volatile auto* c = bar(); // volatile float*

    auto           d = a; // double
    auto&          e = a; // const double&
    auto           f = c; // float*
    volatile auto& g = c; // volatile float*&

    return 0;
}
  1. auto 同一赋值语句中可以声明多个变量的类型,但是这些变量的类型必须相同。同行是“从左向右推导”。
  2. auto 4种不能推导情况
    • 对于函数 fun 来说, auto 不能是其形参类型。
    • 对于结构体来说,非静态成员变量的类型不能是 auto 类型。
    • 声明 auto 数组。
    • 在实例化模板的时候不能使用 auto 作为模板参数,如 vector<auto> v = {1}

4.3 decltype

库作者

4.3.1 typeid和decltype

4.3.2 decltype的应用

  1. decltypetypedef/using 的合用
  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
#include <iostream>
#include <vector>
using namespace std;

// using typedef
using size_t    = decltype(sizeof(0));
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);

// 匿名函数
enum 
{
    K1,
    K2,
    K3
} anon_e;

union
{
    decltype(anon_e) key;
    char*            name;
} anon_u;

struct
{
    int d;
    decltype(anon_u) id;
} anon_s[100];

int main()
{
    // using typedef
    std::vector<int>              vec;
    typedef decltype(vec.begin()) vectype;
    for (vectype i = vec.begin(); i < vec.end(); i++)
    {
        /* code */
    }

    // 匿名函数
    decltype(anon_s) as;
    as[0].id.key = decltype(anon_e)::K1;

    return 0;
}

4.3.3 decltype推导四规则

当程序员用 decltype(e) 来获取类型时,编译器将依序判断以下四规则:

  1. 如果 e 是一个没有带括号的标记符表达式(id-expression )或者类成员访问表达式, 那么 decltype(e) 就是 e 所命名的实体的类型。此外,如果 e 是一个被重载的函数,则会导致编译时错误。
  2. 否则,假设e的类型是T,如果e是一个将亡值(xvalue),那么 decltype(e) 为 T&&。
  3. 否则,假设e的类型是T,如果e是一个左值,则 decltype(e) 为 T&。
  4. 否则,假设e的类型是T,则 decltype(e) 为T。
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
#include <iostream>

using namespace std;

int  i      = 4;
int  arr[5] = {0};
int* ptr    = arr;
struct S
{
    double d;
} s;
void       Overloaded(int);
void       Overloaded(int);
int&&      RvalRef();
const bool Func(int);

int main()
{
    // 规则1:单个标记符表达式以及访问类成员,推导为本类型
    decltype(arr) var1; // int[5]
    decltype(ptr) var2; // int*
    decltype(s.d) var3; // double
    // decltype(Overloaded) var4; // 无法通过编译

    // 规则2:将亡值,推导为类型的右值引用
    decltype(RvalRef()) var6     = 1; // int&&

    // 规则3:左值,推导为类型的引用
    decltype(true ? i : i) var7  = i;      // int&
    decltype((i))          var8  = i;      // int&
    decltype(++i)          var9  = i;      // int&
    decltype((arr[3]))     var10 = i;      // int&
    decltype(*ptr)         va11  = i;      // int&
    decltype("lval")       var12 = "lval"; // const char(&)[5]

    // 规则4:以上都不是,推导为本类型
    decltype(1)         var13; // int
    decltype(i++)       var14; // int
    decltype((Func(1))) var15; // bool

    return 0;
}

C++11 标准库 <type_traits> 添加模板类进行推导结果识别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <type_traits>
using namespace std;

int   i      = 4;
int   arr[5] = {0};
int*  ptr    = arr;
int&& RvalRef();

int main()
{
    std::cout << std::is_lvalue_reference<decltype((i))>::value << std::endl; // 1
    std::cout << std::is_lvalue_reference<decltype(i++)>::value << std::endl; // 0
    std::cout << std::is_rvalue_reference<decltype(i++)>::value << std::endl; // 0

    return 0;
}

4.3.4 cv限制符的继承与冗余的符号

  1. 类型推导时,auto 不能带走 cv限制符,decltype 能带走 cv限制符;对象的定义中包含 cv限制符,decltype推导其成员不会继承 cv限制符。
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
#include <iostream>
#include <type_traits>
using namespace std;

const int    ic = 0;
volatile int iv;
struct S
{
    int i;
};
const S     a = {0};
volatile S  b;
volatile S* p = &b;

int main()
{
    std::cout << std::is_const<decltype(ic)>::value << std::endl;    // 1
    std::cout << std::is_volatile<decltype(iv)>::value << std::endl; // 1

    std::cout << std::is_const<decltype(a)>::value << std::endl;    // 1
    std::cout << std::is_volatile<decltype(b)>::value << std::endl; // 1

    std::cout << std::is_const<decltype(a.i)>::value << std::endl;     // 0
    std::cout << std::is_volatile<decltype(p->i)>::value << std::endl; // 0

    return 0;
}
  1. decltype 推导出的类型已经有了这些属性,冗余的符号则会被忽略。

4.4 追踪返回类型

库作者

4.4.1 追踪放回类型的引入

我们把函数的返回值移至参数声明之后,复合函数 -> decltype(t1 + t2) 被称为追踪返回类型。

1
2
3
4
5
template <typename T1, typename T2>
auto Sum(T1& t1, T2& t2) -> decltype(t1 + t2)
{
    return t1 + t2;
}

4.4.2 使用追踪返回类型的函数

追踪返回类型可以使用在函数模板、普通函数、函数指针、函数应用、结构体或类的成员函数、类模板的成员函数里。

4.5 基于范围的for循环

所有人

for_each 使用了迭代器的概念,其迭代器就是指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <algorithm>
#include <iostream>
using namespace std;

void action1(int& e) { e *= 2; }
void action2(int& e) { std::cout << e << std::endl; }

int main()
{
    int arr[5] = {1, 2, 3, 4, 5};
    for_each(arr, arr + sizeof(arr) / sizeof(arr[0]), action1);	// 2,4,6,8,10
    for_each(arr, arr + sizeof(arr) / sizeof(arr[0]), action2);	
    return 0;
}

for 范围循环,循环后的括号由冒号 : 分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示将被迭代的范围。

如果数组大小不能确定,是不能够使用基于范围的 for 循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

int main()
{
    int arr[5] = {1, 2, 3, 4, 5};
    for (int& e : arr)
    {
        e *= 2;
    }
    for (auto& e : arr)
    {
        std::cout << e << std::endl;
    }
    return 0;
}

习惯使用 C++ 程序员需要注意,基于范围的循环使用在标准库的容器中时,使用 auto 来声明迭代对象的话,那这个对象不会是迭代器对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> v = {1, 2, 3, 4, 5};
    for (auto i = v.begin(); i != v.end(); i++)
    {
        std::cout << *i << std::endl; // i 是迭代器对象
    }
    for (auto& e : v)
    {
        std::cout << e << std::endl; // e 是解应用后的对象
    }
    return 0;
}

5 提高类型安全

5.1 强类型枚举

部分人

5.1.1 枚举:分门别类与数值的名字

1
2
3
4
5
6
7
8
9
10
// 1. 宏定义
#define Male 0
#define Female 1

// 2. 匿名 enum
enum { Male, Female };

// 3. 静态常量
const static int Male = 0;
const static int Female = 1;

5.1.2 有缺陷的枚举类型

  1. C/C++ 中具名(有名字)的enum 类型名字和 enum 的成员名字都是全局可见。非常容易导致名字冲突。

  2. C 中枚举被设计为常量数值的“别名”的本性,所以枚举的成员总是可以被隐式转换为整型。

5.1.3 强类型枚举以及C++11对原有枚举类型的扩展

声明强类型枚举只需要在 enum 后加上关键字 class

1
enum class Type { General, Light, Medium, Heavy };

强类型枚举优势:

  1. 强作用域:强类型枚举成员的名称不会被输出到其父作用域空间。
  2. 转换限制:强类型枚举成员的值不可以与整型隐式的相互转换。
  3. 可以指定底层类型:强类型枚举默认的底层类型为 int,但也可以显式地指定底层类型,具体方法为在枚举名称后面加上 : type,其中 type 可以是除 wchar_t 以外的任何整 型。
1
enum class Type: char { General, Light, Medium, Heavy };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

enum class C : char
{
    C1 = 1,
    C2 = 2
};
enum class D : unsigned int
{
    D1   = 1,
    D2   = 2,
    Dbig = 0xFFFFFFF0U
};

int main()
{
    std::cout << sizeof(C::C1) << std::endl; // 1

    std::cout << (unsigned int)D::Dbig << std::endl; // 4294967280
    std::cout << sizeof(D::D1) << std::endl;         // 4
    std::cout << sizeof(D::Dbig) << std::endl;       // 4
    return 0;
}

注意:

  1. 原有枚举类型可以跟强类型枚举类一样,显示的由程序员来指定。
  2. C++11 枚举成员名字除了会自动输出到父作用域,也可以在枚举类型定义的作用域有效。
  3. 匿名强类型枚举不能使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

// enum Type: char { General, Light, Medium, Heavy };

enum Type { General, Light, Medium, Heavy };

int main()
{
    Type t1 = General;
    Type t2 = Type::General;
    return 0;
}

5.2 堆内存管理:智能指针与垃圾回收

类作者

库作者

5.2.1 显示内存管理

  1. 野指针:一些内存单元已被释放,之前指向它的指针却还在被使用。这些内存有可能 被运行时系统重新分配给程序使用,从而导致了无法预测的错误。
  2. 重复释放:程序试图去释放已经被释放过的内存单元,或者释放已经被重新分配过的 内存单元,就会导致重复释放错误。通常重复释放内存会导致 C/C++ 运行时系统打印 出大量错误及诊断信息。
  3. 内存泄漏:不再需要使用的内存单元如果没有被释放就会导致内存泄漏。如果程序不 断地重复进行这类操作,将会导致内存占用剧增。

5.2.2 C++11的智能指针

C++11 标准中使用 unique_ptrshared_ptrweak_ptr 等智能指针来自动回收堆分配的对象。

  • unique_ptr 与所指对象的内存绑定紧密,不能与其他unique_ptr 类型的指针对象共享所指对象的内存。
  • shared_ptr (引用计数)允许多个该智能指针共享的“拥有”同一堆分配对象的内存。
  • weak_ptr 可以指向 shared_ptr 指针指向的对象内存,却并不拥有该内存。

5.2.3 垃圾回收的分类

我们把之前使用过,现在不再使用或者没有任何指针再指向的内存空间就称为“垃圾”。而将这 些“垃圾”收集起来以便再次利用的机制,就被称为“垃圾回收”(Garbage Collection)。

垃圾回收主要分为两大类:

  • 基于引用计数(reference counting garbage collector)的垃圾回收器

引用计数主要是使用系统记录对象被引用(引用、指针)的次数。当对象被引用的次数变为0时,该对象即可被视作“垃圾”而回收。

  • 基于跟踪处理(tracing garbage collector)的垃圾回收器

相比于引用计数,跟踪处理的垃圾回收机制被更为广泛地应用。其基本方法是产生跟踪对象的关系图,然后进行垃圾回收。使用跟踪方式的垃圾回收算法主要有以下几种:

  1. 标记-清除(Mark-Sweep)

    顾名思义,这个算法可以分为两个过程。首先该算法将程序中正在使用的对象视为“根对象”,从根对象开始査找它们所引用的堆空间,并在这些堆空间上做标记。当标记结束后,所有被标记的对象就是可达对象(Reachable Object)或活对象(Live Object),而没有被标 记的对象就被认为是垃圾,在第二步的清扫(Sweep)阶段会被回收掉。

    这种方法的特点是活的对象不会被移动,但是其存在会出现大量的内存碎片的问题。

  2. 标记-整理(Mark-Compact)

    这个算法标记的方法和标记-清除方法一样,但是标记完之后,不再遍历所有对象清扫 垃圾了,而是将活的对象向“左”靠齐,这就解决了内存碎片的问题。

    标记-整理的方法有个特点就是移动活的对象,因此相应的,程序中所有对堆内存的引用都必须更新。

  3. 标记-拷贝(Mark-Copy)

    这种算法将堆空间分为两个部分:From和To。刚开始系统只从From的堆空间里面分配 内存,当From分配满的时候系统就开始垃圾回收:从From堆空间找出所有活的对象,拷贝到To的堆空间里。这样一来,From的堆空间里面就全剩下垃圾了。而对象被拷贝到 To里之后,在To里是紧凑排列的。接下来是需要将From和To交换一下角色,接着从新的From里面开始分配。

    标记-拷贝算法的一个问题是堆的利用率只有一半,而且也需要移动活的对象。此外, 从某种意义上讲,这种算法其实是标记-整理算法的另一种实现而已。

5.2.4 C++与垃圾回收

最初垃圾回收可能导致指针操作达不到预期,地址更改指针回收。

5.2.5 C++11与最小垃圾回收支持

C++11 新标准为了做到最小的垃圾冋收支持,首先对“安全”的指针进行了定义,或者使用 C++11 中的术语说,安全派生(safely derived )的指针。安全派生的指针是指尚由 new 分配的对象或其子对象的指针。

5.2.6 垃圾回收的兼容性

C++11 标准中对指针的垃圾冋收支持仅限于系统提供的 new 操作符分嶼内存, 而 malloc 分配的内存则会被认为总是可达的,即无论何时垃圾回收器都不予回收。 malloc 等的较老代码的堆内存还是必须由程序员自己控制。

6 提高性能及操作硬件的能力

6.1 常量表达式

类作者

6.1.1 运行时常量性与编译时常量性

const 运行时常量

constexpr 编译时常量

6.1.2 常量表达式函数

在函数返回类型前加关键字 constexpr 来使其成为常量表达式函数。

  • 函数体只有单一的 return 返回语句。
  • 函数必须返回值(不能是 void 函数)。
  • 在使用前必须已有定义。
  • return 返回语句表达式不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式。

6.1.3 常量表达式值

自定义数据需要定义自定义常量构造函数。

1
2
3
4
5
6
struct MyType
{
    constexpr MyType(int x) : i(x) {}
    int i;
};
constexpr MyType mt = {0};

常量表达式的构造函数也有使用上的约束,主要的有以下两点:

  • 函数体必须为空。

  • 初始化列表只能由常量表达式来赋值。

6.1.4 常量表达式的其他应用

6.2 变长模板

库作者

6.2.1 变长函数和变长模板参数

C++11 已经支持 C99 的变长宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdarg.h>
#include <stdio.h>

double SumOfFloat(int count, ...)
{
    va_list ap;
    double  sum = 0;
    va_start(ap, count); // 获得变长列表的句柄 ap
    for (int i = 0; i < count; i++)
    {
        sum += va_arg(ap, double); // 每次获得一个参数
    }
    va_end(ap);
    return sum;
}
int main()
{
    printf("%f\n", SumOfFloat(3, 1.2f, 3.4, 5.6)); // 10.200000
    return 0;
}

6.2.2 变长模板:模板参数包和函数参数包

1
2
// 声明 tuple 是一个变长类模板
template <typename... Elements> class tuple;

我们在标示符 Elements 之前的使用了省略号(三个“点”)来表示该参数是 变长的。在C++11中,Elements 被称作是一个“模板参数包” (template parameter pack)。

为了使用模板参数包,我们总是需要将其解包(unpack)。在C++11中,这通常是通过一个名为包扩展(pack expansion)的表达式来完成。

1
template <typename... A> class Template: private B<A...>{};

这里的表达式 A... (即参数包A加上三个“点”)就是一个包扩展。

1
2
3
4
5
6
7
8
9
template <typename... Elements> class tuple;  // 变长模板的声明

template <typename Head, typename... Tail>    // 递归的偏特化定义
class tuple<Head, Tail...> : private tuple<Tail...>
{
    Head head;
};

template <> class tupleo{}; // 边界条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

template <long... nums>
struct Multiply;

template <long first, long... last>
struct Multiply<first, last...>
{
    static const long val = first * Multiply<last...>::val;
};

template <>
struct Multiply<>
{
    static const long val = 1;
};

int main()
{
    std::cout << Multiply<2, 3, 4, 5>::val << std::endl;        // 120
    std::cout << Multiply<22, 44, 66, 88, 9>::val << std::endl; // 50599296
    return 0;
}

6.2.3 变长模板:进阶

标准定义了7种参数包可以展开位置

  • 表达式
  • 初始化列表
  • 基类描述列表
  • 类成员初始化列表
  • 模板参数列表
  • 通用属性列表(第8章)
  • lambda函数的捕捉列表(第7章)
1
2
3
4
5
6
7
8
// 同样实例化 T<X,Y>
// 解包
template <typename... A> class Template: private B<A>...{};
class T<X, Y>: private B<X>, private B<Y>{};

// 解包
template <typename... A> class Template: private B<A...>{};
class T<X, Y>: private B<X, Y>{};

C++11 中, sizeof... 作用是计算参数包中的参数个数。

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 <cassert>
#include <iostream>
using namespace std;

template <class... A>
void Print(A... arg)
{
    assert(false); // 非 6 参数偏特化版本都会默认 assert(false)
}

// 特化 6 参数板本
void Print(int a1, int a2, int a3, int a4, int a5, int a6)
{
    std::cout << a1 << ", " << a2 << ", " << a3 << ", " << a4 << ", " << a5 << ", " << a6 << std::endl;
}

template <class... A>
int Vaargs(A... args)
{
    int size = sizeof...(A); //计算变长包的长度
    switch (size)
    {
    case 0: Print(99, 99, 99, 99, 99, 99); break;
    case 1: Print(99, 99, args..., 99, 99, 99); break;
    case 2: Print(99, 99, args..., 99, 99); break;
    case 3: Print(args..., 99, 99, 99); break;
    case 4: Print(99, args..., 99); break;
    case 5: Print(99, args...); break;
    case 6: Print(args...); break;
    default: Print(0, 0, 0, 0, 0, 0); break;
    }
    return size;
}

int main()
{
    Vaargs();                    // 99, 99, 99, 99, 99, 99
    Vaargs(1);                   // 99, 99, 1, 99, 99, 99
    Vaargs(1, 2);                // 99, 99, 1, 2, 99, 99
    Vaargs(1, 2, 3);             // 1, 2, 3, 99, 99, 99
    Vaargs(1, 2, 3, 4);          // 99, 1, 2, 3, 4, 99
    Vaargs(1, 2, 3, 4, 5);       // 99, 1, 2, 3, 4, 5
    Vaargs(1, 2, 3, 4, 5, 6);    // 1, 2, 3, 4, 5, 6
    Vaargs(1, 2, 3, 4, 5, 6, 7); // 0, 0, 0, 0, 0, 0
    return 0;
}

6.3 原子类型与原子操作

所有人

6.3.1 并行编程、多线程与C++11

C++11 之前,C++ 一直是一种顺序的编程语言。顺序是指所有指令都是串行执行的, 即在相同的时刻,有且仅有单个 CPU 的程序计数器指向可执行代码的代码段,并运行代码段中的指令。

C++11 中,标准的一个相当大的变化就是引入了多线程的支持。这使得 C/C++ 语言在进行线程编程时,不必依赖第三方库和标准。而 C/C++ 对线程的支持,一个最为重要的部分,就是在原子操作中引入了原子类型的概念。

6.3.2 原子操作与C++11原子类型

所谓原子操作,就是多线程程序中“最小的且不可并行化的”的操作。

通常情况下,原子操作都是通过“互斥”(mutual exclusive )的访问来保证的。

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
#include <iostream>
#include <pthread.h>
using namespace std;

static long long total = 0;
pthread_mutex_t  m     = PTHREAD_MUTEX_INITIALIZER;

void* func(void*)
{
    long long i;
    for (i = 0; i < 100000000LL; i++)
    {
        pthread_mutex_lock(&m);
        total += i;
        pthread_mutex_unlock(&m);
    }
}

int main()
{
    pthread_t thread1, thread2;
    if (pthread_create(&thread1, NULL, &func, NULL))
    {
        throw;
    }
    if (pthread_create(&thread2, NULL, &func, NULL))
    {
        throw;
    }
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    cout << total << endl; // 9999999900000000
    return 0;
}

C++11 直接定义一个原子数据类型,就不需要为原子数据类型显式的声明互斥锁或调用加锁、解锁的 API

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 <atomic>
#include <iostream>
#include <thread>
using namespace std;

atomic_llong total{0};

void func(int)
{
    long long i;
    for (i = 0; i < 100000000LL; i++)
    {
        total += i;
    }
}

int main()
{
    thread t1(func, 0);
    thread t2(func, 0);
    t1.join();
    t2.join();
    cout << total << endl; // 9999999900000000
    return 0;
}
原子类型名称对应的内置类型名称
atomic_boolbool
atomic_charchar
atomic_charsigned char
atomic_ucharunsigned char
atomic_intint
atomic_uintunsigned int
atomic_shortshort
atomic_ushortunsigned short
atomic_longlong
atomic_uiongunsigned long
atomic_llonglong long
atomic_ullongunsigned long long
atomic_char16_tcharl6_t
atomic_char32_tchar32_t
atomic_wchar_twchart

不过更为普遍地,程序员可以使用 atomic 类模板。 通过该类模板,程序员任意定义出需要的原子类型。比如下列语句:

1
std::atomic<T> t;

对于线程而言,原子类型通常属于“资源型”的数据,这意味着多个线程通常只能访 问单个原子类型的拷贝。因此在 C++11 中,原子类型只能从其模板参数类型中进行构造, 标准不允许原子类型进行拷贝构造、移动构造,以及使用 operator= 等,以防止发生意外。

操作atomic_flagatomic_boolatomic_integral-typeatomicatomic<T*>atomic atomic
test_and_setY      
clearY      
is_lock_free yyyyyy
load yyyyyy
store yyyyyy
exchange yyyyyy
compare_exchange_weak +strong yyyyyy
fetch_add, +=  y yy 
fetch_sub, -=  y yy 
fetch_or, |= y  y  
fetch_and, &=  y  y 
fetch_xor, ^=  y  y 
++, –  y yyy

6.3.3 内存模型,顺序一致性与memory_order

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
#include <atomic>
#include <iostream>
#include <thread>
using namespace std;

atomic<int> a;
atomic<int> b;

int Thread1(int)
{
    int t = 1;
    a     = t;
    b     = 2;
}

int Thread2(int)
{
    while (b != 2)
    {
        ; //自旋等待
    }
    std::cout << a << std::endl; //总是期待 a 的值为 1
}

int main()
{
    thread t1(Thread1, 0);
    thread t2(Thread2, 0);
    t1.join();
    t2.join();
    return 0;
}

C++11中并不是只支持顺序一 致单个内存模型的原子变量,因为顺序一致往往意味着最低效的同步方式。

1: Loadi	reg3, 1;	# 将立即数1放入寄存器reg3
2: Move		reg4, reg3;	# 将reg3的数据放入reg4
3: Store	reg4, a;	# 将寄存器reg4中的数据存入内存地址a
4: Loadi	reg5, 2;	# 将立即数2放入寄存器reg5
5: Store	reg5, b;	# 将寄存器reg5中的数据存入内存地址b

这里我们演示了 t = l;a = t;b = 2; 这段C++语言代码的伪汇编表示。按照通常的理解,指令总是按照 1->2->3->4->5 这样顺序执行,如果处理器的执行顺序是这样的话,我们通常称这样的内存模型为强顺序的(strong ordered )。

指令1、2、3和指令4、5运行顺序上毫无影响(使用了不同的寄存器,以及不同的内存地址),一些处理器就有可能将指令执行的顺序打乱,比如按照 1->4->2->5->3 这样顺序(通常这样的执行顺序都是超标量的流水线,即一个时钟周期里发射多条指令而产生的)。如果指令是按照这个顺序被处理器执行的话,我们通常称之为弱顺序的 (weak ordered )。

注意 为什么会有弱顺序的内存模型?

简单地说,弱顺序的内存模型可以使得处理器进一步发掘指令中的并行性, 使得指令执行的性能更高。

枚举值定义规则
memory_order_relaxed不对执行顺序做任何保证
memory_ordcr_acquire本线程中,所有后续的读操作必须在本条原子操作完成后执行
mcmory_ordcr_release本线程中,所有之前的写操作完成后才能执行本条原子操作
memory_order_acq_rel同时包含 memory_order_acquirememory_order_release 标记
memory_order_consume本线程中.所有后续的有关本原子类型的操作,必须在本条原子操作完成之后执行
memory_order_seq_cst全部存取都按顺序执行

通常情况下,我们可以 atomic 成员函数可使用的 memory_order 值分为以下3组:

  • 原子存储操作(store)可以使用 memory_order_relaxedmcmory_ordcr_releasememory_order_seq_cst
  • 原子读取操作(load)可以使用 memory_order_relaxedmemory_order_consumememory_ordcr_acquircmemory_order_seq_cst
  • RMW操作(read-modify-write)即一些需要同时读写的操作。可以使用 memory_order_relaxedmemory_ordcr_acquiremcmory_ordcr_releasememory_order_acq_relmemory_order_consumememory_order_seq_cst
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
#include <atomic>
#include <iostream>
#include <thread>
using namespace std;

atomic<int> a;
atomic<int> b;

int Thread1(int)
{
    int t = 1;
    a.store(t, memory_order_relaxed);
    b.store(2, memory_order_release); // 本原子操作前所有的写原子操作必须完成
}

int Thread2(int)
{
    while (b.load(memory_order_acquire) != 2)
        ; // 本原子操作前必须完成才能执行之后所有读原子操作
    std::cout << a.load(memory_order_relaxed) << std::endl; //总是期待 a 的值为 1
}

int main()
{
    thread t1(Thread1, 0);
    thread t2(Thread2, 0);
    t1.join();
    t2.join();
    return 0;
}

6.4 线程局部存储

所有人

线程局部存储(TLS, thread local storage )是一个已有的概念,就是拥有线程生命期及线程可见性的变量。

C++11TLS 标准做出了一些统一的规定。与 __thread 修饰符类似,声明一个TLS变 量的语法很简单,即通过 thread local 修饰符声明变量即可。

1
2
3
4
5
// g++/clang++/xlc++
__thread int errCode;
    
// C++11
int thread_local errCode;

6.5 快速退出:quick_exit与at_quick_exit

所有人

C++11中,标准引入了 quick_exit 函数,该函数并不执行析构函数而只是使程序终止。使用 at_quick_exit 注册的函数也可以在 quick_exit 的时候被调用,主要是用于退出清理工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstdlib>
#include <iostream>
using namespace std;

struct A
{
    ~A() { std::cout << "Destruct A." << std::endl; }
};

void closeDevice() { std::cout << "device is closed." << std::endl; }

int main()
{
    A a;
    at_quick_exit(closeDevice);
    quick_exit(0);
}

7 为改变思考方式而改变

7.1 指针空值nullptr

所有人

7.1.1 指针空值:从0到NULL再到nullptr

一般情况下,NULL 是一个宏定义。在传统的C头文件(stddef.h)里我们可以找到如下代码:

1
2
3
4
5
#ifndef __cplusplus
#define NULL ((void *)0)
#else   /* C++ */
#define NULL 0
#endif  /* C++ */

可以看到,NULL 可能被定义为字面常量0,或者是定义为无类型指针,这样就会出现歧义。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

void f(int i) { printf("invoke f(int)\n"); }
void f(long l) { printf("invoke f(long)\n"); }
void f(char* c) { printf("invoke f(char*)\n"); }

int main()
{
    f(0);        // invoke f(int)
    f(NULL);     // invoke f(long), [__GNUG__ 将 NULL转换为内部标识 __null]
    f(nullptr);  // invoke f(char*)
    f((char*)0); // invoke f(char*)
}

nullptrC++11 中表示指针空值,就不会产生二义性。

7.1.2 nullptr和nullptr_t

C++11 标准不仅定义了指针空值常量 nullptr,也定义了其指针空值类型 nullptr_t。常见规则:

  • 所有定义为 nullptr_t 类型的数据都是等价的,行为也是完全一致。
  • nullptr_t 类型数据可以隐式转换成任意一个指针类型。
  • nullptr_t 类型数据不能转换为非指针类型,即使使用 reinterpret_cast<nullptr_t>() 的方式也是不可以的。
  • nullptr_t 类型数据不适用于算术运算表达式。
  • nullptr_t 类型数据可以用于关系运算表达式,但仅能与 nullptr_t 类型数据或者指针类型数据进行比较,当且仅当关系运算符为 ==<=>= 等时返回 true

如果读者的编译器能够编译 if(nullptr) 或者 if(nullptr == 0) 这样的语句,可能是因为编译器版本还不够新。老的nullptr定义中允许nullptr向bool的隐式转换,这带来了一些问题,而C++11标准中已经不允许这么做了。

7.1.3 一些关于nullptr规则的讨论

  • nullptr 是一个编译时期的常量,它的名字是一个编译时期的关键字,能够为编译器所识别。而(void*) 只是一个强制转换表达式,其返回的也是一个 void* 指针类型。
  • C++ 语言中,nullptr 到任何指针的转换是隐式的,而 (void*)0 则 必须经过类型转换后才能使用。
  • 对于普通用户不要对 nullptr 做取地址操作即可。

7.2 默认函数的控制

类作者

7.2.1 类与默认函数

在C++语言规则中,一旦程序员实现了这些函数的自定义版本,则编译器不会再为该类自动生成默认版本。

一旦声明了自定义版本的构造函数,则有可能导致我们定义的类型不再是 POD 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <type_traits>
using namespace std;

class TwoCstor
{
public:
    // 提供了带参数版本的构造函数,则必须自行提供不带参数版本,且 TwoCstor 不再是 POD 类型
    TwoCstor(){};
    TwoCstor(int i) : data(i){};

private:
    int data;
};

int main()
{
    std::cout << std::is_pod<TwoCstor>::value << std::endl; // 0
    return 0;
}

C++11中,标准是通过提供了新的机制来控制默认版本函数的生成来”恢复” POD 特质。这个新机制重用了 default 关键字。程序员可以在默认函数定义或者声明时加上 = default,从而显式地指示编译器生成该函数的默认版本。而如果指定产生默认版本后,程序员不再也不应该实现一份同名的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <type_traits>
using namespace std;

class TwoCstor
{
public:
    // 提供了带参数版本的构造函数,再指示编译器提供默认版本,则 TwoCstor 依然是 POD 类型
    TwoCstor() = default;
    TwoCstor(int i) : data(i){};

private:
    int data;
};

int main()
{
    std::cout << std::is_pod<TwoCstor>::value << std::endl; // 1
    return 0;
}

C++11标准中,在函数的定义或者声明加上 = delete 会指示编译器不生成函数的缺省版本,以达到禁止某些默认生成函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <type_traits>
using namespace std;

class NoCopyCstor
{
public:
    NoCopyCstor()                   = default;
    // 使用 (= delete) 可以有效阻止用户错用拷贝构造函数
    NoCopyCstor(const NoCopyCstor&) = delete;
};

int main()
{
    NoCopyCstor a;
    NoCopyCstor b(a); // 无法通过编译
    return 0;
}

7.2.2 (= default)与(= delete)

C++11标准称 =default 修饰的函数为显式缺省(explicit defaulted)函数,而称 =delete 修饰的函数为删除(deleted)函数。

显式缺省不仅可以用于在类的定义中修饰成员函数,也可以在类定义之外修饰成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

void Func(int i) {}
void Func(char c) = delete; // 显示删除 char 版本

int main()
{
    Func(3);
    Func('c'); // 无法通过编译
    return 0;
}

7.3 lambda函数

所有人

7.3.1 lambda的一些历史

lambda ( $\lambda$ )在希腊字母表中位于第11位。同时,由于希腊数字是基于希腊字母的,所以 $\lambda$ 在希腊数字中也表示了值30。在数理逻辑或计算机科学领域中,lambda则是被用来表示一种匿名函数,这种匿名函数代表了一种所谓的 $\lambda$ 演算(lambda calculus)。

7.3.2 C++11中的lambda函数

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int main()
{
    int  girls      = 3;
    int  boys       = 4;
    auto totalChild = [](int x, int y) -> int { return x + y; };
    std::cout << totalChild(girls, boys) << std::endl; // 7
    return 0;
}

lambda 函数的语法定义

1
[capture](parameters) mutable -> return-type {statement}
  • [capture]:捕捉列表。捕捉列表总是出现在 lambda 函数的开始处。事实上,[]lambda 引出符。编译器根据该引岀符判断接下来的代码是否是 lambda 函数。捕捉列表能够捕捉上下文中的变量以供 lambda 函数使用。具体的方法在下文中会再描述。
  • (parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递则可以连同括号 () 一起省略。
  • mutablemutable 修饰符。默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->retum-type:返回类型。用追踪返回类型形式声明函数的返回类型,不需要返回值的时候也可以连同符号 -> 一起省略。在返回类型明确的情况下也可以省略该部分,让编译器对返回类型进行推导。
  • {statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int main()
{
    int  girls      = 3;
    int  boys       = 4;
    auto totalChild = [=]() -> int { return girls + boys; };
    std::cout << totalChild() << std::endl; // 7
    return 0;
}

lambda 函数与普通函数可见的最大区别之一,就是 lambda 函数可以通过捕捉列表访问一些上下文中的数据。

  • [var] 表示值传递方式捕捉变量var。
  • [=] 表示值传递方式捕捉所有父作用域的变量(包括this )。
  • [&var] 表示引用传递捕捉变量var。
  • [&] 表示引用传递捕捉所有父作用域的变量(包括this )。
  • [this] 表示值传递方式捕捉当前的this指针。

父作用域:enclosing scope,这里指的是包含 lambda 函数的语句块

7.3.3 lambda与仿函数

仿函数简单地说,就是重定义了成员函数 operator () 的一种自定义类型对象。

相比于函数,仿函数可以拥有初始状态,一般通过class定义私有成员,并在声明对象的时候对其进行初始化。私有成员的状态就成了仿函数的初始状态。而由于声明一个仿函数对象可以拥有多个不同初始状态的实例,因此可以借由仿函数产生多个功能类似却不同的仿函数实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

class Tax
{
private:
    float rate;
    int   base;

public:
    Tax(float r, int b) : rate(r), base(b) {}
    float operator()(float money) { return (money - base) * rate; }
};

int main()
{
    Tax high(0.40, 30000);
    Tax middle(0.25, 20000);
    std::cout << "tax over 3w: " << high(37500) << std::endl;   // tax over 3w: 3000
    std::cout << "tax over 2w: " << middle(27500) << std::endl; // tax over 2w: 1875
    return 0;
}

7.3.4 lambda的基础使用

局部函数(local function,即在函数作用域中定义的函数),也称为内嵌函教(nested function )。局部函数通常仅属于其父作用域,能够访问父作用域的变量,且在其父作用域中使用。C/C++语言标准中不允许局部函数存在(不过一些其他 语言是允许的,比如FORTRAN), C++11标准却用比较优雅的方式打破了这个规则。因为事实上,lambda可以像局部函数一样使用。

lambda 函数在 C++11 标准中默认是内联的,lambda 函数在代码的作用域上仅属于其父作用域,lambda 函数代码的可读性可能更好,尤其对于小的函数而言。

7.3.5 关于lambda的一些问题及有趣的实验

对于按值方式传递的捕捉列表,其传递的值在 lambda 函数定义的时候就已经决定了。而按引用传递的捕捉列表变量,其传递的值则等于 lambda 函数调用时的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

int main()
{
    int  j             = 12;
    auto by_val_lambda = [=] { return j + 1; };
    auto by_ref_lambda = [&] { return j + 1; };
    std::cout << "by_val_lambda: " << by_val_lambda() << std::endl; // by_val_lambda: 13
    std::cout << "by_ref_lambda: " << by_ref_lambda() << std::endl; // by_ref_lambda: 13

    j++;
    std::cout << "by_val_lambda: " << by_val_lambda() << std::endl; // by_val_lambda: 13
    std::cout << "by_ref_lambda: " << by_ref_lambda() << std::endl; // by_ref_lambda: 14
    return 0;
}

因此简单地总结的话,在使用 lambda 函数的时候,如果需要捕捉的值成为 lambda 函数的常量,我们通常会使用按值传递的方式捕捉;反之,需要捕捉的值成为 lambda 函数运行时的变量(类似于参数的效果),则应该采用按引用方式进行捕捉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

int main()
{
    int val = 0;
    // 编译失败,在const的lambda中修改常量
    // auto const_val_lambda   = [=]() { val = 2; };

    // 非const的lambda,可以修改常量数据
    auto mutable_val_lambda = [=]() mutable { val = 3; };

    // 依然是const的:Lambda,不过没有改动引用本身
    auto const_ref_lambda  = [&] { val = 4; };

    // 依然是const的lambda,通过参数传递val
    auto const_param_lambda = [&](int v) { v = 5; };
    const_param_lambda(val);

    return 0;
}

lambda 函数的 mutable 修饰符可以消除其常量性,大多数时候,我们使用默认版本的(非mutable)的lambda函数也就足够了。

7.3.6 lambda与STL

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

vector<int> nums;
vector<int> largeNums;

const int ubound = 10;

inline void LargeNumsFunc(int i)
{
    if (i > ubound)
    {
        largeNums.push_back(i);
    }
}

// 仿函数
class Lnums
{
public:
    Lnums(int u) : m_ubound(u) {}

    void operator()(int i) const
    {
        if (i > m_ubound)
        {
            largeNums.push_back(i);
        }
    }

private:
    int m_ubound;
};

int main()
{
    // 传统的 for 循环
    for (auto it = nums.begin(); it != nums.end(); it++)
    {
        if (*it >= ubound)
        {
            largeNums.push_back(*it);
        }
    }

    // 使用函数指针
    for_each(nums.begin(), nums.end(), LargeNumsFunc);

    // 使用 lambda 函数和算法 for_each
    for_each(nums.begin(),
             nums.end(),
             [=](int i)
             {
                 if (i > ubound)
                 {
                     largeNums.push_back(i);
                 }
             });

    // 使用仿函数
    for_each(nums.begin(), nums.end(), Lnums(ubound));

    return 0;
}

7.3.7 更多的一些关于lambda的讨论

在现行 C++11 标准中,捕捉列表仅能捕捉父作用域的自动变量,而对超出这个范围的变量是不能被捕捉的。

简单地总结一下,使用 lambda 代替仿函数的应该满足如下一些条件:

  • 是局限于一个局部作用域中使用的代码逻辑。
  • 这些代码逻辑需要被作为参数传递。

8 融入实际应用

8.1 对齐支持

部分人

8.1.1 数据对齐

C++ 中,每个类型的数据除去长度等属性之外,都还有一项“被隐藏”属性,那就是对齐方式。对于每个内置或者自定义类型,都存在一个特定的对齐方式。对齐方式通常是一个整数,它表示的是一个类型的对象存放的内存地址应满足的条件。在这里,我们简单地将其称为对齐值。

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
#include <iostream>
using namespace std;

// 自定义的 ColorVector,拥有 32 字节的数据
struct ColorVector
{
    double r;
    double g;
    double b;
    double a;
};

// 对齐到 32 字节的边界
struct alignas(32) ColorVector2
{
    double r;
    double g;
    double b;
    double a;
};

int main()
{
    // 使用 C++11 中的 alignof 来查询 ColorVecto 的对齐方式
    std::cout << "alignof(ColorVector): " << alignof(ColorVector) << std::endl;
    std::cout << "alignof(ColorVector2): " << alignof(ColorVector2) << std::endl;
    return 0;
}

8.1.2 C++11的alignof和alignas

C++11 在新标准中为了支持对齐,主要引入两个关键字:操作符 alignof、对齐描述符(alignment-specifier) alignas

8.2 通用属性

部分人

8.2.1 语言扩展到通用属性

8.2.2 C++11的通用属性

C++11 语言中的通用属性使用了左右双中括号的形式:

1
[[ attribute-list ]]

8.2.3 预定义的通用属性

C++11 预定义的通用属性包括 [[noretum]][[carries_dependency]] 两种。

  • [[noreturn]] 是用于标识不会返回的函数的。这里必须注意,不会返回和没有返回值的 (void) 函数的区别。没有返回值的void函数在调用完成后,调用者会接着执行函数后的代码;而不会返回的函数在被调用完成后,后续代码不会再被执行。

  • [[carries_dependency]] 则跟并行情况下的编译器优化有关。[[carries_dependency]] 主要是为了解决弱内存模型平台上使用 memory_order_consume 内存顺序枚举问题。

8.3 Unicode支持

所有人

8.3.1 字符集、编码和Unicode

UTF-8的编码方式

Unicode符号范围(十六进制)UTF-8编码方式(二进制)
0000 0000–0000 007F0xxxxxxx
0000 0080–0000 07FF110xxxxx 10xxxxxx
0000 0800–0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
0001 0000–0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

事实上,现行桌面系统中,Windows 内部釆用了 UTF-16 的编码方式,而 Mac OS、Linux 等则釆用了 UTF-8编码方式。

8.3.2 C++11中的Unicode支持

C++11 解决了 Unicode 类型数据的存储问题。C++11 引入以下两种新的内置数据类型来存储不同编码长度的 Unicode 数据。至于 UTF-8 编码的 Unicode 数据,C++11 还是使用 8 字节宽度的 char 类型的数组来保存。

  • charl6_t:用于存储UTF-16编码的Unicode数据。
  • char32_t:用于存储UTF-32编码的Unicode数据。

C++11共定义了 3 种这样的前缀:

  • u8 表示为UTF-8编码。
  • u 表示为UTF-16编码。
  • U 表示为UTF-32编码。

C++11 中还规定了一些简明的方式,即在字符串中用 '\u' 加4个十六进制数编码的 Unicode 码位(UTF-16 )来标识一个 Unicode 字符。比如 '\u4F60' 表示的就是 Unicode 中的中文字符“你”,而 '\u597D' 则是 Unicode 中的“好”。此外也可以通过 '\U' 后跟8个十六进制数编码的 Unicode 码位(UTF-32 )的方式来书写 Unicode 字面常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

int main()
{
    char     utf8[]  = u8"\u4F60\u597D\u554A";
    char16_t utf16[] = u"\u4F60\u597D\u554A";
    char32_t utf32[] = U"hello equals \u4F60\u597D\u554A";

    std::cout << utf8 << std::endl; // 你好啊
    std::cout << utf16 << std::endl;
    std::cout << utf32 << std::endl;

    std::cout << sizeof(utf8) << std::endl;  // 10 byte
    std::cout << sizeof(utf16) << std::endl; // 8 byte

    std::cout << utf8[1] << std::endl;  // 不可见字符
    std::cout << utf16[1] << std::endl; // 22909(0x597D)

    return 0;
}

8.3.3 关于Unicode的库支持

C++11 在标准库中增加了一些 Unicode 编码转换的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Write char16_t representation of multibyte character pointed
   to by S to PC16.  */
extern size_t mbrtoc16 (char16_t *__restrict __pc16,
			const char *__restrict __s, size_t __n,
			mbstate_t *__restrict __p) __THROW;

/* Write multibyte representation of char16_t C16 to S.  */
extern size_t c16rtomb (char *__restrict __s, char16_t __c16,
			mbstate_t *__restrict __ps) __THROW;

/* Write char32_t representation of multibyte character pointed
   to by S to PC32.  */
extern size_t mbrtoc32 (char32_t *__restrict __pc32,
			const char *__restrict __s, size_t __n,
			mbstate_t *__restrict __p) __THROW;

/* Write multibyte representation of char32_t C32 to S.  */
extern size_t c32rtomb (char *__restrict __s, char32_t __c32,
			mbstate_t *__restrict __ps) __THROW;

字母 mbmulti-byte (这里指多字节字符串)的缩写,c16c32 则是 char16char32 的缩写,rrepresentation (表现)的缩写。

C++ 对字符转换的支持则稍微复杂一点,不过 C++ 对编码转换支持的新方法都需要源自于 C++locale 机制的支持。codecvt 还派生一些形如 codecvt_utf8codecvt_utf16codecvt_utf8_utf16 等可以用于字符串转换的模板类。

8.4 原生字符串字面量

所有人

原生字符串使用户书写的字符串“所见即所得”, 不再需要如 \t\n 等控制字符来调整字符串中的格式,这对编程语言的学习和使用都是具有积极意义的。

C++11 中原生字符串的声明相当简单,程序员只需要使用 R"()"就可以声明该字符串字面量为原生字符串。

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;

int main()
{
    std::cout << R"(hello,\n world)" << std::endl; // hello,\n world
    return 0;
}
本文由作者按照 CC BY 4.0 进行授权