0%

Cpp-语言

阅读更多

1 版本新特性

1.1 C++ 11新特性

  1. autodecltype类型推导
  2. defaultdelete函数
  3. finaloverride
  4. 尾置返回类型
  5. 右值引用
  6. 移动构造函数与移动赋值运算符
  7. 有作⽤域的枚举
  8. constexpr与字⾯类型
  9. 扩展「初始化列表」的适⽤范围
  10. 委托与继承的构造函数
  11. 花括号或等号初始化器
  12. 空指针nullptr
  13. long long
  14. char16_tchar32_t
  15. using定义类型别名
  16. 变长参数模板
  17. 推⼴的(⾮平凡)联合体
  18. 推⼴的POD(平凡类型与标准布局类型)
  19. Unicode字符串字⾯量
  20. ⽤户定义字⾯量
  21. 属性,用于提供额外信息
  22. Lambda表达式
  23. noexcept说明符与noexcept运算符
  24. alignofalignas
  25. 多线程内存模型
  26. 线程局部存储,thread_local关键字
  27. GC接口,declare_reachableundeclare_reachable(并未实现)
  28. 基于范围的for循环
  29. static_assert
  30. 智能指针

1.2 C++ 14新特性

  1. 变量模板
  2. 泛型Lambda
  3. Lambda初始化捕获右值对象
  4. new/delete消除
  5. constexpr函数上放松的限制
  6. ⼆进制字⾯量,0b101010
  7. 数字分隔符,100'0000
  8. 函数的返回类型推导
  9. 带默认成员初始化器的聚合类
  10. decltype(auto)

1.3 C++ 17新特性

  1. 折叠表达式
  2. 类模板实参推导
  3. auto占位的⾮类型模板形参
  4. 编译期的constexpr if语句
  5. 内联变量,inline变量
  6. 结构化绑定
  7. if/switch语句的变量初始化
  8. u8-char
  9. 简化的嵌套命名空间
  10. using声明语句可以声明多个名称
  11. noexcept作为类型系统的一部分
  12. 新的求值顺序规则
  13. 强制的复制消除
  14. Lambda表达式捕获*this
  15. constexprLambda表达式
  16. 属性命名空间不必重复
  17. 新属性[[fallthrough]][[nodiscard]][[maybe_unused]]
  18. __has_include

1.4 C++ 20新特性

  1. 特性测试宏
  2. 三路比较运算符<=>
  3. 范围for中的初始化语句和初始化器
  4. char8_t
  5. [[no_unique_address]]
  6. [[likely]][[unlikely]]
  7. Lambda初始化捕获中的包展开
  8. 移除了在多种上下文语境中,使用typename关键字以消除类型歧义的要求
  9. constevalconstinit
  10. 更为宽松的constexpr要求
  11. 规定有符号整数以补码实现
  12. 使用圆括号的聚合初始化
  13. 协程
  14. 模块
  15. 限定与概念(concepts)
  16. 缩略函数模板
  17. 数组长度推导

2 宏

2.1 预定义宏

ANSI C标准中有几个标准预定义宏(也是常用的):

  • __LINE__:在源代码中插入当前源代码行号
  • __FILE__:在源文件中插入当前源文件名
  • __DATE__:在源文件中插入当前的编译日期
  • __TIME__:在源文件中插入当前编译时间
  • __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1
  • __cplusplus:当编写C++程序时该标识符被定义

2.2 语法

  • #:字符串化操作符
  • ##:连接操作符
  • \:续行操作符

2.3 do while(0) in macros

考虑下面的宏定义

1
#define foo(x) bar(x); baz(x)

然后我们调用

1
foo(wolf);

会被展开为

1
bar(wolf); baz(wolf);

看起来没有问题,我们接着考虑另一个情况

1
2
if (condition) 
foo(wolf);

会被展开为

1
2
3
if (condition) 
bar(wolf);
baz(wolf);

这并不符合我们的预期,为了避免出现这种问题,需要用一个作用域将宏包围起来,避免语句的作用域发生偏移,于是我们进一步将宏表示为如下形式

1
#define foo(x) { bar(x); baz(x); }

然后我们调用

1
2
3
4
if (condition)
foo(wolf);
else
bin(wolf);

会被展开为

1
2
3
4
5
6
if (condition) {
bar(wolf);
baz(wolf);
}; // syntax error
else
bin(wolf);

最终,我们将宏优化成如下形式

1
#define foo(x) do { bar(x); baz(x); } while (0)

2.4 参考

3 关键字

3.1 const

默认状态下,const对象仅在文件内有效。编译器将在编译过程中把用到该变量的地方都替代成对应的值,也就是说,编译器会找到代码中所有用到该const变量的地方,然后将其替换成定义的值

为了执行上述替换,编译器必须知道变量的初始值,如果程序包含多个文件,则每个用了const对象的文件都必须能访问到它的初始值才行。要做到这一点,就必须在每一个用到该变量的文件中都对它有定义(将定义该const变量的语句放在头文件中,然后用到该变量的源文件包含头文件即可),为了支持这一用法,同时避免对同一变量的重复定义,默认情况下const被设定为尽在文件内有效(const的全局变量,其实只是在每个文件中都定义了一边而已)

有时候出现这样的情况:const变量的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件生成独立的变量,相反,我们想让这类const对象像其他对象一样工作。即:在一个文件中定义const,在多个文件中声明并使用它,无论声明还是定 义都添加extern关键字

  • .h文件中:extern const int a;
  • .cpp文件中:extern const int a=f();

3.1.1 顶层/底层const

只有指针和引用才有顶层底层之分

  • 顶层const属性表示对象本身不可变
  • 底层const属性表示指向的对象不可变
  • 引用的const属性只能是底层。因为引用本身不是对象,没法指定顶层的const属性
  • 指针的const属性既可以是顶层又可以是底层
    • 注意,只有const变量名相邻时(中间不能有*),才算顶层const。例如下面例子中的p1p2都是顶层const
  • 指针的底层const是可以重新绑定的,例如下面例子中的p1p2
  • 引用的底层const是无法重新绑定的,这是因为引用本身就不支持重新绑定,而非const的限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main() {
int a = 0, b = 1, c = 2;

// top level const
const int* p1 = &a;
p1 = &c;
// *p1 += 1; // compile error

// top level const
int const* p2 = &b;
p2 = &c;
// *p2 += 1; // compile error

// bottom level const
int* const p3 = &c;
// p3 = &a; // compile error
*p3 += 1;

const int& r1 = a;
// r1 = b; // compile error
// r1 += 1; // compile error

return 0;
}

const遵循如下规则:

  • 顶层const可以访问const和非const的成员
  • 底层const只能访问const的成员

示例如下,可以发现:

  • const Container* container以及const Container& container都只能访问const成员,而无法访问非const成员
  • Container* const container可以访问const成员以及非const成员
  • 特别地,const ContainerPtr& container可以访问非const成员,这是因为container->push_back(num)是一个两级调用
    • 第一级:访问的是std::shared_ptr::operator->运算符,该运算符是const的,且返回类型为element_type*
    • 第二级:通过返回的element_type*访问std::vector::push_back,因此与上述结论并不矛盾
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
#include <stddef.h>

#include <memory>
#include <vector>

using Container = std::vector<int32_t>;
using ContainerPtr = std::shared_ptr<Container>;

void append_by_const_reference_shared_ptr(const ContainerPtr& container, const int num) {
// can calling non-const member function
container->push_back(num);
}

void append_by_const_reference(const Container& container, const int num) {
// cannot calling non-const member function
// container.push_back(num);
}

void append_by_bottom_const_pointer(const Container* container, const int num) {
// cannot calling non-const member function
// container->push_back(num);
}

void append_by_top_const_pointer(Container* const container, const int num) {
// can calling non-const member function
container->push_back(num);
}

int main() {
return 0;
}

3.1.2 const实参和形参

实参初始化形参时会自动忽略掉顶层const属性

顶层const不影响形参的类型,例如下面的代码,编译会失败,错误信息是函数重定义

