C++11新特性学习
语言可用性的强化
C++11 引入了 nullptr 关键字,专门用来区分空指针、0。
C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式。
从C++14开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句。
C++14 可以 C++11不可以
constexpr int fibonacci(const int n) {
if(n == 1) return 1;
if(n == 2) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
C++17 将 constexpr 这个关键字引入到 if 语句中,允许在代码中声明常量表达式的判断条件。
// >= C++17
#include <iostream>
template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) {
return t + 1;
} else {
return t + 0.001;
}
}
int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}
区间for循环,懂的都懂。
final和override
:final关键字来限制某个类不能被继承,或者某个虚函数不能被重写,override关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,和final一样这个关键字要写到方法的后面。
初始化列表
std::initializer_list:允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁。
public:
void foo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin();
it != list.end(); ++it) vec.push_back(*it);
}
magicFoo.foo({6,7,8,9});
结构化绑定:C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。
类型推导:auto decltype 尾返回类型推导
使用auto的一个典型方法是迭代器,可以少写很多。auto不可以用作数组类型推导。
decltype关键字是为了解决auto关键字只能对变量进行类型推导的缺陷而产生的,和typeof相似。
auto x = 1;
auto y = 2;
decltype(x+y) z;
尾置返回类型推导:和auto 和 decltype结合,就变得花样多起来了
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
template <typename F, typename... Args>
auto submit(F &&f, Args &&...args) -> std::future<decltype(f(args...))>{
return ...; //最后根据返回值来判断最终的auto类型。
}
C++14开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:
template<typename T, typename U>
auto add3(T x, U y){
return x + y;//这个时候推导,牛
}
模板
C++11之后,容器的嵌套模板是可以使用的。
可以用using 声明模板的特殊实例方法,不建议用,降低代码可读性。
变长参数模板
:
template<typename... Ts>
class DarkMagic;
class DarkMagic<> nothing;
如果不希望产生的模板的参数个数为为0,可以手动定义一个模板参数:
template<typename Require, typename... Args>
class Magic;
可以用sizeof… 来计算参数的个数:
template<typename... Ts>
void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}
对参数进行解包
- 递归模板函数:有一个递归出口。
- 变参模板展开:递归的方法必须有一个递归出口,也是一个缺点,C++17后,可以采用变参模板展开的方法来做。
- 初始化列表展开:
递归模板函数
#include <iostream>
template<typename T0>
void printf1(T0 value) { //递归出口
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}
初始化列表展开
// >= C++17
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0)
printf2(t...);
}
面向对象
委托构造:构造函数可以在同一个类中,一个构造函数调用另一个构造函数,从而达到简化代码的目的。
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = value;
}
};
继承构造:利用关键字 using。
override和final:引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的其函数签名一致的虚函数,否则将无法通过编译。final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。
struct Base {
virtual void foo(int);
};
struct SubClass: Base {
virtual void foo(int) override; // 合法
virtual void foo(float) override; // 非法, 父类没有此虚函数
};
struct Base {
virtual void foo() final;
};
struct SubClass1 final: Base {
}; // 合法
struct SubClass2 : SubClass1 {
}; // 非法, SubClass1 已 final
struct SubClass3: Base {
void foo(); // 非法, foo 已 final
};
显式禁用默认函数:
class Magic {
public:
Magic() = default; // 显式声明使用编译器生成的构造
Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
Magic(int magic_number);
}
语言运行期的强化
lambda表达式
Lambda 表达式,实际上就是提供了一个类似匿名函数的特性, 而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
[capture](params) opt -> ret {body;};
[] 捕获列表
()参数列表,没有参数列表可以省略不写
opt:异常属性,可以省略不写
返回值类型:可以不写,会根据return的值类型自动推导。
函数体:函数的实现,必须要写。
// 完整的lambda表达式定义
auto f = [](int a) -> int
{
return a+10;
};
// 忽略返回值的lambda表达式定义
auto f = [](int a)
{
return a+10;
};
lambda表达式在C++中会被看做一个仿函数,当 Lambda 表达式的捕获列表为空时,闭包对象还能够转换为函数指针值进行传递:
#include <iostream>
using foo = void(int); // 定义函数类型, using别名语法
void functional(foo f) { // 参数列表中定义的函数类型 foo 被视为退化后的函数指针类型 foo*
f(1); // 通过函数指针调用函数
}
int main() {
auto f = [](int value) {
std::cout << value << std::endl;
};
functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
f(1); // lambda 表达式调用
return 0;
}
std::function
std::function是一种通用、多态的函数封装,它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作,实现类似于函数的容器。
#include<iostream>
#include<functional>
int foo(int para){
return para;
}
int main(){
std::function<int(int)> func = foo; //std::function 包装了一个返回值为 int, 参数为 int 的函数
int i = 10;
std::function<int(int)> func2 = [&](int value) -> int {
return 1+i+value;
};
std::cout<<func(10)<<std::endl;
std::cout<<func2(10)<<std::endl;
}
std::bind 和 std::placeholder
std::bind是用来绑定函数调用的参数的,有时候可能无法一次性获得调用某个函数的全部参数,通过这个函数, 我们可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用。
#include<iostream>
#include<functional>
int foo(int a, int b, int c){
std::cout<<"a = "<<a<<std::endl; // 3
std::cout<<"b = "<<b<<std::endl; // 1
std::cout<<"c = "<<c<<std::endl; // 2
return 0;
}
int main(){
// 将参数1,2绑定到函数 foo 上,
// 但使用 std::placeholders::_1 来对第一个参数进行占位
auto bindFoo = std::bind(foo,std::placeholders::_1,1,2);
// 这时调用 bindFoo 时,只需要提供第一个参数即可
bindFoo(3);
}
使用std::function和std::bind来存储和操作lambda表达式:
#include <iostream>
#include <functional>
using namespace std;
int main(void)
{
// 包装可调用函数
std::function<int(int)> f1 = [](int a) {return a; };
// 绑定可调用函数
std::function<int(int)> f2 = bind([](int a) {return a; }, placeholders::_1);
// 函数调用
cout << f1(100) << endl;
cout << f2(200) << endl;
return 0;
}
//黑魔法
std::function<decltype(f(args...))()> func = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
右值引用
右值引用消除了诸如 std::vector、std::string 之类的额外开销,也才使得函数对象容器 std::function 成为了可能。
- 纯右值:纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
- 将亡值:即将被销毁,却能被移动的值。
要拿到一个将亡值,就要用到右值引用
:T &&,其中 T 是类型。右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。
C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值
移动语义
:采用右值引用和移动构造的方式,避免了重复的复制和析构操作,加强了性能。
#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string
int main() {
std::string str = "Hello world.";
std::vector<std::string> v;
// 将使用 push_back(const T&), 即产生拷贝行为
v.push_back(str);
// 将输出 "str: Hello world."
std::cout << "str: " << str << std::endl;
// 将使用 push_back(const T&&), 不会出现拷贝行为
// 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
// 这步操作后, str 中的值会变为空
v.push_back(std::move(str));
// 将输出 "str: "
std::cout << "str: " << str << std::endl;
return 0;
}
完美转发
一个声明的右值引用其实是一个左值。
这就导致在参数转发的过程中会出现问题:
#include <iostream>
using namespace std;
void reference(int& v) {
std::cout << "左值" << std::endl;
}
void reference(int&& v) {
std::cout << "右值" << std::endl;
}
template <typename T>
void pass(T&& v) {
std::cout << "普通传参:";
reference(v); // 始终调用 reference(int&)
}
int main() {
std::cout << "传递右值:" << std::endl;
pass(1); // 1是右值, 但输出是左值
std::cout << "传递左值:" << std::endl;
int l = 1;
pass(l); // l 是左值, 输出左值
# l是左值,但是成功传递给了pass(T&&),这是基于引用坍缩原则:
return 0;
}
l是左值,但是成功传递给了pass(T&&),这是基于引用坍缩原则:允许对引用进行引用, 既能左引用,又能右引用。
- 模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。
- 无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。
完美转发,就是为了在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)。 为了解决这个问题,应该使用 std::forward 来进行参数的转发。
#include <iostream>
using namespace std;
void reference(int& v) {
std::cout << "左值" << std::endl;
}
void reference(int&& v) {
std::cout << "右值" << std::endl;
}
template <typename T>
void pass(T&& v) {
std::cout << " 普通传参: ";
reference(v);
std::cout << " std::move 传参: ";
reference(std::move(v));
std::cout << " std::forward 传参: ";
reference(std::forward<T>(v));
std::cout << "static_cast<T&&> 传参: ";
reference(static_cast<T&&>(v));
}
int main() {
std::cout << "传递右值:" << std::endl;
pass(1); // 1是右值, 但输出是左值
std::cout << "传递左值:" << std::endl;
int l = 1;
pass(l); // l 是左值, 输出左值
return 0;
}
# 输出结果
传递右值:
普通传参: 左值
std::move 传参: 右值
std::forward 传参: 右值
static_cast<T&&> 传参: 右值
传递左值:
普通传参: 左值
std::move 传参: 右值
std::forward 传参: 左值
static_cast<T&&> 传参: 左值
- 普通传参:将参数作为左值进行转发。
- std::move:总会接受到一个左值,从而转发调用了reference(int&&) 输出右值引用。
- std::forward:没有造成任何多余的拷贝,同时完美转发(传递)了函数的实参给了内部调用的其他函数。
- static_cast<T&&>(v):从现象上看,std::forward(v) 和 static_cast<T&&>(v) 是完全一样的。
完美转发实现原理:
对forward函数进行重载: std::remove_reference:消除类型中的引用,std::is_lvalue_reference 用于检查类型推导是否正确。当 std::forward 接受左值时,_Tp 被推导为左值,所以返回值为左值;而当其接受右值时, _Tp 被推导为 右值引用,则基于坍缩规则,返回值便成为了 && + && 的右值。 可见 std::forward 的原理在于巧妙的利用了模板类型推导中产生的差异。
为什么在使用循环语句的过程中,auto&& 是最安全的方式? 因为当 auto 被推导为不同的左右引用时,与 && 的坍缩组合是完美转发。
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
智能指针与内存管理
RAII:资源获取即初始化:在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII 资源获取即初始化技术。核心思想就是在资源或状态与类对象的生命周期进行绑定,通过C++的语言机制,实现资源与状态的安全管理。
- 在资源管理方面,
智能指针(std::shared_ptr和std::unique_ptr)
是RAII最具代表性的实现,使用了智能指针,可以实现自动的内存管理,再也不用担心忘记delete造成内存泄漏了。 - 在状态管理方面,线程同步中使用
std::unique_lock或std::lock_guard
对互斥量std::mutex进行状态管理也是RAII的典型实现,通过这种方式,我们再也不用担心互斥量之间的代码出现异常而造成线程死锁。
std::shared_ptr
:能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。
- 使用std::make_shared分配创建传入参数中的对象, 并返回这个对象类型的std::shared_ptr指针。
- 通过 get() 方法来获取原始指针
- 通过 reset() 来减少一个引用计数
- 通过use_count()来查看一个对象的引用计数。
#include <iostream>
#include <memory>
void foo(std::shared_ptr<int> i) {
(*i)++;
}
int main(){
// auto pointer = new int(10); // illegal, no direct assignment
// Constructed a std::shared_ptr
auto pointer = std::make_shared<int>(10);
foo(pointer);
std::cout << *pointer << std::endl;// 11
auto pointer2 = pointer; // 引用计数+1
auto pointer3 = pointer; // 引用计数+1
int *p = pointer.get(); // 获取原始指针,不会增加引用计数
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
return 0;
}
std::unique_ptr:
独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全。
- C++14以后,可以使用make_unique来创建一个unique_ptr
- 不可复制,但是可以move
std::weak_ptr:
std::shared_ptr 就会发现依然存在着资源无法释放的问题。当两个指针循环指向对方的时候。
std::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它可以用于检查 std::shared_ptr 是否存在。
并行与并发
锁
多线程:std::thread。
std::mutex
是 C++11 中最基本的 mutex 类,通过实例化 std::mutex 可以创建互斥量,而通过其成员函数 lock() 可以进行上锁,unlock() 可以进行解锁。 但是在实际编写代码的过程中,最好不去直接调用成员函数,因为调用成员函数就需要在每个临界区的出口处调用 unlock(),当然,还包括异常。
C++11 为互斥量提供了一个 RAII 语法的模板类 std::lock_guard。 RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。
int v = 1;
void critical_section(int change_v) {
static std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);
// 执行竞争操作
v = change_v;
// 离开此作用域后 mtx 会被释放
}
std::unique_lock
:更加灵活,std::unique_lock 的对象会以独占所有权(没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权) 的方式管理 mutex 对象上的上锁和解锁的操作。在并发编程中,推荐使用 std::unique_lock。
- std::lock_guard 不能显式的调用 lock 和 unlock, 而 std::unique_lock 可以在声明后的任意位置调用, 可以缩小锁的作用范围,提供更高的并发度。
- 用到了条件变量
std::condition_variable::wait 则必须使用 std::unique_lock 作为参数
。
int v = 1;
void critical_section(int change_v) {
static std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
// 执行竞争操作
v = change_v;
std::cout << v << std::endl;
// 将锁进行释放
lock.unlock();
// 在此期间,任何人都可以抢夺 v 的持有权
// 开始另一组竞争操作,再次加锁
lock.lock();
v += 1;
std::cout << v << std::endl;
lock.unlock();
}
std::future
std::future,用来获取异步任务的结果。
std::packaged_task,用来封装任何可以调用的目标,从而用于实现异步的调用。
#include <iostream>
#include <future>
#include <thread>
int main() {
// 将一个返回值为7的 lambda 表达式封装到 task 中
// std::packaged_task 的模板参数为要封装函数的类型
std::packaged_task<int()> task([]{return 7;});
// 获得 task 的future
std::future<int> result = task.get_future(); // 在一个线程中执行 task
std::thread(std::move(task)).detach();
std::cout << "waiting...";
result.wait(); // 在此设置屏障,阻塞到future的完成
// 输出执行结果
std::cout << "done!" << std:: endl << "future result is "
<< result.get() << std::endl;
return 0;
}
条件变量
std::condition_variable 是为了解决死锁而生,当互斥操作不够用而引入的。 notify_one() 用于唤醒一个线程; notify_all() 则是通知所有线程。
#include <queue>
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream>
#include <condition_variable>
int main(){
std::queue<int> produced_nums;
std::mutex mtx;
std::condition_variable cv;
bool notified = false; //通知信号
auto producer = [&]{
for (int i = 0; ; i++)
{
std::this_thread::sleep_for(std::chrono::milliseconds(900));
std::unique_lock<std::mutex> lock(mtx); //使用unique_lock而不是lock_guard
std::cout << "producing " << i << std::endl;
produced_nums.push(i);
notified = true;
cv.notify_all();
}
};
auto consumer = [&](){
while(true){
std::unique_lock<std::mutex> lock(mtx);
while (!notified) { // 避免虚假唤醒
cv.wait(lock);
}
// 短暂取消锁,使得生产者有机会在消费者消费空前继续生产
lock.unlock();
// 消费者慢于生产者
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
lock.lock();
while(!produced_nums.empty()){
std::cout<<"consuming " << produced_nums.front() << std::endl;
produced_nums.pop();
}
notified = false;
}
};
std::thread p(producer);
std::thread cs[2];
for (int i = 0; i < 2; ++i) {
cs[i] = std::thread(consumer);
}
p.join();
for (int i = 0; i < 2; ++i) {
cs[i].join();
}
return 0;
}
原子操作与内存模型
互斥锁是操作系统级的功能,是一组非常强的同步条件,当最终编译为 CPU 指令时会表现为非常多的指令,这些操作对于原子级操作,没有中间变量的类型,比较苛刻。
C++11 多线程中引入std::atomic模板,可以实例化一个原子类型,将一个原子类型读写操作从一组指令,最小化到单个 CPU 指令。
- 对整数和浮点数,有fetch_add, fetch_sub 等,同时通过重载方便的提供了对应的 +,- 版本。
- 可以通过
std::atomic<T>::is_lock_free
来检查该原子类型是否需支持原子操作
# 编译出错
#include <atomic>
#include <iostream>
struct A {
float x;
int y;
long long z;
};
int main() {
std::atomic<A> a;//这里不行
std::cout << std::boolalpha << a.is_lock_free() << std::endl;
return 0;
}
内存顺序:
C++11 为原子操作定义了六种不同的内存顺序 std::memory_order 的选项,表达了四种多线程间的同步模型:
-
宽松模型:在此模型下,单个线程内的原子操作都是顺序执行的,不允许指令重排,但不同线程间原子操作的顺序是任意的。类型通过 std::memory_order_relaxed 指定。
-
释放/消费模型:开始限制进程间的操作顺序,如果某个线程需要修改某个值,但另一个线程会对该值的某次操作产生依赖,即后者依赖前者。
std::atomic<int *> ptr(nullptr);
int v;
std::thread producer([&](){
int* p = new int(42);
v = 1024;
ptr.store(p, std::memory_order_release);
});
std::thread consumer([&](){
int* p;
while(!(p = ptr.load(std::memory_order_consume))); //会等待
std::cout << "p: " << *p << std::endl;
std::cout << "v: " << v << std::endl;
});
- 释放/获取模型:进一步加紧对不同线程间原子操作的顺序的限制,在释放 std::memory_order_release 和获取 std::memory_order_acquire 之间规定时序。
- 顺序一致性模型:可能会产生性能损耗。
杂项
- C++11 提供了原始字符串字面量的写法,可以在一个字符串前方使用 R 来修饰这个字符串, 同时,将原始字符串使用括号包裹。
- 引入long long int,至少具备 64 位的比特数。
- 数值与字符串之间的相互转化。
- assert是运行时断言,C++11添加了静态时断言:static_assert,编译时就可以检测。
- C++11 将异常的声明简化为两种,可以抛出异常,或者不可以,noexcept修饰过的函数如果抛出异常,编译器会使用std::terminate() 来立即终止程序运行。
- 内存对齐:C++ 11 引入了两个新的关键字 alignof 和 alignas 来支持对内存对齐进行控制。
a. alignof 关键字能够获得一个与平台相关的 std::size_t 类型的值,用于查询该平台的对齐方式。
b. alignas 来重新修饰某个结构的对齐方式。
struct alignas(16) AlignedStruct {
int a;
float b;
};
AlignedStruct 的每个实例都将以16字节对齐。
- 正则表达式:一般采用boost库,C++11 正式将正则表达式的的处理方法纳入标准库的行列,从语言级上提供了标准的支持, 不再依赖第三方。
std::regex_match 用于匹配字符串和正则表达式,有很多不同的重载形式。 最简单的一个形式就是传入 std::string 以及一个 std::regex 进行匹配, 当匹配成功时,会返回 true,否则返回 false。
- 操作 std::string 对象
- 模式 std::regex (本质是 std::basic_regex)进行初始化,
- 通过 std::regex_match 进行匹配,
- 产生 std::smatch (本质是 std::match_results 对象)。
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
// 在 C++ 中 \ 会被作为字符串内的转义符,
// 为使 \. 作为正则表达式传递进去生效,需要对 \ 进行二次转义,从而有 \\.
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname: fnames)
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;
}
比较重要的新特性总结
- auto 类型推导
- 范围 for 迭代
- 初始化列表
- 变参模板
- lambda表达式
- 函数对象包装器
- 右值引用
- 移动语义
- 完美转发
- 智能指针
- RAII
- 多线程相关
参考列表
https://changkun.de/modern-cpp/zh-cn/02-usability/
https://subingwen.cn/cpp/longlong/
https://zhuanlan.zhihu.com/p/389300115