TC++PL
C++ 程序设计语言(第 1 - 3 部分) / The C++ Programming Language / 第四版练习题 / C++11 Origin lib / 第三版源码 /
概念
graph TB
A[ability]
B[const<br/>constexpr]
C[default ctor]
D[RAII]
E[concepts]
F[variadic]
G[ctor inherent]
H[stream itr]
I[thread]
J[unique_lock<br/>lock_gurad]
K[template]
L[Class]
M[future<br/>promise<br/>packaged_task]
N[literal]
P[alignof]
Q[override<br/>new]
R[attr<br/>noreturn]
T[ODR]
A-->B
A-->E
L-->C
L-->D
K-->F
L-->G
A-->H
E-->I
I-->J
E-->L
E-->K
I-->M
A-->N
A-->P
A-->T
A-->R
A-->Q
-
const vs constexpr。[利用 constexpr 实现编译时双向映射](#constexpr mapping)
const,编译器保证不会有代码直接修改变量,但编码人员可以使用特殊的技巧欺骗编译器从而修改变量
constexpr,编译时求值的常量,可以放到内存的只读区域,const 保证这一点
-
不需要参数就能调用的构造函数被称为默认构造函数
-
RAII,类似于 vector 这类资源管理类在构造的时候分配资源,在析构的时候释放资源
-
可变模板参数,区别对待第一个和其他参数,定义一个参数为空的函数来处理边界条件
-
构造函数继承:
using vector<T>::vector
。继承 vector 可以快速实现一个自动边界检查的 Vec(4.4.1) -
流迭代器。流也有序列的概念,把迭代器用在流上面也是合理的。流迭代器的操作一般等价于对应的 IO 操作。假设
io_it
是输出流的迭代器,那么*io_it=123
与cout << 123
是等价的 -
容器算法。标准库中大部分算法都是基于迭代器的,迭代器作为容器和算法之间的桥梁。直接将算法应用到容器对象上其实也是不错的选择,不过灵活性就差了一些
namespace estd { // extend std template<typename C> void sort(C& c) { std::sort(c.begin(), c.end()); } };
-
unique_lock
,和lock_guard
相比前者有更多的属性与功能,如下所示unique_lock<mutex> lck1 {m1,defer_lock}; // defer_lock: don’t yet try to acquire the mutex unique_lock<mutex> lck2 {m2,defer_lock}; unique_lock<mutex> lck3 {m3,defer_lock}; // ... lock(lck1,lck2,lck3); // acquire all three locks
-
future && promise &&
packaged_task
,期货、承诺与包装。包装用于将普通函数转换为future与promise的形式。async 函数是左边三个对象的封装 -
字面值,字符串字面值前缀 / 整数的前后缀 / 浮点数的前后缀 / User-defined literals /
- C++14 以后定义了若让非常方便的用户自定义字面值比如:
24h
/60s
/60min
/28d
等等(std::literals::chrono_literals
)
- C++14 以后定义了若让非常方便的用户自定义字面值比如:
-
重载 new 可以实现对象存储位置的指定
-
C++11 以后提供了属性,例如
[[noreturn]]
,以供不同平台进行优化 -
ODR,one define rule,一般情况下 C++ 中的类、变量、模板等只能定义一次,但对于类、模板和内联函数而言出现两次定义是可以的:他们出现在不同编译单元 && 代码逐单词对应 && 单词在不同编译单元含义一致
- ODR 是 header only 的基础
-
其他
变量的生命周期
graph TB
A[Life of Obj]
B[auto/free/temp]
C[static]
F[thread local]
G[const& temp]
H[ctor/dtor]
J[default/delete]
K[any declable]
A-->B
A-->C
B-->G
A-->F
A-->H
H-->J
J-->K
style K fill:#f9f,stroke:#333
- auto,栈空间变量
- static,全局静态,多线程场景下有竞争风险
- free,new/delete,堆空间变量
- temp,计算的中间结果、const 实参引用的对象等。temp 一般也是 auto 对象
- thread local,随线程创建而创建,随线程销毁而销毁
构造与析构
- 除构造函数外,类成员函数、变量不能与类名相同
- 注意自赋值检测
- 默认函数生成的原则如下(17.6):
- Big5,如果定义了其中一个,就应该设置其他 5 个,使用 default/delete
If the programmer declares any constructor for a class, the default constructor is not generated for that class
If the programmer declares a copy operation, a move operation, or a destructor for a class, no copy operation, move operation, or destructor is generated for that class
Unfortunately, the second rule is only incompletely enforced: for backward compatibility, copy constructors and copy assignments are generated even if a destructor is defined. However, that generation is deprecated in the ISO standard (§iso.D), and you should expect a modern compiler to warn against it.
default && delete
delete 可以删除任何我们能声明的函数,如下:
-
禁止某些类型的模板特例化
template<class T> T∗ clone(T∗ p) { return new T{∗p}; }; Foo∗ clone(Foo∗) = delete; // don’t try to clone a Foo
-
禁止某些类型的自动类型转换
struct Z { Z(double); // can initialize with a double Z(int) = delete; // but not with an integer };
-
其他
Operator
graph TB
A[OPs]
B[List Params]
C[def ctor/init-list/Ctor params/Aggregate]
F[ctor]
G[def ctor]
H[no params]
I[init-list ctor]
A-->B
B-->C
A-->F
F-->G
G-->H
F-->I
style C fill:#f9f,stroke:#333
- 列表可以用在三个地方,构造函数;聚合体初始化;初始化列表
- 优先尝试使用初始化列表构造;然后尝试普通构造函数;最后尝试聚合体构造
- 包含单一
std::initial_list
参数的构造函数被称为初始化列表构造函数 ,默认构造与初始化列表构造优先
性能与资源
graph TB
A[effiency]
B[合理inline]
C[pbr vs pbv]
D[thread<br/>share vs pass]
E[vector<br/>valarray]
F[struct<br/>member]
G[bigger<br/>header]
H[bit-field]
I[lambda]
J[ns val]
K[multable]
L[this]
M[init list]
N[handle]
A-->B
A-->C
A-->D
A-->E
A-->F
F-->G
F-->H
A-->I
I-->J
I-->K
I-->L
A-->M
M-->N
- inline,合理的选择类 inline 方法
- 合理的选择 pass-by-ref 和 pass-by-val,因为 CPU Cache,前者的实现效率并不一定比后者好
- 多线程领域里尽量使用 msg-passing 而不是 msg-sharing
- valarray 是专门用于向量计算的工具,其形式与 vector 类似,但对数值计算做了优化
- struct
- 如果想紧凑的存储结构体数据,把尺寸较大的成员放在前面;
- 位域(bit-field)是减少结构体占用内存空间的一个手段,因为使用位域的代码片段比常规代码长,所以只用在大量使用位域对象时才会节省内存空间
- lambda 表达式可以直接捕获 namespace 变量(包含全局变量),捕获类内成员时需要手动捕获 this
- 如果 lambda 什么也不捕获,则可以将 lambda 直接赋值给一个对应类型的函数指针
- 初始化列表对象(
std::initial_list
)是一个 handle ,内部有两个指针指向对象数组,且元素不可以修改 - 其他
依赖于实现的操作
类型转换
char c = 255;
// i 的值依赖于 char 的实现方式,如果 char 是有符号的则 i 为 -1;如果 char 无符号则 i 为 255
int i = c;
异常
graph TB
A[except]
B[any class]
C[copyable]
D[time csm]
E[Expected class]
F[exception safe]
G[B/S/N]
H[dtor nthrow]
I[scope_gurad<br/>finally]
K[fun try block<br/>ctor try block]
L[thread]
M[call terminate]
N[usage]
O[except ptr]
B-->C
D-->E
A-->F
F-->G
F-->H
F-->I
A-->K
L-->M
A-->N
N-->L
N-->D
N-->B
L-->O
-
异常可以是任意类型对象(甚至是 int),只要可以拷贝。异常在被捕获前可能被拷贝了多次
- 异常一般在抛出的时候被拷贝,所以异常对象有切片的可能
-
抛出异常与捕获异常的耗时是无法严格保证的,所以在嵌入式领域一般不使用异常机制
- 对时间非常敏感的函数可以使用异常的替代品,比如:https://github.com/TartanLlama/expected
-
异常抛出后依系统依旧处于有效状态,则称系统为异常安全
- 异常的三个层次:基本(Basic),不泄露资源;强保证(Strong),要么成功要么没影响;不抛异常(Nothrow)
scope_guard
可以实现 java 中 finally 的功能
-
C++提供了函数体 try 和构造函数 try
int main() try{ ... } catch(...){ ... }
-
可以使用 exception pointer 传输异常,
std::current_exception()
-
其他
代码示例
概念
constexpr mapping
能写到代码中的配置一般都不会太多,所以直接使用线性查找速度也不慢,如果考虑到缓存(cache)的特性,相比于 map,数组查找会更快
using CchrpIPair = std::pair<const char*, int>;
constexpr CchrpIPair test_map_vec_g[] = {{"zero", 0}, {"one", 1}, {"two", 2},
{"three", 3}, {"four", 4}, {"five", 5}};
int str2int(const char* ch_ptr) {
auto end_it = std::end(test_map_vec_g);
// 改变 find_if 中的 functor 就可以实现 int2str
auto it = find_if(std::begin(test_map_vec_g), end_it, [ch_ptr](const CchrpIPair& p) {
return std::strcmp(ch_ptr, p.first) == 0 ? true : false;
});
return it == end_it ? -1 : it->second;
}