1
2
3
4
5
6
7
8
void func(int value) {}

void func(const int value) {}

int main() {
int value = 5;
func(value);
}

3.1.3 const成员

构造函数中显式初始化:在初始化部分进行初始化,而不能在函数体内初始化;如果没有显式初始化,就调用定义时的初始值进行初始化

3.1.4 const成员函数

const关键字修饰的成员函数,不能修改当前类的任何字段的值,如果字段是对象类型,也不能调用非const修饰的成员方法。(有一个特例,就是当持有的是某个类型的指针时,可以通过该指针调用非const方法)

常量对象以及常量对象的引用或指针都只能调用常量成员函数

常量对象以及常量对象的引用或指针都可以调用常量成员函数以及非常量成员函数

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

class Demo {
public:
void sayHello1() const {
std::cout << "hello world, const version" << std::endl;
}

void sayHello2() {
std::cout << "hello world, non const version" << std::endl;
}
};

int main() {
Demo d;
d.sayHello1();
d.sayHello2();

const Demo cd;
cd.sayHello1();
// the following statement will lead to compile error
// cd.sayHello2();
};

3.2 类型长度

3.2.1 内存对齐

内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的

假如你指定要获取的是0x0001-0x0008,也是8字节,但是不是0开头的,内存需要怎么工作呢?没有好办法,内存只好先工作一次把0x0000-0x0007取出来,然后再把0x0008-0x0015取出来,把两次的结果都返回给你。CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO。这样你的应用程序就会变慢,算是计算机因为你不懂内存对齐而给你的一点点惩罚

内存对齐规则

  1. 结构体第一个成员的偏移量offset0,以后每个成员相对于结构体首地址的offset都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节
  2. 结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最末一个成员之后加上填充字节
  • 有效对齐值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数

下面以一个例子来说明

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
# 创建源文件
cat > main.cpp << 'EOF'
#include <iostream>

struct Align1 {
int8_t f1;
};

struct Align2 {
int8_t f1;
int16_t f2;
};

struct Align3 {
int8_t f1;
int16_t f2;
int32_t f3;
};

struct Align4 {
int8_t f1;
int16_t f2;
int32_t f3;
int64_t f4;
};

int main() {
std::cout << "Align1's size = " << sizeof(Align1) << std::endl;
std::cout << "\tf1's offset = " << offsetof(Align1, f1) << ", f1's size = " << sizeof(Align1::f1) << std::endl;
std::cout << std::endl;

std::cout << "Align2's size = " << sizeof(Align2) << std::endl;
std::cout << "\tf1's offset = " << offsetof(Align2, f1) << ", f1's size = " << sizeof(Align2::f1) << std::endl;
std::cout << "\tf2's offset = " << offsetof(Align2, f2) << ", f2's size = " << sizeof(Align2::f2) << std::endl;
std::cout << std::endl;

std::cout << "Align3's size = " << sizeof(Align3) << std::endl;
std::cout << "\tf1's offset = " << offsetof(Align3, f1) << ", f1's size = " << sizeof(Align3::f1) << std::endl;
std::cout << "\tf2's offset = " << offsetof(Align3, f2) << ", f2's size = " << sizeof(Align3::f2) << std::endl;
std::cout << "\tf3's offset = " << offsetof(Align3, f3) << ", f3's size = " << sizeof(Align3::f3) << std::endl;
std::cout << std::endl;

std::cout << "Align4's size = " << sizeof(Align4) << std::endl;
std::cout << "\tf1's offset = " << offsetof(Align4, f1) << ", f1's size = " << sizeof(Align4::f1) << std::endl;
std::cout << "\tf2's offset = " << offsetof(Align4, f2) << ", f2's size = " << sizeof(Align4::f2) << std::endl;
std::cout << "\tf3's offset = " << offsetof(Align4, f3) << ", f3's size = " << sizeof(Align4::f3) << std::endl;
std::cout << "\tf4's offset = " << offsetof(Align4, f4) << ", f4's size = " << sizeof(Align4::f4) << std::endl;
std::cout << std::endl;
return 0;
}
EOF

# 编译
gcc -o main main.cpp -lstdc++

# 执行
./main

执行结果如下

  • 由于每个成员的offset必须是该成员与有效对齐值中较小的那个值的整数倍,下面称较小的这个值为成员有效对齐值
  • Align1:最长数据类型的长度是1,pack=4,因此,有效对齐值min(1, 4) = 1
    • 规则1:
      • f1,第一个成员的offset = 0
    • 规则2:
      • 类型总长度为1,是有效对齐值(1)的整数倍
  • Align2:最长数据类型的长度是2,pack=4,因此,有效对齐值min(2, 4) = 2
    • 规则1:
      • f1,第一个成员的offset = 0
      • f2,类型长度为2,因此,成员有效对齐值min(2, 2) = 2offset = 2成员有效对齐值(2)的整数倍
    • 规则2:
      • 类型总长度为4,是有效对齐值(2)的整数倍
  • Align3:最长数据类型的长度是4,pack=4,因此,有效对齐值min(4, 4) = 4
    • 规则1:
      • f1,第一个成员的offset = 0
      • f2,类型长度为2,因此,成员有效对齐值min(2, 4) = 2offset = 2成员有效对齐值(2)的整数倍
      • f3,类型长度为4,因此,成员有效对齐值min(4, 4) = 4offset = 4成员有效对齐值(4)的整数倍
    • 规则2:
      • 类型总长度为8,是有效对齐值(4)的整数倍
  • Align4:最长数据类型的长度是8,pack=4,因此,有效对齐值min(8, 4) = 4
    • 规则1:
      • f1,第一个成员的offset = 0
      • f2,类型长度为2,因此,成员有效对齐值min(2, 4) = 2offset = 2成员有效对齐值(2)的整数倍
      • f3,类型长度为4,因此,成员有效对齐值min(4, 4) = 4offset = 4成员有效对齐值(4)的整数倍
      • f4,类型长度为8,因此,成员有效对齐值min(8, 4) = 4offset = 8成员有效对齐值(4)的整数倍
    • 规则2:
      • 类型总长度为16,是有效对齐值(4)的整数倍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Align1's size = 1
f1's offset = 0, f1's size = 1

Align2's size = 4
f1's offset = 0, f1's size = 1
f2's offset = 2, f2's size = 2

Align3's size = 8
f1's offset = 0, f1's size = 1
f2's offset = 2, f2's size = 2
f3's offset = 4, f3's size = 4

Align4's size = 16
f1's offset = 0, f1's size = 1
f2's offset = 2, f2's size = 2
f3's offset = 4, f3's size = 4
f4's offset = 8, f4's size = 8

3.2.2 sizeof

sizeof用于获取对象的内存大小

3.2.3 alignof

alignof用于获取对象的有效对齐值。alignas用于设置有效对其值(不允许小于默认的有效对齐值)

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
#include <iostream>

struct Foo1 {
char c;
int32_t i32;
};

// Compile error
// Requested alignment is less than minimum int alignment of 4 for type 'Foo2'
// struct alignas(1) Foo2 {
// char c;
// int32_t i32;
// };

// Compile error
// Requested alignment is less than minimum int alignment of 4 for type 'Foo3'
// struct alignas(2) Foo3 {
// char c;
// int32_t i32;
// };

struct alignas(4) Foo4 {
char c;
int32_t i32;
};

struct alignas(8) Foo5 {
char c;
int32_t i32;
};

struct alignas(16) Foo6 {
char c;
int32_t i32;
};

#define PRINT_SIZE(name) \
std::cout << "sizeof(" << #name << ")=" << sizeof(name) << ", alignof(" << #name << ")=" << alignof(name) \
<< std::endl;

int main() {
PRINT_SIZE(Foo1);
PRINT_SIZE(Foo4);
PRINT_SIZE(Foo5);
PRINT_SIZE(Foo6);
return 0;
}

输出如下:

1
2
3
4
sizeof(Foo1)=8, alignof(Foo1)=4
sizeof(Foo4)=8, alignof(Foo4)=4
sizeof(Foo5)=8, alignof(Foo5)=8
sizeof(Foo6)=16, alignof(Foo6)=16

3.3 类型推断

3.3.1 auto

auto会忽略顶层const,保留底层的const,但是当设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留

3.3.2 decltype

  • decltype会保留变量的所有类型信息(包括顶层const和引用在内)
  • 如果表达式的内容是解引用操作,得到的将是引用类型
    • int i = 42;
    • int *p = &i;
    • decltype(*p)得到的是int&
  • decltype((c))会得到c的引用类型(无论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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <type_traits>

#define print_type_info(exp) \
do { \
std::cout << #exp << ": " << std::endl; \
std::cout << "\tis_reference_v=" << std::is_reference_v<exp> << std::endl; \
std::cout << "\tis_lvalue_reference_v=" << std::is_lvalue_reference_v<exp> << std::endl; \
std::cout << "\tis_rvalue_reference_v=" << std::is_rvalue_reference_v<exp> << std::endl; \
std::cout << "\tis_const_v=" << std::is_const_v<exp> << std::endl; \
std::cout << "\tis_pointer_v=" << std::is_pointer_v<exp> << std::endl; \
std::cout << std::endl; \
} while (0)

int main() {
int num1 = 0;
int& num2 = num1;
const int& num3 = num1;
int&& num4 = 0;
int* ptr1 = &num1;
int* const ptr2 = &num1;
const int* ptr3 = &num1;

print_type_info(decltype(0));
print_type_info(decltype((0)));

print_type_info(decltype(num1));
print_type_info(decltype((num1)));

print_type_info(decltype(num2));
print_type_info(decltype(num3));
print_type_info(decltype(num4));

print_type_info(decltype(ptr1));
print_type_info(decltype(*ptr1));

print_type_info(decltype(ptr2));
print_type_info(decltype(*ptr2));

print_type_info(decltype(ptr3));
print_type_info(decltype(*ptr3));
}

输出如下:

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
81
82
83
84
85
86
87
88
89
90
decltype(0):
is_reference_v=0
is_lvalue_reference_v=0
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

decltype((0)):
is_reference_v=0
is_lvalue_reference_v=0
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

decltype(num1):
is_reference_v=0
is_lvalue_reference_v=0
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

decltype((num1)):
is_reference_v=1
is_lvalue_reference_v=1
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

decltype(num2):
is_reference_v=1
is_lvalue_reference_v=1
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

decltype(num3):
is_reference_v=1
is_lvalue_reference_v=1
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

decltype(num4):
is_reference_v=1
is_lvalue_reference_v=0
is_rvalue_reference_v=1
is_const_v=0
is_pointer_v=0

decltype(ptr1):
is_reference_v=0
is_lvalue_reference_v=0
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=1

decltype(*ptr1):
is_reference_v=1
is_lvalue_reference_v=1
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

decltype(ptr2):
is_reference_v=0
is_lvalue_reference_v=0
is_rvalue_reference_v=0
is_const_v=1
is_pointer_v=1

decltype(*ptr2):
is_reference_v=1
is_lvalue_reference_v=1
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

decltype(ptr3):
is_reference_v=0
is_lvalue_reference_v=0
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=1

decltype(*ptr3):
is_reference_v=1
is_lvalue_reference_v=1
is_rvalue_reference_v=0
is_const_v=0
is_pointer_v=0

此外,decltype发生在编译期,即它不会产生任何运行时的代码。示例如下,编译执行后,可以发现say_hello并未执行

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

int say_hello() {
std::cout << "hello" << std::endl;
return 0;
}

int main() {
decltype(say_hello()) a;
return 0;
}

3.3.3 typeof

C++标准

3.3.4 typeid

typeid运算符允许在运行时确定对象的类型。若要判断是父类还是子类的话,那么父类必须包含虚函数

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
#define CHECK_TYPE(left, right)                                                            \
std::cout << "typeid(" << #left << ") == typeid(" << #right << "): " << std::boolalpha \
<< (typeid(left) == typeid(right)) << std::noboolalpha << std::endl;

class BaseWithoutVirtualFunc {};

class DeriveWithoutVirtualFunc : public BaseWithoutVirtualFunc {};

class BaseWithVirtualFunc {
public:
virtual void func() {}
};

class DeriveWithVirtualFunc : public BaseWithVirtualFunc {};

int main() {
std::string str;
CHECK_TYPE(str, std::string);

BaseWithoutVirtualFunc* ptr1 = nullptr;
CHECK_TYPE(*ptr1, BaseWithoutVirtualFunc);
CHECK_TYPE(*ptr1, DeriveWithoutVirtualFunc);

BaseWithoutVirtualFunc* ptr2 = new BaseWithoutVirtualFunc();
CHECK_TYPE(*ptr2, BaseWithoutVirtualFunc);
CHECK_TYPE(*ptr2, DeriveWithoutVirtualFunc);

BaseWithoutVirtualFunc* ptr3 = new DeriveWithoutVirtualFunc();
CHECK_TYPE(*ptr3, BaseWithoutVirtualFunc);
CHECK_TYPE(*ptr3, DeriveWithoutVirtualFunc);

BaseWithVirtualFunc* ptr4 = new BaseWithVirtualFunc();
CHECK_TYPE(*ptr4, BaseWithVirtualFunc);
CHECK_TYPE(*ptr4, DeriveWithVirtualFunc);

BaseWithVirtualFunc* ptr5 = new DeriveWithVirtualFunc();
CHECK_TYPE(*ptr5, BaseWithVirtualFunc);
CHECK_TYPE(*ptr5, DeriveWithVirtualFunc);
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
typeid(str) == typeid(std::string): true
typeid(*ptr1) == typeid(BaseWithoutVirtualFunc): true
typeid(*ptr1) == typeid(DeriveWithoutVirtualFunc): false
typeid(*ptr2) == typeid(BaseWithoutVirtualFunc): true
typeid(*ptr2) == typeid(DeriveWithoutVirtualFunc): false
typeid(*ptr3) == typeid(BaseWithoutVirtualFunc): true
typeid(*ptr3) == typeid(DeriveWithoutVirtualFunc): false
typeid(*ptr4) == typeid(BaseWithVirtualFunc): true
typeid(*ptr4) == typeid(DeriveWithVirtualFunc): false
typeid(*ptr5) == typeid(BaseWithVirtualFunc): false
typeid(*ptr5) == typeid(DeriveWithVirtualFunc): true

此外,还可以使用dynamic_cast来判断指针指向子类还是父类

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
#define CHECK_TYPE(left, right)                                                                   \
std::cout << "dynamic_cast<" << #right << ">(" << #left << ") != nullptr: " << std::boolalpha \
<< (dynamic_cast<right>(left) != nullptr) << std::noboolalpha << std::endl;

class Base {
public:
virtual ~Base() {}
};

class Derive : public Base {
virtual ~Derive() {}
};

int main() {
Base* ptr1 = nullptr;
CHECK_TYPE(ptr1, Base*);
CHECK_TYPE(ptr1, Derive*);

Base* ptr2 = new Base();
CHECK_TYPE(ptr2, Base*);
CHECK_TYPE(ptr2, Derive*);

Base* ptr3 = new Derive();
CHECK_TYPE(ptr3, Base*);
CHECK_TYPE(ptr3, Derive*);
}

输出如下:

1
2
3
4
5
6
dynamic_cast<Base*>(ptr1) != nullptr: false
dynamic_cast<Derive*>(ptr1) != nullptr: false
dynamic_cast<Base*>(ptr2) != nullptr: true
dynamic_cast<Derive*>(ptr2) != nullptr: false
dynamic_cast<Base*>(ptr3) != nullptr: true
dynamic_cast<Derive*>(ptr3) != nullptr: true

3.4 类型转换

3.4.1 static_cast

用法:static_cast<type> (expr)

static_cast运算符执行非动态转换,没有运行时类检查来保证转换的安全性。例如,它可以用来把一个基类指针转换为派生类指针。任何具有明确意义的类型转换,只要不包含底层const,都可以使用static_cast

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

int main() {
const char *cc = "hello, world";
auto s = static_cast<std::string>(cc);
std::cout << s << std::endl;

// compile error
// auto i = static_cast<int>(cc);
}

3.4.2 dynamic_cast

用法:dynamic_cast<type> (expr)

dynamic_cast通常用于在继承结构之间进行转换,在运行时执行转换,验证转换的有效性。type必须是类的指针、类的引用或者void*。若指针转换失败,则得到的是nullptr;若引用转换失败,那么会抛出std::bad_cast类型的异常

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>
#include <string>

class Base {
public:
virtual void func() const {
std::cout << "Base's func" << std::endl;
}
};

class Derive : public Base {
public:
void func() const override {
std::cout << "Derive's func" << std::endl;
}
};

int main() {
const Base &b = Derive{};
try {
auto &d = dynamic_cast<const Derive &>(b);
d.func();
auto &s = dynamic_cast<const std::string &>(b); // error case
} catch (std::bad_cast &err) {
std::cout << "err=" << err.what() << std::endl;
}

const Base *pb = &b;
auto *pd = dynamic_cast<const Derive *>(pb);
pd->func();
auto *ps = dynamic_cast<const std::string *>(pb); // error case
std::cout << "ps=" << ps << std::endl; // print nullptr
}

3.4.3 const_cast

用法:const_cast<type> (expr)

这种类型的转换主要是用来操作所传对象的const属性,可以加上const属性,也可以去掉const属性(顶层底层均可)。其中,type只能是如下几类(必须是引用或者指针类型)

  • T &
  • const T &
  • T &&
  • T *
  • const T *
  • T *const
  • const T *const
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main() {
std::cout << "const T & -> T &" << std::endl;
const int &v1 = 100;
std::cout << "v1's address=" << &v1 << std::endl;
int &v2 = const_cast<int &>(v1);
v2 = 200;
std::cout << "v2's address=" << &v2 << std::endl;

std::cout << "\nT & -> T &&" << std::endl;
int &&v3 = const_cast< int &&>(v2);
std::cout << "v3's address=" << &v3 << std::endl;

std::cout << "\nT * -> const T *const" << std::endl;
int *p1 = &v2;
std::cout << "p1=" << p1 << std::endl;
const int *const p2 = const_cast<const int *const >(p1);
std::cout << "p2=" << p2 << std::endl;
}

3.4.4 reinterpret_cast

用法:reinterpret_cast<type> (expr)

reinterpret_cast是最危险的类型转换,它能够直接将一种类型的指针转换为另一种类型的指针,应该非常谨慎地使用。在很大程度上,使用reinterpret_cast获得的唯一保证是,通常如果你将结果转换回原始类型,您将获得完全相同的值(但如果中间类型小于原始类型,则不会)。也有许多reinterpret_cast不能做的转换。它主要用于特别奇怪的转换和位操作,例如将原始数据流转换为实际数据,或将数据存储在指向对齐数据的指针的低位中

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

int main() {
int32_t i = 0x7FFFFFFF;
int32_t *pi = &i;

{
auto *pl = reinterpret_cast<int64_t *> (pi);
std::cout << *pl << std::endl;
auto *rebuild_pi = reinterpret_cast<int32_t *> (pl);
std::cout << *rebuild_pi << std::endl;
}
}

3.5 extern/static

extern:告诉编译器,这个符号在别的编译单元里定义,也就是要把这个符号放到未解决符号表里去(外部链接)

static:如果该关键字位于全局函数或者变量声明的前面,表示该编译单元不 导出这个函数/变量的符号,因此无法再别的编译单元里使用。(内部链接)。如果 static是局部变量,则该变量的存储方式和全局变量一样,但仍然不导出符号

3.5.1 共享全局变量

每个源文件中都得有该变量的声明,但是只有一个源文件中可以包含该变量的定义,通常可以采用如下做法

  • 定义一个头文件xxx.h,声明该变量(需要用extern关键字)
  • 所有源文件包含该头文件xxx.h
  • 在某个源文件中定义该变量

示例如下:

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
# 创建头文件
cat > extern.h << 'EOF'
#pragma once

extern int extern_value;
EOF

# 创建源文件
cat > extern.cpp << 'EOF'
#include "extern.h"

int extern_value = 5;
EOF

# 创建源文件
cat > main.cpp << 'EOF'
#include <iostream>

#include "extern.h"

int main() {
std::cout << extern_value << std::endl;
}
EOF

# 编译
gcc -o main main.cpp extern.cpp -lstdc++ -Wall

# 执行
./main

3.6 virtual

virtual关键词修饰的就是虚函数,虚函数的分派发生在运行时

  1. 有虚函数的每个类,维护一个虚函数表
  2. 有虚函数的类的对象,会包含一个指向该类的虚函数表的指针

virtual-method-table

3.7 volatile

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改(程序之外的因素),比如:操作系统、硬件等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问

  • 仅从C/C++标准的角度来说(不考虑平台以及编译器扩展),volatile并不保证线程间的可见性。在实际场景中,例如x86平台,在MESI协议的支持下,volatile是可以保证可见性的,这可以理解为一个巧合,利用了平台相关性,因此不具备平台可移植性

Java中也有volatile关键字,但作用完全不同,Java在语言层面就保证了volatile具有线程可见性

  • x86
    • 仅依赖MESI协议,可能也无法实现可见性。举个例子,当CPU1执行写操作时,要等到其他CPU将对应的缓存行设置成I状态后,写入才能完成,性能较差,于是CPU又引入了Store BufferMESI协议不感知Store Buffer),CPU1只需要将数据写入Store Buffer而不用等待其他CPU将缓存行设置成I状态就可以干其他事了
    • 为了解决上述问题,JVM使用了lock前缀的汇编指令,将当前Store Buffer中的所有数据(不仅仅是volatile修饰的变量)都通过MESI写入
  • 其他架构,采用其他方式来保证线程可见性这一承诺

参考:

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > volatile.cpp << 'EOF'
#include <stdint.h>

void with_volatile(volatile int32_t* src, int32_t* target) {
*target = *(src);
*target = *(src);
*target = *(src);
}

void without_volatile(int32_t* src, int32_t* target) {
*target = *(src);
*target = *(src);
*target = *(src);
}
EOF

gcc -o volatile.o -c volatile.cpp -O3 -lstdc++ -std=gnu++17
objdump -drwCS volatile.o

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
volatile.o:     文件格式 elf64-x86-64

Disassembly of section .text:

0000000000000000 <with_volatile(int volatile*, int*)>:
0: 8b 07 mov (%rdi),%eax
2: 89 06 mov %eax,(%rsi)
4: 8b 07 mov (%rdi),%eax
6: 89 06 mov %eax,(%rsi)
8: 8b 07 mov (%rdi),%eax
a: 89 06 mov %eax,(%rsi)
c: c3 retq
d: 0f 1f 00 nopl (%rax)

0000000000000010 <without_volatile(int*, int*)>:
10: 8b 07 mov (%rdi),%eax
12: 89 06 mov %eax,(%rsi)
14: c3 retq

3.7.1 如何验证volatile不具备可见性

这个问题比较难直接验证,我们打算用一种间接的方式来验证。假设写操作和读操作的性能开销之比为w/r = α。开两个线程,分别循环执行读操作和写操作,读写分别执行n次。统计读线程,相邻两次读操作,读取数值不同的次数为mm/r=β

  • 若满足可见性,那么β应该大致接近1/α

首先,测试atomicvolatile的读写性能

  • 测试时,会有一个额外的线程对atomicvolatile变量进行持续的读写操作
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
#include <benchmark/benchmark.h>

#include <atomic>
#include <thread>

std::atomic<uint64_t> atomic_value{0};
uint64_t volatile volatile_value = 0;

template <typename T>
static void random_write(T& value, std::atomic<bool>& stop) {
uint32_t tmp = 1;
while (!stop) {
value = tmp;
tmp++;
}
}

template <typename T>
static void random_read(T& value, std::atomic<bool>& stop) {
uint64_t tmp;
while (!stop) {
benchmark::DoNotOptimize(tmp = value);
}
}

static void atomic_read(benchmark::State& state) {
uint64_t tmp = 0;
std::atomic<bool> stop{false};
std::thread t([&]() { random_write(atomic_value, stop); });
for (auto _ : state) {
benchmark::DoNotOptimize(tmp = atomic_value);
}
stop = true;
t.join();
}

static void atomic_write(benchmark::State& state) {
uint64_t tmp = 0;
std::atomic<bool> stop{false};
std::thread t([&]() { random_read(atomic_value, stop); });
for (auto _ : state) {
benchmark::DoNotOptimize(atomic_value = tmp);
tmp++;
}
stop = true;
t.join();
}

static void volatile_read(benchmark::State& state) {
uint64_t tmp = 0;
std::atomic<bool> stop{false};
std::thread t([&]() { random_write(volatile_value, stop); });
for (auto _ : state) {
benchmark::DoNotOptimize(tmp = volatile_value);
}
stop = true;
t.join();
}

static void volatile_write(benchmark::State& state) {
uint64_t tmp = 0;
std::atomic<bool> stop{false};
std::thread t([&]() { random_read(volatile_value, stop); });
for (auto _ : state) {
benchmark::DoNotOptimize(volatile_value = tmp);
tmp++;
}
stop = true;
t.join();
}

BENCHMARK(atomic_read);
BENCHMARK(atomic_write);
BENCHMARK(volatile_read);
BENCHMARK(volatile_write);

BENCHMARK_MAIN();

结果如下:

  • 对于atomic<uint64_t>α = 31.8/1.01 = 31.49
  • 对于volatileα = 0.794/0.622 = 1.28
1
2
3
4
5
6
7
---------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------
atomic_read 1.01 ns 1.01 ns 617176522
atomic_write 31.8 ns 31.8 ns 22684714
volatile_read 0.622 ns 0.622 ns 1000000000
volatile_write 0.794 ns 0.793 ns 990971682

测试程序如下:

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
#include <atomic>
#include <iostream>
#include <thread>

constexpr uint64_t size = 1000000000;

template <class Tp>
inline void DoNotOptimize(Tp const& value) {
asm volatile("" : : "r,m"(value) : "memory");
}

template <typename T>
void test(T& value, const std::string& description) {
std::thread write_thread([&]() {
for (uint64_t i = 0; i < size; i++) {
DoNotOptimize(value = i);
}
});

std::thread read_thread([&]() {
uint64_t prev_value = 0;
uint64_t non_diff_cnt = 0;
uint64_t diff_cnt = 0;
uint64_t cur_value;
for (uint64_t i = 0; i < size; i++) {
DoNotOptimize(cur_value = value);

// These two statements have little overhead which can be ignored if enable -03
cur_value == prev_value ? non_diff_cnt++ : diff_cnt++;
prev_value = cur_value;
}
std::cout << description << ", β=" << static_cast<double>(diff_cnt) / size << std::endl;
});
write_thread.join();
read_thread.join();
}

int main() {
{
std::atomic<uint64_t> value = 0;
test(value, "atomic");
}
{
uint64_t volatile value = 0;
test(value, "volatile");
}
return 0;
}

结果如下:

  • 对于atomic而言,预测结果是1/α = 1/31.49 ≈ 0.032,实际为0.025,符合预测
  • 对于volatile而言,预测结果是1/α = 1/1.28 ≈ 0.781,实际为0.006,相距甚远。也就是说,写操作写的值,读操作大概率读不到,即不满足可见性
1
2
atomic, β=0.0246502
volatile, β=0.00602403

如果用Java进行上述等价验证,会发现实际结果与预期吻合,这里不再赘述

3.8 constexpr

3.8.1 if constexpr

编译期分支判断,一般用于泛型。如果在分支中使用的是不同类型的不同特性,那么普通的if是没法通过编译的,如下:

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>
#include <type_traits>

template <typename T>
struct Condition1 {
static T append(T left, T right) {
if (std::is_integral<T>::value) {
return left + right;
} else if (std::is_pointer<T>::value) {
return (*left) + (*right);
}
return T();
}
};

template <typename T>
struct Condition2 {
static T append(T left, T right) {
if constexpr (std::is_integral<T>::value) {
return left + right;
} else if constexpr (std::is_pointer<T>::value) {
return (*left) + (*right);
}
return T();
}
};
int main() {
// Condition1<int32_t>::append(1, 2);
Condition2<int32_t>::append(1, 2);
}

3.9 static_assert

编译期断言

3.10 throw与异常

throw关键字可以抛出任何对象,例如可以抛出一个整数

1
2
3
4
5
6
7
8
9
10
11
try {
throw 1;
} catch (int &i) {
std::cout << i << std::endl;
}

try {
// 保护代码
} catch (...) {
// 能处理任何异常的代码
}

3.11 placement new

placement new的功能就是在一个已经分配好的空间上,调用构造函数,创建一个对象

1
2
void *buf = // 在这里为buf分配内存
Class *pc = new (buf) Class();

4 模板

4.1 形参包

C++ 语言构造参考手册-形参包

模板形参包是接受零或更多模板实参(非类型、类型或模板)的模板形参。函数模板形参包是接受零或更多函数实参的函数形参

至少有一个形参包的模板被称作变参模板

模板形参包(出现于别名模版、类模板、变量模板及函数模板形参列表中)

  • 类型 ... Args(可选)
  • typename|class ... Args(可选)
  • template <形参列表> typename(C++17)|class ... Args(可选)

函数参数包(声明符的一种形式,出现于变参函数模板的函数形参列表中)

  • Args ... args(可选)

形参包展开(出现于变参模板体中),展开成零或更多模式的逗号分隔列表。模式必须包含至少一个形参包

  • 模式 ...

4.2 折叠表达式

C++ 语言构造参考手册-折叠表达式

格式如下:

  • 一元右折叠:( 形参包 op ... )
  • 一元左折叠:( ... op 形参包 )
  • 二元右折叠:( 形参包 op ... op 初值 )
  • 二元左折叠:( 初值 op ... op 形参包 )

形参包:含未展开的形参包且其顶层不含有优先级低于转型(正式而言,是 转型表达式)的运算符的表达式。说人话,就是表达式

31个合法op如下(二元折叠的两个op必须一样):

  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. ->

4.3 非类型模板参数

我们还可以在模板中定义非类型参数,一个非类型参数表示一个值而非一个类型。当一个模板被实例化时,非类型参数被编译器推断出的值所代替,这些值必须是常量表达式,从而允许编译器在编译时实例化模板。一个非类型参数可以是一个整型(枚举可以理解为整型),或是一个指向对象或函数类型的指针或引用

  • 绑定到非类型整型参数的实参必须是一个常量表达式
  • 绑定到指针或引用非类型参数必须具有静态的生命周期
  • 在模板定义内,模板非类型参数是一个常量值,在需要常量表达式的地方,可以使用非类型参数,例如指定数组大小
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
enum BasicType {
INT,
DOUBLE
};

template<BasicType BT>
struct RuntimeTypeTraits {
};

// 特化
template<>
struct RuntimeTypeTraits<INT> {
using Type = int;
};

// 特化
template<>
struct RuntimeTypeTraits<DOUBLE> {
using Type = double;
};

int main() {
// 编译期类型推断,value的类型是int
RuntimeTypeTraits<INT>::Type value = 100;
}

4.4 模板形参无法推断

通常,在::左边的模板形参是无法进行推断的(这里的::特指用于连接两个类型),例如下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename T>
void func(const typename T::type &obj) {
// ...
}

struct Int {
using type = int;
};

struct Long {
using type = long;
};

int main() {
func(1); // compile error
func<Int>(1);
func<Long>(2);
}

4.5 typename消除歧义

什么情况下会有歧义?。例如foo* ptr;

  • foo是个类型,那么该语句就是个声明语句,即定义了一个类型为foo*变量
  • foo是个变量,那么该语句就是个表达式语句,即对foo以及ptr进行*运算
  • 编译器无法分辨出是上述两种情况的哪一种,因此可以显式使用typename来告诉编译器foo是个类型

对于模板而言,例如T::value_type,编译器同样无法确定T::value_type是个类型还是不是类型。因为类作用域运算符::可以访问类型成员也可以访问静态成员。而编译器默认会认为T::value_type这种形式默认不是类型

示例1:

1
2
3
4
5
6
7
8
9
// 下面这个会编译失败
template<typename T>
T::value_type sum(const T &container) {
T::value_type res = {};
for (const auto &item: container) {
res += item;
}
return res;
}

上面的代码有2处错误:

  1. 需要用typename显式指定返回类型T::value_type
  2. 需要用typename显式指定res的声明类型

修正后:

1
2
3
4
5
6
7
8
template<typename T>
typename T::value_type sum(const T &container) {
typename T::value_type res = {};
for (const auto &item: container) {
res += item;
}
return res;
}

4.6 template消除歧义

什么情况下会有歧义?。例如container.emplace<int>(1);

  • container.emplace是个成员变量,那么<可以理解成小于号
  • container.emplace是个模板,那么<可以理解成模板形参的括号

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Container {
public:
template<typename T>
void emplace(T value) {
std::cout << "emplace value: " << value << std::endl;
}
};

// 下面这个会编译失败
template<typename T>
void add(T &container) {
container.emplace<int>(1);
}

上面的代码有1处错误:

  1. 编译器无法确定container.emplace是什么含义

修正后:

1
2
3
4
template<typename T>
void add(T &container) {
container.template emplace<int>(1);
}

示例2:

1
2
3
4
5
6
7
8
9
10
template<typename T>
class Foo {
template<typename C>
using container = std::vector<C>;
};

template<typename T>
void bar() {
T::container<int> res;
}

上面的代码有1处错误:

  1. 编译器无法确定T::container是什么含义
  2. 需要用typename显式指定T::container<int>是个类型

修正后:

1
2
3
4
5
6
7
8
9
10
template<typename T>
class Foo {
template<typename C>
using container = std::vector<C>;
};

template<typename T>
void bar() {
typename T::template container<int> res;
}

4.7 template参数列表中定义类型别名

语法上,我们是无法在template的参数列表中定义别名的(无法使用using)。但是我们可以通过定义有默认值的类型形参来实现类似类型别名的功能,如下:

1
2
3
4
5
template <typename HashMap, typename KeyType = typename HashMap::key_type,
typename ValueType = typename HashMap::mapped_type>
ValueType& get(HashMap& map, const KeyType& key) {
return map[key];
}

4.8 非模板子类访问模板父类中的成员

  • 方式1:MemberName
  • 方式2:this->MemberName
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
struct Base {
T data;
};

struct Derive : Base<int> {
void set_data_1(const int& other) { data = other; }
void set_data_2(const int& other) { this->data = other; }
};

int main() {
Derive t;
t.set_data_1(1);
t.set_data_2(2);
return 0;
}

4.9 模板子类访问模板父类中的成员

  • 访问方式1:ParentClass<Template Args...>::MemberName
  • 访问方式2:this->MemberName
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
struct Base {
T data;
};

template <typename T>
struct Derive : Base<T> {
void set_data_1(const T& data) { Base<T>::data = data; }
void set_data_2(const T& data) { this->data = data; }
};

int main() {
Derive<int> t;
t.set_data_1(5);
t.set_data_2(6);
return 0;
}

5 __attribute__

Compiler-specific Features

5.1 aligned

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
#include <iostream>

#define FOO_WITH_ALIGN(SIZE) \
struct Foo_##SIZE { \
int v; \
} __attribute__((aligned(SIZE)))

#define PRINT_SIZEOF_FOO(SIZE) std::cout << "Foo_##SIZE's size=" << sizeof(Foo_##SIZE) << std::endl;

FOO_WITH_ALIGN(1);
FOO_WITH_ALIGN(2);
FOO_WITH_ALIGN(4);
FOO_WITH_ALIGN(8);
FOO_WITH_ALIGN(16);
FOO_WITH_ALIGN(32);
FOO_WITH_ALIGN(64);
FOO_WITH_ALIGN(128);
FOO_WITH_ALIGN(256);

int main() {
PRINT_SIZEOF_FOO(1);
PRINT_SIZEOF_FOO(2);
PRINT_SIZEOF_FOO(4);
PRINT_SIZEOF_FOO(8);
PRINT_SIZEOF_FOO(16);
PRINT_SIZEOF_FOO(32);
PRINT_SIZEOF_FOO(64);
PRINT_SIZEOF_FOO(128);
PRINT_SIZEOF_FOO(256);
return 1;
}

6 ASM

gcc-online-docs

6.1 Basic Asm

6.2 Extended Asm

GCC设计了一种特有的嵌入方式,它规定了汇编代码嵌入的形式和嵌入汇编代码需要由哪几个部分组成,格式如下:

  • 汇编语句模板是必须的,其余三部分是可选的
1
2
3
4
5
6
7
8
9
10
asm asm-qualifiers ( AssemblerTemplate 
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])

asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
: InputOperands
: Clobbers
: GotoLabels)

Qualifiers,修饰符:

  • volatile:禁止编译器优化
  • inline
  • goto

AssemblerTemplate,汇编语句模板:

  • 汇编语句模板由汇编语句序列组成,语句之间使用;\n\n\t分开
  • 指令中的操作数可以使用占位符,占位符可以指向OutputOperandsInputOperandsGotoLabels
  • 指令中使用占位符表示的操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节
  • 对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母
    • b代表低字节
    • h代表高字节
    • 例如:%h1

OutputOperands,输出操作数:

  • 操作数之间用逗号分隔
  • 每个操作数描述符由限定字符串(Constraints)和C语言变量或表达式组成

InputOperands,输入操作数:

  • 操作数之间用逗号分隔
  • 每个操作数描述符由限定字符串(Constraints)和C语言变量或表达式组成

Clobbers,描述部分:

  • 用于通知编译器我们使用了哪些寄存器或内存,由逗号格开的字符串组成
  • 每个字符串描述一种情况,一般是寄存器名;除寄存器外还有memory。例如:%eax%ebxmemory

Constraints,限定字符串(下面仅列出常用的):

  • m:内存
  • o:内存,但是其寻址方式是偏移量类型
  • v:内存,但寻址方式不是偏移量类型
  • r:通用寄存器
  • i:整型立即数
  • g:任意通用寄存器、内存、立即数
  • p:合法指针
  • =:write-only
  • +:read-write
  • &:该输出操作数不能使用过和输入操作数相同的寄存器

示例1:

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 <stddef.h>
#include <stdint.h>

#include <iostream>

struct atomic_t {
volatile int32_t a_count;
};

static inline int32_t atomic_read(const atomic_t* v) {
return (*(volatile int32_t*)&(v)->a_count);
}

static inline void atomic_write(atomic_t* v, int32_t i) {
v->a_count = i;
}

static inline void atomic_add(atomic_t* v, int32_t i) {
__asm__ __volatile__(
"lock;"
"addl %1,%0"
: "+m"(v->a_count)
: "ir"(i));
}

static inline void atomic_sub(atomic_t* v, int32_t i) {
__asm__ __volatile__(
"lock;"
"subl %1,%0"
: "+m"(v->a_count)
: "ir"(i));
}

static inline void atomic_inc(atomic_t* v) {
__asm__ __volatile__(
"lock;"
"incl %0"
: "+m"(v->a_count));
}

static inline void atomic_dec(atomic_t* v) {
__asm__ __volatile__(
"lock;"
"decl %0"
: "+m"(v->a_count));
}

int main() {
atomic_t v;
atomic_write(&v, 0);
atomic_add(&v, 10);
atomic_sub(&v, 5);
atomic_inc(&v);
atomic_dec(&v);
std::cout << atomic_read(&v) << std::endl;
return 0;
}

示例2:

  • 这个程序是没法跑的,因为cli指令必须在内核态执行
  • hal_save_flags_cli:将eflags寄存器的值保存到内存中,然后关闭中断
  • hal_restore_flags_sti:将hal_save_flags_cli保存在内存中的值恢复到eflags寄存器中
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
#include <stddef.h>
#include <stdint.h>

#include <iostream>

typedef uint32_t cpuflg_t;

static inline void hal_save_flags_cli(cpuflg_t* flags) {
__asm__ __volatile__(
"pushf;" // 把eflags寄存器的值压入当前栈顶
"cli;" // 关闭中断,会改变eflags寄存器的值
"pop %0" // 把当前栈顶弹出到eflags为地址的内存中
: "=m"(*flags)
:
: "memory");
}

static inline void hal_restore_flags_sti(cpuflg_t* flags) {
__asm__ __volatile__(
"push %0;" // 把flags为地址处的值寄存器压入当前栈顶
"popf" // 把当前栈顶弹出到eflags寄存器中
:
: "m"(*flags)
: "memory");
}

void foo(cpuflg_t* flags) {
hal_save_flags_cli(flags);
std::cout << "step1: foo()" << std::endl;
hal_restore_flags_sti(flags);
}

void bar() {
cpuflg_t flags;
hal_save_flags_cli(&flags);
foo(&flags);
std::cout << "step2: bar()" << std::endl;
hal_restore_flags_sti(&flags);
}

int main() {
bar();
return 0;
}

示例3:linux内核大量用到了asm,具体可以参考linux-asm

7 Policy

7.1 Pointer Stability

pointer stability通常用于描述容器。当我们说一个容器是pointer stability时,是指,当某个元素添加到容器之后、从容器删除之前,该元素的内存地址不变,也就是说,该元素的内存地址,不会受到容器的添加删除元素、扩缩容、或者其他操作影响

absl

容器 是否pointer stability
std::vector
std::list
std::map
std::unordered_map
std::set
std::unordered_set
absl::flat_hash_map
absl::flat_hash_set
absl::node_hash_map
absl::node_hash_set
phmap::flat_hash_map
phmap::flat_hash_set
phmap::node_hash_map
phmap::node_hash_set

7.2 Exception Safe

Wiki-Exception safety

exception safety的几个级别:

  1. No-throw guarantee:承诺不会对外抛出任何异常。方法内部可能会抛异常,但都会被正确处理
  2. Strong exception safety:可能会抛出异常,但是承诺不会有副作用,所有对象都会恢复到调用方法时的初始状态
  3. Basic exception safety:可能会抛出异常,操作失败的部分可能会导致副作用,但所有不变量都会被保留。任何存储的数据都将包含可能与原始值不同的有效值。资源泄漏(包括内存泄漏)通常通过一个声明所有资源都被考虑和管理的不变量来排除
  4. No exception safety:不承诺异常安全

7.3 RAII

RAII, Resource Acquisition is initialization,即资源获取即初始化。典型示例包括:std::lock_guarddefer。简单来说,就是在对象的构造方法中初始化资源,在析构函数中销毁资源。而构造函数与析构函数的调用是由编译器自动插入的,减轻了开发者的心智负担

1
2
3
4
5
6
7
8
9
10
template <class DeferFunction>
class DeferOp {
public:
explicit DeferOp(DeferFunction func) : _func(std::move(func)) {}

~DeferOp() { _func(); };

private:
DeferFunction _func;
};

8 Tips

8.1 如何在类中定义静态成员

在类中声明静态成员,在类外定义(赋值)静态成员,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建源文件
cat > main.cpp << 'EOF'
#include <iostream>

class Demo {
public:
static size_t BUFFER_LEN;
};

size_t Demo::BUFFER_LEN = 5;

int main() {
std::cout << Demo::BUFFER_LEN << std::endl;
}
EOF

# 编译
gcc -o main main.cpp -lstdc++ -Wall

# 执行
./main

8.2 初始化

8.2.1 初始化列表

  1. 对于内置类型,直接进行值拷贝。使用初始化列表还是在构造函数体中进行初始化没有差别
  2. 对于类类型
    • 在初始化列表中初始化:调用的是拷贝构造函数或者移动构造函数
    • 在构造函数体中初始化:虽然在初始化列表中没有显式指定,但是仍然会用默认的构造函数来进行初始化,然后在构造函数体中使用拷贝或者移动赋值运算符
  3. 哪些东西必须放在初始化列表中
    • 常量成员
    • 引用类型
    • 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝或者移动构造函数初始化
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
#include <iostream>

class A {
public:
A() {
std::cout << "A's default constructor" << std::endl;
}

A(int a) : _a(a), _b(0) {
std::cout << "A's (int) constructor" << std::endl;
}

A(int a, int b) : _a(a), _b(b) {
std::cout << "A's (int, int) constructor" << std::endl;
}

A(const A &a) : _a(a._a), _b(a._b) {
std::cout << "A's copy constructor" << std::endl;
}

A(A &&a) : _a(a._a), _b(a._b) {
std::cout << "A's move constructor" << std::endl;
}

A &operator=(const A &a) {
std::cout << "A's copy assign operator" << std::endl;
this->_a = a._a;
this->_b = a._b;
return *this;
}

A &operator=(A &&a) noexcept {
if (this == &a) {
return *this;
}
std::cout << "A's move assign operator" << std::endl;
this->_a = a._a;
this->_b = a._b;
return *this;
}

private:
int _a;
int _b;
};

class B {
public:
B(A &a) : _a(a) {}

B(A &a, std::nullptr_t) {
this->_a = a;
}

B(A &&a) : _a(std::move(a)) {}

B(A &&a, std::nullptr_t) {
this->_a = std::move(a);
}

private:
A _a;
};

int main() {
std::cout << "============(create a)============" << std::endl;
A a(1, 2);
std::cout << "\n============(create b1)============" << std::endl;
B b1(a);
std::cout << "\n============(create b2)============" << std::endl;
B b2(a, nullptr);
std::cout << "\n============(create b3)============" << std::endl;
B b3(static_cast<A &&>(a));
std::cout << "\n============(create b4)============" << std::endl;
B b4(static_cast<A &&>(a), nullptr);
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
============(create a)============
A's (int, int) constructor

============(create b1)============
A's copy constructor

============(create b2)============
A's default constructor
A's copy assign operator

============(create b3)============
A's move constructor

============(create b4)============
A's default constructor
A's move assign operator

8.2.2 各种初始化类型

  1. 默认初始化:type variableName;
  2. 直接初始化/构造初始化(至少有1个参数):type variableName(args);
  3. 列表初始化:type variableName{args};
    • 本质上列表初始化会调用相应的构造函数(匹配参数类型以及参数数量)来进行初始化
    • 它的好处之一是可以简化return语句,可以直接return {args};
  4. 拷贝初始化:
    • type variableName = otherVariableName,本质上调用了拷贝构造函数
    • type variableName = <type (args)>,其中<type (args)>指的是返回类型为type的函数。看起来会调用拷贝构造函数,但是编译器会对这种形式的初始化进行优化,也就是只有函数内部调用了构造函数(如果有的话),而=并未调用任何构造函数
  5. 值初始化:type variableName()
    • 对于内置类型,初始化为0或者nullptr
    • 对于类类型,等同于默认初始化。测试发现并未调用任何构造函数
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>

class A {
public:
A() {
std::cout << "A's default constructor" << std::endl;
}

A(int a) : _a(a), _b(0) {
std::cout << "A's (int) constructor" << std::endl;
}

A(int a, int b) : _a(a), _b(b) {
std::cout << "A's (int, int) constructor" << std::endl;
}

A(const A &a) : _a(a._a), _b(a._b) {
std::cout << "A's copy constructor" << std::endl;
}

A(A &&a) : _a(a._a), _b(a._b) {
std::cout << "A's move constructor" << std::endl;
}

A &operator=(const A &a) {
std::cout << "A's copy assign operator" << std::endl;
this->_a = a._a;
this->_b = a._b;
return *this;
}

A &operator=(A &&a) noexcept {
if (this == &a) {
return *this;
}
std::cout << "A's move assign operator" << std::endl;
this->_a = a._a;
this->_b = a._b;
return *this;
}

private:
int _a;
int _b;
};

A createA(int argNum) {
if (argNum == 0) {
return {};
} else if (argNum == 1) {
return {1};
} else {
return {1, 2};
}
}

int main() {
std::cout << "============(默认初始化 a1)============" << std::endl;
A a1;
std::cout << "\n============(直接初始化 a2)============" << std::endl;
A a2(1);
std::cout << "\n============(直接初始化 a3)============" << std::endl;
A a3(1, 2);
std::cout << "\n============(列表初始化 a4)============" << std::endl;
A a4 = {};
std::cout << "\n============(列表初始化 a5)============" << std::endl;
A a5 = {1};
std::cout << "\n============(列表初始化 a6)============" << std::endl;
A a6 = {1, 2};
std::cout << "\n============(拷贝初始化 a7)============" << std::endl;
A a7 = a6;
std::cout << "\n============(拷贝初始化 a8)============" << std::endl;
A a8 = createA(0);
std::cout << "\n============(拷贝初始化 a9)============" << std::endl;
A a9 = createA(1);
std::cout << "\n============(拷贝初始化 a10)============" << std::endl;
A a10 = createA(2);
std::cout << "\n============(值初始化 a11)============" << std::endl;
A a11();
}

输出:

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
============(默认初始化 a1)============
A's default constructor

============(直接初始化 a2)============
A's (int) constructor

============(直接初始化 a3)============
A's (int, int) constructor

============(列表初始化 a4)============
A's default constructor

============(列表初始化 a5)============
A's (int) constructor

============(列表初始化 a6)============
A's (int, int) constructor

============(拷贝初始化 a7)============
A's copy constructor

============(拷贝初始化 a8)============
A's default constructor

============(拷贝初始化 a9)============
A's (int) constructor

============(拷贝初始化 a10)============
A's (int, int) constructor

============(值初始化 a11)============

8.3 指针

8.3.1 成员函数指针

成员函数指针需要通过.*或者->*运算符进行调用

  • 类内调用:(this->*<name>)(args...)
  • 类外调用:(obj.*obj.<name>)(args...)或者(pointer->*pointer-><name>)(args...)
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>
#include <memory>

class Demo {
public:
explicit Demo(bool flag) {
if (flag) {
say_hi = &Demo::say_hi_1;
} else {
say_hi = &Demo::say_hi_2;
}
}

void invoke_say_hi() {
(this->*say_hi)();
}

void (Demo::*say_hi)() = nullptr;

void say_hi_1();

void say_hi_2();
};

void Demo::say_hi_1() {
std::cout << "say_hi_1" << std::endl;
}

void Demo::say_hi_2() {
std::cout << "say_hi_2" << std::endl;
}

int main() {
Demo demo1(true);

// invoke inside class
demo1.invoke_say_hi();

// invoke outside class with obj
(demo1.*demo1.say_hi)();

// invoke outside class with pointer
Demo *p1 = &demo1;
(p1->*p1->say_hi)();

// invoke outside class with pointer
std::shared_ptr<Demo> sp1 = std::make_shared<Demo>(false);
(sp1.get()->*sp1->say_hi)();
}

8.4 引用

8.4.1 引用赋值

引用只能在定义处初始化

1
2
3
4
5
6
7
8
9
10
11
int main() {
int a = 1;
int b = 2;

int &ref = a;
ref = b; // a的值变为2

std::cout << "a=" << a << std::endl;
std::cout << "b=" << b << std::endl;
std::cout << "ref=" << ref << std::endl;
}

结果:

1
2
3
a=2
b=2
ref=2

8.5 mock class

有时在测试的时候,我们需要mock一个类的实现,我们可以在测试的cpp文件中实现这个类的所有方法(注意,必须是所有方法),就能够覆盖原有库文件中的实现。下面以一个例子来说明

目录结构如下

1
2
3
4
5
6
7
.
├── lib
│   ├── libperson.a
│   ├── person.cpp
│   ├── person.h
│   └── person.o
└── main.cpp

lib/person.h内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
#pragma once

#include <string>

class Person {
public:
void work();

void sleep();

void eat();
};

lib/person.cpp内容如下:

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

void Person::work() {
std::cout << "work" << std::endl;
}

void Person::sleep() {
std::cout << "sleep" << std::endl;
}

void Person::eat() {
std::cout << "eat" << std::endl;
}

编译person.cpp生成链接文件,并生成.a归档文件

1
2
3
4
5
# 指定-c参数,只生成目标文件(person.o),不进行链接
g++ person.cpp -c -std=gnu++11

# 生成归档文件
ar crv libperson.a person.o

main.cpp内容如下:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include "lib/person.h"

int main() {
Person person;
person.work();
person.sleep();
person.eat();
};

编译main.cpp并执行

1
2
3
4
5
6
7
8
9
10
11
# 编译
# -L参数将lib目录加入到库文件的扫描路径
# -l参数指定需要链接的库文件
g++ -o main main.cpp -std=gnu++11 -L lib -lperson

# 执行,输出如下
./main

work
sleep
eat

接下来,我们修改main.cpp,覆盖原有的worksleepeat方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include "lib/person.h"

void Person::work() {
std::cout << "mock work" << std::endl;
}

void Person::sleep() {
std::cout << "mock sleep" << std::endl;
}

void Person::eat() {
std::cout << "mock eat" << std::endl;
}

int main() {
Person person;
person.work();
person.sleep();
person.eat();
};

编译main.cpp并执行

1
2
3
4
5
6
7
8
9
10
11
# 编译
# -L参数将lib目录加入到库文件的扫描路径
# -l参数指定需要链接的库文件
g++ -o main main.cpp -std=gnu++11 -L lib -lperson

# 执行,输出如下,可以发现,都变成了mock版本
./main

mock work
mock sleep
mock eat

然后,我们继续修改main.cpp,删去其中一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include "lib/person.h"

void Person::work() {
std::cout << "mock work" << std::endl;
}

void Person::sleep() {
std::cout << "mock sleep" << std::endl;
}

// void Person::eat() {
// std::cout << "mock eat" << std::endl;
// }

int main() {
Person person;
person.work();
person.sleep();
person.eat();
};

编译main.cpp(编译会失败)

1
2
3
4
5
6
7
8
9
10
11
12
# 编译
# -L参数将lib目录加入到库文件的扫描路径
# -l参数指定需要链接的库文件
g++ -o main main.cpp -std=gnu++11 -L lib -lperson

lib/libperson.a(person.o):在函数‘Person::work()’中:
person.cpp:(.text+0x0): Person::work() 的多重定义
/tmp/ccfhnlz4.o:main.cpp:(.text+0x0):第一次在此定义
lib/libperson.a(person.o):在函数‘Person::sleep()’中:
person.cpp:(.text+0x2a): Person::sleep() 的多重定义
/tmp/ccfhnlz4.o:main.cpp:(.text+0x2a):第一次在此定义
collect2: 错误:ld 返回 1

9 FAQ

9.1 为什么free和delete释放内存时不用指定大小

How does free know how much to free?

分配内存时,除了分配指定的内存之外,还会分配一个header,用于存储一些信息,例如

  • size
  • special marker
  • checksum
  • pointers to next/previous block
1
2
3
4
5
6
7
8
____ The allocated block ____
/ \
+--------+--------------------+
| Header | Your data area ... |
+--------+--------------------+
^
|
+-- The address you are given

9.2 形参类型是否需要左右值引用

9.3 返回类型是否需要左右值引用

10 参考