├── .gitignore ├── docs ├── _config.yml ├── 02_auto.md ├── index.md ├── 08_tweaks.md ├── 06_lambda_expressions.md ├── 01_deducing_types.md ├── 05_rvalue_references_move_semantics_and_perfect_forwarding.md ├── 07_the_concurrency_api.md ├── 03_moving_to_modern_cpp.md └── 04_smart_pointers.md ├── .gitattributes ├── src ├── concatenation.cpp ├── print_special_formatting.cpp ├── random_sequence.cpp └── aligned_new.cpp ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/concatenation.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace jc { 4 | 5 | template 6 | constexpr auto concat(F f, Fs... fs) { 7 | if constexpr (sizeof...(fs) > 0) { 8 | return [=](auto... args) { return f(concat(fs...)(args...)); }; 9 | } else { 10 | return f; 11 | } 12 | } 13 | 14 | } // namespace jc 15 | 16 | int main() { 17 | auto twice = [](int i) { return i * 2; }; 18 | auto thrice = [](int i) { return i * 3; }; 19 | auto combined = 20 | jc::concat(twice, thrice, std::plus{}); // twice(thrice(plus)) 21 | static_assert(combined(2, 3) == 30); // 30 = 2 * 3 * (2 + 3) 22 | } -------------------------------------------------------------------------------- /src/print_special_formatting.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace jc { 5 | 6 | class format_guard { 7 | public: 8 | ~format_guard() { std::cout.flags(f); } 9 | 10 | private: 11 | decltype(std::cout.flags()) f{std::cout.flags()}; 12 | }; 13 | 14 | template 15 | struct scientific_type { 16 | T value; 17 | explicit scientific_type(T val) : value{val} {} 18 | }; 19 | 20 | template 21 | std::ostream &operator<<(std::ostream &os, const scientific_type &w) { 22 | format_guard _; 23 | return os << std::scientific << std::uppercase << std::showpos << w.value; 24 | } 25 | 26 | } // namespace jc 27 | 28 | int main() { 29 | { 30 | jc::format_guard _; 31 | std::cout << std::hex << std::scientific << std::showbase << std::uppercase; 32 | 33 | std::cout << "Numbers with special formatting:\n"; 34 | std::cout << 0x123abc << '\n'; 35 | std::cout << 0.123456789 << '\n'; 36 | } 37 | std::cout << "Same numbers, but normal formatting again:\n"; 38 | std::cout << 0x123abc << '\n'; 39 | std::cout << 0.123456789 << '\n'; 40 | std::cout << "Mixed formatting: " << 123.0 << " " 41 | << jc::scientific_type{123.0} << " " << 123.456 << '\n'; 42 | } 43 | -------------------------------------------------------------------------------- /src/random_sequence.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace jc { 11 | 12 | template 13 | std::vector random_sequence() { 14 | std::random_device rd; 15 | std::mt19937 mt{rd()}; 16 | std::uniform_int_distribution d(Min, Max); 17 | auto rand_num([=]() mutable { return d(mt); }); 18 | std::vector res(N); 19 | std::generate(std::execution::par, res.begin(), res.end(), rand_num); 20 | return res; 21 | } 22 | 23 | } // namespace jc 24 | 25 | int main() { 26 | constexpr std::size_t N = 100000; 27 | constexpr std::size_t Min = 0; 28 | constexpr std::size_t Max = 10; 29 | auto v = jc::random_sequence(); 30 | std::sort(std::execution::par, v.begin(), v.end()); 31 | std::size_t n = 0; 32 | std::vector cnt; 33 | for (auto i = Min; i <= Max; ++i) { 34 | cnt.emplace_back(std::count_if(std::execution::par, v.begin(), v.end(), 35 | [i](int x) { return x == i; })); 36 | } 37 | assert(std::accumulate(cnt.begin(), cnt.end(), 0) == N); 38 | std::copy(cnt.begin(), cnt.end(), 39 | std::ostream_iterator{std::cout, "\n"}); 40 | } 41 | -------------------------------------------------------------------------------- /src/aligned_new.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace jc { 7 | 8 | template 9 | T* aligned_new() { 10 | if constexpr (_Element_Count == 0) { 11 | return nullptr; 12 | } 13 | T* res = reinterpret_cast(::operator new ( 14 | sizeof(T) * _Element_Count, 15 | std::align_val_t{std::hardware_constructive_interference_size})); 16 | if constexpr (_Default_Ctor) { 17 | new (res) T[_Element_Count]; 18 | } 19 | return res; 20 | } 21 | 22 | template 23 | void aligned_release(T* p) { 24 | if constexpr (_Element_Count == 0) { 25 | return; 26 | } 27 | if (!p) { 28 | return; 29 | } 30 | if constexpr (_Dtor) { 31 | for (std::size_t i = 0; i < _Element_Count; ++i) { 32 | p[i].~T(); 33 | } 34 | } 35 | ::operator delete ( 36 | p, sizeof(T) * _Element_Count, 37 | std::align_val_t{std::hardware_constructive_interference_size}); 38 | } 39 | 40 | } // namespace jc 41 | 42 | int main() { 43 | constexpr std::size_t n = 3; 44 | std::string* p = jc::aligned_new(); 45 | for (std::size_t i = 0; i < n; ++i) { 46 | p[i] = std::to_string(i); 47 | } 48 | for (std::size_t i = 0; i < n; ++i) { 49 | assert(p[i] == std::to_string(i)); 50 | } 51 | jc::aligned_release(p); 52 | } 53 | -------------------------------------------------------------------------------- /docs/02_auto.md: -------------------------------------------------------------------------------- 1 | ## 05 用 [auto](https://en.cppreference.com/w/cpp/language/auto) 替代显式类型声明 2 | 3 | * auto 声明的变量必须初始化,因此使用 auto 可以避免忘记初始化的问题 4 | 5 | ```cpp 6 | int a; // 潜在的未初始化风险 7 | auto b; // 错误:必须初始化 8 | ``` 9 | 10 | * 对于名称非常长的类型,如迭代器相关的类型,用 auto 声明可以大大简化工作 11 | 12 | ```cpp 13 | template 14 | void f(It first, It last) { 15 | while (first != last) { 16 | auto val = *first; 17 | // auto 相当于 typename std::iterator_traits::value_type 18 | } 19 | } 20 | ``` 21 | 22 | * lambda 生成的闭包类型是编译期内部的匿名类型,无法得知,使用 auto 推断就没有这个问题 23 | 24 | ```cpp 25 | auto f = [](auto& x, auto& y) { return x < y; }; 26 | ``` 27 | 28 | * 如果不使用 auto,可以改用 [std::function](https://en.cppreference.com/w/cpp/utility/functional/function) 29 | 30 | ```cpp 31 | // std::function 的模板参数中不能使用 auto 32 | std::function f = [](auto& x, auto& y) { return x < y; }; 33 | ``` 34 | 35 | * 除了明显的语法冗长和不能利用 auto 参数的缺点,[std::function](https://en.cppreference.com/w/cpp/utility/functional/function) 与 auto 的最大区别在于,auto 和闭包类型一致,内存量和闭包相同,而 [std::function](https://en.cppreference.com/w/cpp/utility/functional/function) 是类模板,它的实例有一个固定大小,这个大小不一定能容纳闭包,于是会分配堆上的内存以存储闭包,导致比 auto 变量占用更多内存。此外,编译器一般会限制内联,[std::function](https://en.cppreference.com/w/cpp/utility/functional/function) 调用闭包会比 auto 慢 36 | * auto 可以避免简写类型存在的潜在问题。比如如下代码有潜在隐患 37 | 38 | ```cpp 39 | std::vector v; 40 | unsigned sz = v.size(); // v.size() 类型实际为 std::vector::size_type 41 | // 在 32 位机器上 std::vector::size_type 与 unsigned 尺寸相同 42 | // 但在 64 位机器上,std::vector::size_type 是 64 位,而 unsigned 是 32 位 43 | 44 | std::unordered_map m; 45 | for (const std::pair& p : m) { 46 | // m 元素类型为 std::pair 47 | // 循环中使用的元素类型不一致,需要转换,期间将构造大量临时对象 48 | } 49 | ``` 50 | 51 | * 如果显式类型声明能让代码更清晰或有其他好处就不用强行 auto,此外 IDE 的类型提示也能缓解不能直接看出对象类型的问题 52 | 53 | ## 06 [auto](https://en.cppreference.com/w/cpp/language/auto) 推断出非预期类型时,先强制转换出预期类型 54 | 55 | * auto 推断得到的类型可能与直觉认知不同 56 | 57 | ```cpp 58 | std::vector v{true, false}; 59 | for (auto& x : v) { // 错误:未定义行为 60 | } 61 | ``` 62 | 63 | * [std::vector\](https://en.cppreference.com/w/cpp/container/vector_bool 不是真正的 STL 容器,也不包含 bool 类型元素。它是 [std::vector](https://en.cppreference.com/w/cpp/container/vector) 对于 bool 类型的特化,为了节省空间,每个元素用一个 bit(而非一个 bool,一个 bool 占一字节)表示,于是 [operator[]](https://en.cppreference.com/w/cpp/container/vector/operator_at) 返回的应该是单个 bit 的引用,但 C++ 中不存在指向单个 bit 的指针,因此也不能获取单个 bit 的引用 64 | 65 | ```cpp 66 | std::vector v{true, false}; 67 | bool& p = v[0]; // 错误 68 | ``` 69 | 70 | * 因此需要一个行为类似单个 bit 并可以被引用的对象,也就是 [std::vector\::reference](https://en.cppreference.com/w/cpp/container/vector_bool/reference),它可以隐式转换为 bool 71 | 72 | ```cpp 73 | bool x = v[0]; 74 | ``` 75 | 76 | * 而 auto 推断不会进行隐式转换 77 | 78 | ```cpp 79 | auto x = v[0]; // x 类型为 std::vector::reference 80 | // x 不一定指向 std::vector的第 0 个 bit, 81 | // 这取决于 std::vector::reference 的实现, 82 | // 一种实现是含有一个指向一个 machine word的指针, 83 | // word 持有被引用的 bit 和这个 bit 相对 word 的 offset, 84 | // 于是 x 持有一个由 opeartor[] 返回的 85 | // 临时的 machine word 的指针和 bit 的 offset, 86 | // 这条语句结束后临时对象被析构, 87 | // 于是 x 含有一个空悬指针,导致后续的未定义行为 88 | ``` 89 | 90 | * [std::vector\::reference](https://en.cppreference.com/w/cpp/container/vector_bool/reference) 是一个代理类(proxy class,模拟或扩展其他类型的类)的例子,比如 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 和 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 是很明显的代理类。还有一些为了提高数值计算效率而使用表达式模板技术开发的类,比如给定一个 Matrix 类和它的对象 91 | 92 | ```cpp 93 | Matrix sum = m1 + m2 + m3 + m4; 94 | ``` 95 | 96 | * Matrix 对象的 operator+ 返回的是结果的代理而非结果本身,这样可以使得表达式的计算更为高效 97 | 98 | ```cpp 99 | auto x = m1 + m2; // x 可能是 Sum 而不是 Matrix 对象 100 | ``` 101 | 102 | * auto 推断出代理类的问题实际很容易解决,事先做一次到预期类型的强制转换即可 103 | 104 | ```cpp 105 | auto x = static_cast(v[0]); 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | * Modern C++ 一般指 C++11 及其之后的标准,已在工业界被广泛应用。 C++ 初学者适合从 *[C++ Primer](https://learning.oreilly.com/library/view/c-primer-fifth/9780133053043/)* 开始学习 Modern C++ 的基本语法,通过 *[Effective C++](https://learning.oreilly.com/library/view/effective-c-55/0321334876/)* 掌握 C++98 的最佳实践,通过 *[Effective STL](https://learning.oreilly.com/library/view/effective-stl/9780321545183/)* 掌握 STL 的正确使用,通过 *[Effective Modern C++](https://learning.oreilly.com/library/view/effective-modern-c/9781491908419/)* 掌握 C++11/14 的最佳实践,至此即可避开语言缺陷,得心应手地发挥 C++ 的长处。此为个人笔记,还将补充 C++17 相关特性。 2 | 3 | ## [1. 类型推断](01_deducing_types.html) 4 | 5 | * 01 模板类型推断机制 6 | * 02 [auto](https://en.cppreference.com/w/cpp/language/auto) 类型推断机制 7 | * 03 [decltype](https://en.cppreference.com/w/cpp/language/decltype) 8 | * 04 查看推断类型的方法 9 | 10 | ## [2. auto](02_auto.html) 11 | 12 | * 05 用 [auto](https://en.cppreference.com/w/cpp/language/auto) 替代显式类型声明 13 | * 06 [auto](https://en.cppreference.com/w/cpp/language/auto) 推断出非预期类型时,先强制转换出预期类型 14 | 15 | ## [3. 转向现代 C++](03_moving_to_modern_cpp.html) 16 | 17 | * 07 创建对象时注意区分 () 和 {} 18 | * 08 用 [nullptr](https://en.cppreference.com/w/cpp/language/nullptr) 替代 0 和 [NULL](https://en.cppreference.com/w/cpp/types/NULL) 19 | * 09 用 [using 别名声明](https://en.cppreference.com/w/cpp/language/type_alias)替代 [typedef](https://en.cppreference.com/w/cpp/language/typedef) 20 | * 10 用 [enum class](https://en.cppreference.com/w/cpp/language/enum#Scoped_enumerations) 替代 [enum](https://en.cppreference.com/w/cpp/language/enum#Unscoped_enumeration) 21 | * 11 用 =delete 替代 private 作用域来禁用函数 22 | * 12 用 [override](https://en.cppreference.com/w/cpp/language/override) 标记被重写的虚函数 23 | * 13 用 [std::cbegin](https://en.cppreference.com/w/cpp/iterator/begin) 和 [std::cend](https://en.cppreference.com/w/cpp/iterator/end) 获取 const_iterator 24 | * 14 用 [noexcept](https://en.cppreference.com/w/cpp/language/noexcept_spec) 标记不抛异常的函数 25 | * 15 用 [constexpr](https://en.cppreference.com/w/cpp/language/constexpr) 表示编译期常量 26 | * 16 用 [std::mutex](https://en.cppreference.com/w/cpp/thread/mutex) 或 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 保证 const 成员函数线程安全 27 | * 17 特殊成员函数的隐式合成与抑制机制 28 | 29 | ## [4. 智能指针](04_smart_pointers.html) 30 | 31 | * 18 用 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 管理所有权唯一的资源 32 | * 19 用 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 管理所有权可共享的资源 33 | * 20 用 [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 观测 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的内部状态 34 | * 21 用 [std::make_unique](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique)([std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared)) 创建 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr)([std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr)) 35 | * 22 用 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 实现 [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) 必须在源文件中提供析构函数定义 36 | 37 | ## [5. 右值引用、移动语义和完美转发](05_rvalue_references_move_semantics_and_perfect_forwarding.html) 38 | 39 | * 23 [std::move](https://en.cppreference.com/w/cpp/utility/move) 和 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 只是一种强制类型转换 40 | * 24 转发引用与右值引用的区别 41 | * 25 对右值引用使用 [std::move](https://en.cppreference.com/w/cpp/utility/move),对转发引用使用 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 42 | * 26 避免重载使用转发引用的函数 43 | * 27 重载转发引用的替代方案 44 | * 28 引用折叠 45 | * 29 移动不比拷贝快的情况 46 | * 30 无法完美转发的类型 47 | 48 | ## [6. lambda 表达式](06_lambda_expressions.html) 49 | 50 | * 31 捕获的潜在问题 51 | * 32 用初始化捕获将对象移入闭包 52 | * 33 用 [decltype](https://en.cppreference.com/w/cpp/language/decltype) 获取 auto&& 参数类型以 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 53 | * 34 用 lambda 替代 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 54 | 55 | ## [7. 并发 API](07_the_concurrency_api.html) 56 | 57 | * 35 用 [std::async](https://en.cppreference.com/w/cpp/thread/async) 替代 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 58 | * 36 用 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch) 指定异步求值 59 | * 37 RAII 线程管理 60 | * 38 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的析构行为 61 | * 39 用 [std::promise](https://en.cppreference.com/w/cpp/thread/promise) 和 [std::future](https://en.cppreference.com/w/cpp/thread/future) 之间的通信实现一次性通知 62 | * 40 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 提供原子操作,volatile 禁止优化内存 63 | 64 | ## [8. 其他轻微调整](08_tweaks.html) 65 | 66 | * 41 对于可拷贝的形参,如果移动成本低且一定会被拷贝则考虑传值 67 | * 42 用 emplace 操作替代 insert 操作 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | * Modern C++ 一般指 C++11 及其之后的标准,已在工业界被广泛应用。 C++ 初学者适合从 *[C++ Primer](https://learning.oreilly.com/library/view/c-primer-fifth/9780133053043/)* 开始学习 Modern C++ 的基本语法,通过 *[Effective C++](https://learning.oreilly.com/library/view/effective-c-55/0321334876/)* 掌握 C++98 的最佳实践,通过 *[Effective STL](https://learning.oreilly.com/library/view/effective-stl/9780321545183/)* 掌握 STL 的正确使用,通过 *[Effective Modern C++](https://learning.oreilly.com/library/view/effective-modern-c/9781491908419/)* 掌握 C++11/14 的最佳实践,至此即可避开语言缺陷,得心应手地发挥 C++ 的长处。此为个人笔记,还将补充 C++17 相关特性。 2 | 3 | ## [1. 类型推断](https://github.com/downdemo/Effective-Modern-Cpp/tree/master/docs/01_deducing_types.md) 4 | 5 | * 01 模板类型推断机制 6 | * 02 [auto](https://en.cppreference.com/w/cpp/language/auto) 类型推断机制 7 | * 03 [decltype](https://en.cppreference.com/w/cpp/language/decltype) 8 | * 04 查看推断类型的方法 9 | 10 | ## [2. auto](https://github.com/downdemo/Effective-Modern-Cpp/tree/master/docs/02_auto.md) 11 | 12 | * 05 用 [auto](https://en.cppreference.com/w/cpp/language/auto) 替代显式类型声明 13 | * 06 [auto](https://en.cppreference.com/w/cpp/language/auto) 推断出非预期类型时,先强制转换出预期类型 14 | 15 | ## [3. 转向现代 C++](https://github.com/downdemo/Effective-Modern-Cpp/tree/master/docs/03_moving_to_modern_cpp.md) 16 | 17 | * 07 创建对象时注意区分 () 和 {} 18 | * 08 用 [nullptr](https://en.cppreference.com/w/cpp/language/nullptr) 替代 0 和 [NULL](https://en.cppreference.com/w/cpp/types/NULL) 19 | * 09 用 [using 别名声明](https://en.cppreference.com/w/cpp/language/type_alias)替代 [typedef](https://en.cppreference.com/w/cpp/language/typedef) 20 | * 10 用 [enum class](https://en.cppreference.com/w/cpp/language/enum#Scoped_enumerations) 替代 [enum](https://en.cppreference.com/w/cpp/language/enum#Unscoped_enumeration) 21 | * 11 用 =delete 替代 private 作用域来禁用函数 22 | * 12 用 [override](https://en.cppreference.com/w/cpp/language/override) 标记被重写的虚函数 23 | * 13 用 [std::cbegin](https://en.cppreference.com/w/cpp/iterator/begin) 和 [std::cend](https://en.cppreference.com/w/cpp/iterator/end) 获取 const_iterator 24 | * 14 用 [noexcept](https://en.cppreference.com/w/cpp/language/noexcept_spec) 标记不抛异常的函数 25 | * 15 用 [constexpr](https://en.cppreference.com/w/cpp/language/constexpr) 表示编译期常量 26 | * 16 用 [std::mutex](https://en.cppreference.com/w/cpp/thread/mutex) 或 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 保证 const 成员函数线程安全 27 | * 17 特殊成员函数的隐式合成与抑制机制 28 | 29 | ## [4. 智能指针](https://github.com/downdemo/Effective-Modern-Cpp/tree/master/docs/04_smart_pointers.md) 30 | 31 | * 18 用 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 管理所有权唯一的资源 32 | * 19 用 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 管理所有权可共享的资源 33 | * 20 用 [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 观测 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的内部状态 34 | * 21 用 [std::make_unique](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique)([std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared)) 创建 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr)([std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr)) 35 | * 22 用 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 实现 [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) 必须在源文件中提供析构函数定义 36 | 37 | ## [5. 右值引用、移动语义和完美转发](https://github.com/downdemo/Effective-Modern-Cpp/tree/master/docs/05_rvalue_references_move_semantics_and_perfect_forwarding.md) 38 | 39 | * 23 [std::move](https://en.cppreference.com/w/cpp/utility/move) 和 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 只是一种强制类型转换 40 | * 24 转发引用与右值引用的区别 41 | * 25 对右值引用使用 [std::move](https://en.cppreference.com/w/cpp/utility/move),对转发引用使用 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 42 | * 26 避免重载使用转发引用的函数 43 | * 27 重载转发引用的替代方案 44 | * 28 引用折叠 45 | * 29 移动不比拷贝快的情况 46 | * 30 无法完美转发的类型 47 | 48 | ## [6. lambda 表达式](https://github.com/downdemo/Effective-Modern-Cpp/tree/master/docs/06_lambda_expressions.md) 49 | 50 | * 31 捕获的潜在问题 51 | * 32 用初始化捕获将对象移入闭包 52 | * 33 用 [decltype](https://en.cppreference.com/w/cpp/language/decltype) 获取 auto&& 参数类型以 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 53 | * 34 用 lambda 替代 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 54 | 55 | ## [7. 并发 API](https://github.com/downdemo/Effective-Modern-Cpp/tree/master/docs/07_the_concurrency_api.md) 56 | 57 | * 35 用 [std::async](https://en.cppreference.com/w/cpp/thread/async) 替代 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 58 | * 36 用 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch) 指定异步求值 59 | * 37 RAII 线程管理 60 | * 38 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的析构行为 61 | * 39 用 [std::promise](https://en.cppreference.com/w/cpp/thread/promise) 和 [std::future](https://en.cppreference.com/w/cpp/thread/future) 之间的通信实现一次性通知 62 | * 40 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 提供原子操作,volatile 禁止优化内存 63 | 64 | ## [8. 其他轻微调整](https://github.com/downdemo/Effective-Modern-Cpp/tree/master/docs/08_tweaks.md) 65 | 66 | * 41 对于可拷贝的形参,如果移动成本低且一定会被拷贝则考虑传值 67 | * 42 用 emplace 操作替代 insert 操作 68 | -------------------------------------------------------------------------------- /docs/08_tweaks.md: -------------------------------------------------------------------------------- 1 | ## 41 对于可拷贝的形参,如果移动成本低且一定会被拷贝则考虑传值 2 | 3 | * 一些函数的形参本身就是用于拷贝的,考虑性能,对左值实参应该执行拷贝,对右值实参应该执行移动 4 | 5 | ```cpp 6 | class A { 7 | public: 8 | void f(const std::string& s) { v_.push_back(s); } 9 | void f(std::string&& s) { v_.push_back(std::move(s)); } 10 | 11 | private: 12 | std::vector v_; 13 | }; 14 | ``` 15 | 16 | * 为同一个功能写两个函数太过麻烦,因此改用为参数为转发引用的模板 17 | 18 | ```cpp 19 | class A { 20 | public: 21 | template 22 | void f(T&& s) { 23 | v_.push_back(std::forward(s)); 24 | } 25 | 26 | private: 27 | std::vector v_; 28 | }; 29 | ``` 30 | 31 | * 但模板会带来复杂性,一是模板一般要在头文件中实现,它可能在对象代码中产生多个函数,二是如果传入了不正确的实参类型,将出现十分冗长的错误信息,难以调试。所以最好的方法是,针对左值拷贝,针对右值移动,并且在源码和目标代码中只需要处理一个函数,还能避开转发引用,而这种方法就是按值传递 32 | 33 | ```cpp 34 | class A { 35 | public: 36 | void f(std::string s) { v_.push_back(std::move(s)); } 37 | 38 | private: 39 | std::vector v_; 40 | }; 41 | ``` 42 | 43 | * C++98 中,按值传递一定是拷贝构造,但在 C++11 中,只在传入左值时拷贝,如果传入右值则移动 44 | 45 | ```cpp 46 | A a; 47 | std::string s{"hi"}; 48 | a.f(s); // 以传左值的方式调用 49 | a.f("hi"); // 以传右值的方式调用 50 | ``` 51 | 52 | * 对比不同方法的开销,重载和模板的成本是,对左值一次拷贝,对右值一次移动(此外模板可以用转发实参直接构造,可能一次拷贝或移动都不要)。传值一定会对形参有一次拷贝(左值)或移动构造(右值),之后再移动进容器,因此对左值一次拷贝一次移动,对右值两次移动。对比之下,传值只多出了一次移动,虽然成本高一些,但极大避免了麻烦 53 | * 可拷贝的形参才考虑传值,因为 move-only 类型只需要一个处理右值类型的函数 54 | 55 | ```cpp 56 | class A { 57 | public: 58 | void f(std::unique_ptr&& p) { p_ = std::move(p); } 59 | 60 | private: 61 | std::unique_ptr p_; 62 | }; 63 | ``` 64 | 65 | * 如果使用传值,则同样的调用需要先移动构造形参,多出了一次移动 66 | 67 | ```cpp 68 | class A { 69 | public: 70 | void f(std::unique_ptr p) { p_ = std::move(p); } 71 | 72 | private: 73 | std::unique_ptr p_; 74 | }; 75 | ``` 76 | 77 | * 只有当移动成本低时,多出的一次移动才值得考虑,因此应该只对一定会被拷贝的形参传值 78 | 79 | ```cpp 80 | class A { 81 | public: 82 | void f(std::string s) { 83 | if (s.size() <= 15) { 84 | v_.push_back(std::move(s)); // 不满足条件则不添加,但比传引用多了一次析构 85 | } 86 | } 87 | 88 | private: 89 | std::vector v_; 90 | }; 91 | ``` 92 | 93 | * 之前的函数通过构造拷贝,如果通过赋值来拷贝,按值传递可能存在其他额外开销,这取决于很多方面,比如传入类型是否使用动态分配内存、使用动态分配内存时赋值运算符的实现、赋值目标和源对象的内存大小、是否使用 SSO 94 | 95 | ```cpp 96 | #include 97 | #include 98 | 99 | class A { 100 | public: 101 | explicit A(std::string s) : s_(std::move(s)) {} 102 | void f(std::string s) { s_ = std::move(s); } 103 | 104 | private: 105 | std::string s_; 106 | }; 107 | 108 | int main() { 109 | std::string s{"hello"}; 110 | A a(s); 111 | 112 | std::string x{"hi"}; 113 | /* 114 | * 额外的分配和回收成本,可能远高于 std::string 的移动成本 115 | * 传引用则不会有此成本,因为现在 a.s 的长度比之前小 116 | */ 117 | a.f(x); 118 | 119 | std::string y{"hello world"}; 120 | a.f(y); // a.s 比之前长,传值和传引用都有额外的分配和回收成本,开销区别不大 121 | } 122 | ``` 123 | 124 | ## 42 用 emplace 操作替代 insert 操作 125 | 126 | * [std::vector::push_back](https://en.cppreference.com/w/cpp/container/vector/push_back) 对左值和右值的重载为 127 | 128 | ```cpp 129 | template > 130 | class vector { 131 | public: 132 | void push_back(const T& x); 133 | void push_back(T&& x); 134 | }; 135 | ``` 136 | 137 | * 直接传入字面值时,会创建一个临时对象。使用 [std::vector::emplace_back](https://en.cppreference.com/w/cpp/container/vector/emplace_back) 则可以直接用传入的实参调用元素的构造函数 138 | * 所有 insert 操作都有对应的 emplace 操作 139 | 140 | ``` 141 | push_back => emplace_back // std::list、std::deque、std::vector 142 | push_front => emplace_front // std::list、std::deque、std::forward_list 143 | insert_after => emplace_after // std::forward_list 144 | insert => emplace // 除 std::forward_list、std::array 外的所有容器 145 | insert => try_emplace // C++17,std:map、std::unordered_map 146 | emplace_hint // 所有关联容器 147 | ``` 148 | 149 | * 即使 insert 函数不需要创建临时对象,也可以用 emplace 函数替代,此时两者本质上做的是同样的事。因此 emplace 函数就能做到 insert 函数能做的所有事,有时甚至做得更好 150 | 151 | ```cpp 152 | std::vector v; 153 | std::string s{"hi"}; 154 | // 下面两个调用的效果相同 155 | v.push_back(s); 156 | v.emplace_back(s); 157 | ``` 158 | 159 | * emplace 不一定比 insert 快。之前 emplace 添加元素到容器末尾,该位置不存在对象,因此新值会使用构造方式。但如果添加值到已有对象占据的位置,则会采用赋值的方式,于是必须创建一个临时对象作为移动的源对象,此时 emplace 并不会比 insert 高效 160 | 161 | ```cpp 162 | std::vector v{"hhh", "iii"}; 163 | v.emplace(v.begin(), "hi"); // 创建一个临时对象后移动赋值 164 | ``` 165 | 166 | * 对于 [std::set](https://en.cppreference.com/w/cpp/container/set) 和 [std::map](https://en.cppreference.com/w/cpp/container/map),为了检查值是否已存在,emplace 会为新值创建一个 node,以便能与容器中已存在的 node 进行比较。如果值不存在,则将 node 链接到容器中。如果值已存在,emplace 就会中止,node 会被析构,这意味着构造和析构的成本被浪费了,此时 emplace 不如 insert 高效 167 | 168 | ```cpp 169 | #include 170 | #include 171 | #include 172 | #include 173 | #include 174 | #include 175 | 176 | class A { 177 | public: 178 | A(int a, int b, int c) : a_(a), b_(b), c_(c) {} 179 | bool operator<(const A &rhs) const { 180 | return std::tie(a_, b_, c_) < std::tie(rhs.a_, rhs.b_, rhs.c_); 181 | } 182 | 183 | private: 184 | int a_; 185 | int b_; 186 | int c_; 187 | }; 188 | 189 | constexpr int n = 100; 190 | 191 | void set_emplace() { 192 | std::set set; 193 | for (int i = 0; i < n; ++i) { 194 | for (int j = 0; j < n; ++j) { 195 | for (int k = 0; k < n; ++k) { 196 | set.emplace(i, j, k); 197 | } 198 | } 199 | } 200 | } 201 | 202 | void set_insert() { 203 | std::set set; 204 | for (int i = 0; i < n; ++i) { 205 | for (int j = 0; j < n; ++j) { 206 | for (int k = 0; k < n; ++k) { 207 | set.insert(A(i, j, k)); 208 | } 209 | } 210 | } 211 | } 212 | 213 | void test(std::function f) { 214 | auto start = std::chrono::system_clock::now(); 215 | f(); 216 | auto stop = std::chrono::system_clock::now(); 217 | std::chrono::duration time = stop - start; 218 | std::cout << std::fixed << std::setprecision(2) << time.count() << " ms\n"; 219 | } 220 | 221 | int main() { 222 | test(set_insert); 223 | test(set_emplace); 224 | test(set_insert); 225 | test(set_emplace); 226 | test(set_insert); 227 | test(set_emplace); 228 | } 229 | ``` 230 | 231 | * 创建临时对象并非总是坏事。假设给一个存储 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的容器添加一个自定义删除器的 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 对象 232 | 233 | ```cpp 234 | std::list> v; 235 | 236 | void f(A*); 237 | v.push_back(std::shared_ptr(new A, f)); 238 | // 或者如下,意义相同 239 | v.push_back({new A, f}); 240 | ``` 241 | 242 | * 如果使用 [emplace_back](https://en.cppreference.com/w/cpp/container/list/emplace_back) 会禁止创建临时对象。但这里临时对象带来的收益远超其成本。考虑如下可能发生的事件序列: 243 | * 创建一个 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 临时对象 244 | * [push_back](https://en.cppreference.com/w/cpp/container/list/push_back) 以引用方式接受临时对象,分配内存时抛出了内存不足的异常 245 | * 异常传到 [push_back](https://en.cppreference.com/w/cpp/container/list/push_back) 之外,临时对象被析构,于是删除器被调用,A 被释放 246 | * 即使发生异常,也没有资源泄露。[push_back](https://en.cppreference.com/w/cpp/container/list/push_back) 的调用中,由 new 构造的 A 会在临时对象被析构时释放。如果使用的是 [emplace_back](https://en.cppreference.com/w/cpp/container/list/emplace_back),new 创建的原始指针被完美转发到 [emplace_back](https://en.cppreference.com/w/cpp/container/list/emplace_back) 分配内存的执行点。如果内存分配失败,抛出内存不足的异常,异常传到 [emplace_back](https://en.cppreference.com/w/cpp/container/list/emplace_back) 外,唯一可以获取堆上对象的原始指针丢失,于是就产生了资源泄漏 247 | * 实际上不应该把 new A 这样的表达式直接传递给函数,应该单独用一条语句来创建 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 再将对象作为右值传递给函数 248 | 249 | ```cpp 250 | std::shared_ptr p(new A, f); 251 | v.push_back(std::move(p)); 252 | // emplace_back 的写法相同,此时两者开销区别不大 253 | v.emplace_back(std::move(p)); 254 | ``` 255 | 256 | * emplace 函数在调用 [explicit](https://en.cppreference.com/w/cpp/language/explicit) 构造函数时存在一个隐患 257 | 258 | ```cpp 259 | std::vector v; 260 | v.push_back(nullptr); // 编译出错 261 | v.emplace_back(nullptr); // 编译通过,运行时抛出异常,难以发现此问题 262 | ``` 263 | 264 | * 原因在于 [std::regex](https://en.cppreference.com/w/cpp/regex/basic_regex) 接受 `const char*` 参数的构造函数被声明为 [explicit](https://en.cppreference.com/w/cpp/language/explicit),用 [nullptr](https://en.cppreference.com/w/cpp/language/nullptr) 赋值要求 [nullptr](https://en.cppreference.com/w/cpp/language/nullptr) 到 [std::regex](https://en.cppreference.com/w/cpp/regex/basic_regex) 的隐式转换,因此不能通过编译 265 | 266 | ```cpp 267 | std::regex r = nullptr; // 错误 268 | ``` 269 | 270 | * 而 [emplace_back](https://en.cppreference.com/w/cpp/container/vector/emplace_back) 直接传递实参给构造函数,这个行为在编译器看来等同于 271 | 272 | ```cpp 273 | std::regex r{nullptr}; // 能编译但会引发异常,未定义行为 274 | ``` 275 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docs/06_lambda_expressions.md: -------------------------------------------------------------------------------- 1 | ## 31 捕获的潜在问题 2 | 3 | * 值捕获只保存捕获时的对象状态 4 | 5 | ```cpp 6 | #include 7 | 8 | int main() { 9 | int x = 1; 10 | auto f = [x] { return x; }; 11 | auto g = [x]() mutable { return ++x; }; 12 | x = 42; // 不改变 lambda 中已经捕获的 x 13 | assert(f() == 1); 14 | assert(g() == 2); 15 | } 16 | ``` 17 | 18 | * 引用捕获会保持与被捕获对象状态一致 19 | 20 | ```cpp 21 | #include 22 | 23 | int main() { 24 | int x = 1; 25 | auto f = [&x] { return x; }; 26 | x = 42; 27 | assert(f() == 42); 28 | } 29 | ``` 30 | 31 | * 引用捕获时,在捕获的局部变量析构后调用 lambda,将出现空悬引用 32 | 33 | ```cpp 34 | #include 35 | #include 36 | 37 | std::function f() { 38 | int x = 0; 39 | return [&]() { std::cout << x; }; 40 | } 41 | 42 | int main() { 43 | f()(); // x 已被析构,值未定义 44 | } 45 | ``` 46 | 47 | * 值捕获的问题比较隐蔽 48 | 49 | ```cpp 50 | #include 51 | 52 | struct A { 53 | auto f() { 54 | // 如果去掉 =,或改为捕获 i 就会出错 55 | return [=] { std::cout << i; }; 56 | }; 57 | int i = 1; 58 | }; 59 | 60 | int main() { 61 | A a; 62 | a.f()(); // 1 63 | } 64 | ``` 65 | 66 | * 数据成员位于 lambda 作用域外,不能被捕获,= 捕获的是 this 指针,实际效果相当于引用捕获 67 | 68 | ```cpp 69 | #include 70 | 71 | struct A { 72 | auto f() { 73 | return [this] { std::cout << i; }; // i 被视为 this->i 74 | }; 75 | int i = 1; 76 | }; 77 | 78 | int main() { 79 | A a; 80 | auto x = a.f(); // 内部的 i 与 a.i 绑定 81 | a.i = 2; 82 | x(); // 2 83 | } 84 | ``` 85 | 86 | * 因此值捕获也可以出现空悬引用 87 | 88 | ```cpp 89 | #include 90 | #include 91 | 92 | struct A { 93 | auto f() { 94 | return [this] { std::cout << i; }; 95 | }; 96 | int i = 1; 97 | }; 98 | 99 | auto g() { 100 | auto p = std::make_unique(); 101 | return p->f(); 102 | } // p 被析构 103 | 104 | int main() { 105 | g()(); // A 对象 p 已被析构,lambda 捕获的 this 失效,i 值未定义 106 | } 107 | ``` 108 | 109 | * this 指针会被析构,可以值捕获 `*this` 对象,这才是真正的值捕获 110 | 111 | ```cpp 112 | #include 113 | #include 114 | 115 | struct A { 116 | auto f() { 117 | return [*this] { std::cout << i; }; 118 | }; 119 | int i = 1; 120 | }; 121 | 122 | // 现在 lambda 捕获的是对象的一个拷贝,不会被原对象的析构影响 123 | auto g() { 124 | auto p = std::make_unique(); 125 | return p->f(); 126 | } 127 | 128 | int main() { 129 | A a; 130 | auto x = a.f(); // 只保存此刻的 a.i 131 | a.i = 2; 132 | x(); // 1 133 | g()(); // A 对象 p 已被析构,lambda 捕获的 this 失效,i 值未定义 134 | } 135 | ``` 136 | 137 | * 更细致的解决方法是,捕获数据成员的一个拷贝 138 | 139 | ```cpp 140 | #include 141 | #include 142 | 143 | struct A { 144 | auto f() { 145 | int j = i; 146 | return [j] { std::cout << j; }; 147 | }; 148 | int i = 1; 149 | }; 150 | 151 | auto g() { 152 | auto p = std::make_unique(); 153 | return p->f(); 154 | } 155 | 156 | int main() { 157 | g()(); // 1 158 | } 159 | ``` 160 | 161 | * C++14 中提供了广义 lambda 捕获(generalized lambda-capture),也叫初始化捕获(init-capture),可以直接在捕获列表中拷贝 162 | 163 | ```cpp 164 | #include 165 | #include 166 | 167 | struct A { 168 | auto f() { 169 | return [i = i] { std::cout << i; }; 170 | }; 171 | int i = 1; 172 | }; 173 | 174 | auto g = [p = std::make_unique()] { return p->f(); }; 175 | 176 | int main() { 177 | g()(); // 1 178 | } 179 | ``` 180 | 181 | * 值捕获似乎表明闭包是独立的,与闭包外的数据无关,但并非如此,比如 lambda 可以直接使用 static 变量(但无法捕获) 182 | 183 | ```cpp 184 | #include 185 | 186 | static int i = 1; 187 | 188 | int main() { 189 | constexpr auto f = [] { return i; }; 190 | assert(f() == i); 191 | } 192 | ``` 193 | 194 | ## 32 用初始化捕获将对象移入闭包 195 | 196 | * move-only 类型对象不支持拷贝,只能采用引用捕获 197 | 198 | ```cpp 199 | auto p = std::make_unique(42); 200 | auto f = [&p]() { std::cout << *p; }; 201 | ``` 202 | 203 | * 初始化捕获则支持把 move-only 类型对象移动进 lambda 中 204 | 205 | ```cpp 206 | auto p = std::make_unique(42); 207 | auto f = [p = std::move(p)]() { std::cout << *p; }; 208 | assert(p == nullptr); 209 | ``` 210 | 211 | * 还可以直接在捕获列表中初始化 move-only 类型对象 212 | 213 | ```cpp 214 | auto f = [p = std::make_unique(42)]() { std::cout << *p; }; 215 | ``` 216 | 217 | * 如果不使用 lambda,C++11 中可以封装一个类来模拟 lambda 的初始化捕获 218 | 219 | ```cpp 220 | class A { 221 | public: 222 | A(std::unique_ptr&& p) : p_(std::move(p)) {} 223 | void operator()() const { std::cout << *p_; } 224 | 225 | private: 226 | std::unique_ptr p_; 227 | }; 228 | 229 | auto f = A(std::make_unique(42)); 230 | ``` 231 | 232 | * 如果要在 C++11 中使用 lambda 并模拟初始化捕获,需要借助 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 233 | 234 | ```cpp 235 | auto f = std::bind([](const std::unique_ptr& p) { std::cout << *p; }, 236 | std::make_unique(42)); 237 | ``` 238 | 239 | * bind 对象([std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 返回的对象)中包含传递给 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 的所有实参的拷贝,对于每个左值实参,bind 对象中的对应部分由拷贝构造,对于右值实参则是移动构造。上例中第二个实参是右值,采用的是移动构造,这就是把右值移动进 bind 对象的手法 240 | 241 | ```cpp 242 | std::vector v; // 要移动到闭包的对象 243 | // C++14:初始化捕获 244 | auto f = [v = std::move(v)] {}; 245 | // C++11:模拟初始化捕获 246 | auto g = std::bind([](const std::vector& v) {}, std::move(v)); 247 | ``` 248 | 249 | * 默认情况下,lambda 生成的闭包类的 operator() 默认为 const,闭包中的所有成员变量也会为 const,因此上述模拟中使用的 lambda 形参都为 const 250 | 251 | ```cpp 252 | auto f = [](auto x, auto y) { return x < y; }; 253 | 254 | // 上述 lambda 相当于生成如下匿名类 255 | struct X { 256 | template 257 | auto operator()(T x, U y) const { 258 | return x < y; 259 | } 260 | }; 261 | ``` 262 | 263 | * 如果是可变 lambda,则闭包中的 operator() 就不会为 const,因此模拟可变 lambda 则模拟中使用的 lambda 形参就不必声明为 const 264 | 265 | ```cpp 266 | std::vector v; 267 | // C++14:初始化捕获 268 | auto f = [v = std::move(v)]() mutable {}; 269 | // C++11:模拟可变 lambda 的初始化捕获 270 | auto g = std::bind([](std::vector& v) {}, std::move(v)); 271 | ``` 272 | 273 | * 因为 bind 对象的生命期和闭包相同,所以对 bind 对象中的对象和闭包中的对象可以用同样的手法处理 274 | 275 | ## 33 用 [decltype](https://en.cppreference.com/w/cpp/language/decltype) 获取 auto&& 参数类型以 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 276 | 277 | * 对于泛型 lambda 278 | 279 | ```cpp 280 | auto f = [](auto x) { return g(x); }; 281 | ``` 282 | 283 | * 同样可以使用完美转发 284 | 285 | ```cpp 286 | // 传入参数是 auto,类型未知,std::forward 的模板参数应该是什么? 287 | auto f = [](auto&& x) { return g(std::forward < ? > (x)); }; 288 | ``` 289 | 290 | * 此时可以用 decltype 判断传入的实参是左值还是右值 291 | * 如果传递给 auto&& 的实参是左值,则 x 为左值引用类型,decltype(x) 为左值引用类型 292 | * 如果传递给 auto&& 的实参是右值,则 x 为右值引用类型,decltype(x) 为右值引用类型 293 | 294 | ```cpp 295 | auto f = [](auto&& x) { return g(std::forward(x)); }; 296 | ``` 297 | 298 | * 转发任意数量的实参 299 | 300 | ```cpp 301 | auto f = [](auto&&... args) { 302 | return g(std::forward(args)...); 303 | }; 304 | ``` 305 | 306 | ## 34 用 lambda 替代 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 307 | 308 | * lambda 代码更简洁 309 | 310 | ```cpp 311 | int l = 0; 312 | int r = 9; 313 | auto f = [l, r](const auto& x) { return l <= x && x <= r; }; 314 | 315 | // 用 std::bind 实现相同效果 316 | using namespace std::placeholders; 317 | // C++14 318 | auto f = std::bind(std::logical_and<>{}, std::bind(std::less_equal<>{}, l, _1), 319 | std::bind(std::less_equal<>(), _1, r)); 320 | // C++11 321 | auto f = std::bind(std::logical_and{}, 322 | std::bind(std::less_equal{}, l, _1), 323 | std::bind(std::less_equal{}, _1, r)); 324 | ``` 325 | 326 | * lambda 可以指定值捕获和引用捕获,而 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 总会按值拷贝实参,要按引用传递则需要使用 [std::ref](https://en.cppreference.com/w/cpp/utility/functional/ref) 327 | 328 | ```cpp 329 | void f(const A&); 330 | 331 | using namespace std::placeholders; 332 | A a; 333 | auto g = std::bind(f, std::ref(a), _1); 334 | ``` 335 | 336 | * lambda 中可以正常使用重载函数,而 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 无法区分重载版本,为此必须指定对应的函数指针类型。lambda 闭包类的 operator() 采用的是能被编译器内联的常规的函数调用,而 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 采用的是一般不会被内联的函数指针调用,这意味着 lambda 比 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 运行得更快 337 | 338 | ```cpp 339 | void f(int) {} 340 | void f(double) {} 341 | 342 | auto g = [] { f(1); }; // OK 343 | auto g = std::bind(f, 1); // 错误 344 | auto g = std::bind(static_cast(f), 1); // OK 345 | ``` 346 | 347 | * 实参绑定的是 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 返回的对象,而非内部的函数 348 | 349 | ```cpp 350 | #include 351 | #include 352 | #include 353 | #include 354 | 355 | void f(std::chrono::steady_clock::time_point t, int i) { 356 | std::this_thread::sleep_until(t); 357 | std::cout << i; 358 | } 359 | 360 | int main() { 361 | using namespace std::chrono_literals; 362 | using namespace std::placeholders; 363 | 364 | auto g1 = [](int i) { f(std::chrono::steady_clock::now() + 3s, i); }; 365 | g1(1); // 3 秒后打印 1 366 | 367 | auto g2 = std::bind(f, std::chrono::steady_clock::now() + 3s, _1); 368 | g2(1); // 调用 std::bind 后的 3 秒打印 1,而非调用 f 后的 3 秒 369 | } 370 | ``` 371 | 372 | * 上述代码的问题在于,计算时间的表达式作为实参被传递给 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind),因此计算发生在调用 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 的时刻,而非调用其绑定的函数的时刻。解决办法是延迟到调用绑定的函数时再计算表达式值,这可以通过在内部再嵌套一个 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 来实现 373 | 374 | ```cpp 375 | auto g = std::bind(f, 376 | std::bind(std::plus<>{}, std::chrono::steady_clock::now(), 377 | std::chrono::seconds{3}), 378 | std::placeholders::_1); 379 | ``` 380 | 381 | * C++14 中没有需要使用 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 的理由,C++11 由于特性受限存在两个使用场景,一是模拟 C++11 缺少的移动捕获,二是函数对象的 operator() 是模板时,若要将此函数对象作为参数使用,用 [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 绑定才能接受任意类型实参 382 | 383 | ```cpp 384 | struct X { 385 | template 386 | void operator()(const T&) const; 387 | }; 388 | 389 | X x; 390 | auto f = std::bind(x, _1); // f 可以接受任意参数类型 391 | ``` 392 | 393 | * C++11 的 lambda 无法达成上述效果,但 C++14 可以 394 | 395 | ```cpp 396 | X a; 397 | auto f = [a](const auto& x) { a(x); }; 398 | ``` 399 | 400 | ### [std::bind](https://en.cppreference.com/w/cpp/utility/functional/bind) 用法示例 401 | 402 | * 占位符 403 | 404 | ```cpp 405 | #include 406 | #include 407 | 408 | void f(int a, int b, int c) { std::cout << a << b << c; } 409 | 410 | int main() { 411 | using namespace std::placeholders; 412 | auto x = std::bind(f, _2, _1, 3); 413 | // _n 表示 f 的第 n 个参数 414 | // x(a, b, c) 相当于 f(b, a, 3); 415 | x(4, 5, 6); // 543 416 | } 417 | ``` 418 | 419 | * 传引用 420 | 421 | ```cpp 422 | #include 423 | #include 424 | 425 | void f(int& n) { ++n; } 426 | 427 | int main() { 428 | int n = 1; 429 | auto x = std::bind(f, n); 430 | x(); 431 | assert(n == 1); 432 | auto y = std::bind(f, std::ref(n)); 433 | y(); 434 | assert(n == 2); 435 | } 436 | ``` 437 | 438 | * 传递占位符给其他函数 439 | 440 | ```cpp 441 | #include 442 | #include 443 | #include 444 | 445 | void f(int a, int b) { std::cout << a << b; } 446 | int g(int n) { return n + 1; } 447 | 448 | int main() { 449 | using namespace std::placeholders; 450 | auto x = std::bind(f, _1, std::bind(g, _1)); 451 | x(1); // 12 452 | } 453 | ``` 454 | 455 | * 绑定成员函数指针 456 | 457 | ```cpp 458 | #include 459 | #include 460 | 461 | struct A { 462 | int f(int n) const { return n; } 463 | }; 464 | 465 | int main() { 466 | A a; 467 | auto x = std::bind(&A::f, &a, std::placeholders::_1); 468 | assert(x(42) == 42); 469 | auto y = std::bind(&A::f, a, std::placeholders::_1); 470 | assert(y(42) == 42); 471 | } 472 | ``` 473 | 474 | * 绑定数据成员 475 | 476 | ```cpp 477 | #include 478 | #include 479 | #include 480 | 481 | struct A { 482 | int i = 0; 483 | }; 484 | 485 | int main() { 486 | auto x = std::bind(&A::i, std::placeholders::_1); 487 | A a; 488 | assert(x(a) == 1); 489 | assert(x(&a) == 1); 490 | assert(x(std::make_unique(a)) == 1); 491 | assert(x(std::make_shared(a)) == 1); 492 | } 493 | ``` 494 | 495 | * 生成随机数 496 | 497 | ```cpp 498 | #include 499 | #include 500 | #include 501 | 502 | int main() { 503 | std::default_random_engine e; 504 | std::uniform_int_distribution<> d(0, 9); 505 | auto x = std::bind(d, e); 506 | for (int i = 0; i < 20; ++i) { 507 | std::cout << x(); 508 | } 509 | } 510 | ``` 511 | 512 | ### [std::function](https://en.cppreference.com/w/cpp/utility/functional/function) 用法示例 513 | 514 | * 存储函数对象 515 | 516 | ```cpp 517 | #include 518 | #include 519 | 520 | struct X { 521 | int operator()(int n) const { return n; } 522 | }; 523 | 524 | int main() { 525 | std::function f = X(); 526 | assert(f(0) == 0); 527 | } 528 | ``` 529 | 530 | * 存储 bind 对象 531 | 532 | ```cpp 533 | #include 534 | #include 535 | 536 | int f(int i) { return i; } 537 | 538 | int main() { 539 | std::function g = std::bind(f, std::placeholders::_1); 540 | assert(g(0) == 0); 541 | } 542 | ``` 543 | 544 | * 存储绑定成员函数指针的 bind 对象 545 | 546 | ```cpp 547 | #include 548 | #include 549 | 550 | struct A { 551 | int f(int n) const { return n; } 552 | }; 553 | 554 | int main() { 555 | A a; 556 | std::function g = std::bind(&A::f, &a, std::placeholders::_1); 557 | assert(g(0) == 0); 558 | } 559 | ``` 560 | 561 | * 存储成员函数 562 | 563 | ```cpp 564 | #include 565 | #include 566 | 567 | struct A { 568 | int f(int n) const { return n; } 569 | }; 570 | 571 | int main() { 572 | std::function g1 = &A::f; 573 | A a; 574 | assert(g1(a, 0) == 0); 575 | 576 | std::function g2 = &A::f; 577 | const A b; 578 | assert(g2(b, 0) == 0); 579 | } 580 | ``` 581 | 582 | * 存储数据成员 583 | 584 | ```cpp 585 | #include 586 | #include 587 | 588 | struct A { 589 | int i = 0; 590 | }; 591 | 592 | int main() { 593 | std::function g = &A::i; 594 | A a; 595 | assert(g(a) == 0); 596 | } 597 | ``` 598 | -------------------------------------------------------------------------------- /docs/01_deducing_types.md: -------------------------------------------------------------------------------- 1 | ## 01 模板类型推断机制 2 | 3 | * auto 推断的基础是模板类型推断机制,但部分特殊情况下,模板推断机制不适用于 auto 4 | * 模板的形式可以看成如下伪代码 5 | 6 | ```cpp 7 | template 8 | void f(ParamType x); // ParamType 即 x 的类型 9 | ``` 10 | 11 | * 调用可看成 12 | 13 | ```cpp 14 | f(expr); 15 | ``` 16 | 17 | * 编译期间,编译器用 expr 推断 T 和 ParamType,实际上两者通常不一致,比如 18 | 19 | ```cpp 20 | template 21 | void f(const T& x); 22 | 23 | int x; // 为方便演示,只指定类型不初始化,后续同理 24 | f(x); // T 被推断为 int,ParamType 被推断为 const int& 25 | ``` 26 | 27 | * T 的类型推断与 expr 和 ParamType 相关 28 | 29 | ### 情形 1:ParamType 不是引用或指针 30 | 31 | * 丢弃 expr 的 top-level cv 限定符和引用限定符,最后得到的 expr 类型就是 T 和 ParamType 的类型 32 | 33 | ```cpp 34 | template 35 | void f(T x); 36 | 37 | int a; 38 | const int b; 39 | const int& c; 40 | 41 | int* p1; 42 | const int* p2; 43 | int* const p3; 44 | const int* const p4; 45 | 46 | char s1[] = "downdemo"; 47 | const char s2[] = "downdemo"; 48 | 49 | // 以下情况 T 和 ParamType 都是 int 50 | f(a); 51 | f(b); 52 | f(c); 53 | // 指针类型丢弃的是 top-level const(即指针本身的 const) 54 | // low-level const(即所指对象的 const)会保留 55 | f(p1); // T 和 ParamType 都是 int* 56 | f(p2); // T 和 ParamType 都是 const int* 57 | f(p3); // T 和 ParamType 都是 int* 58 | f(p4); // T 和 ParamType 都是 const int* 59 | // char 数组会退化为指针 60 | f(s1); // T 和 ParamType 都是 char* 61 | f(s2); // T 和 ParamType 都是 const char* 62 | ``` 63 | 64 | ### 情形 2:ParamType 是引用类型 65 | 66 | * 如果 expr 的类型是引用,保留 cv 限定符,ParamType 一定是左值引用类型,ParamType 去掉引用符就是 T 的类型,即 T 一定不是引用类型 67 | 68 | ```cpp 69 | template 70 | void f(T& x); 71 | 72 | int a; 73 | int& b; 74 | int&& c; 75 | const int d; 76 | const int& e; 77 | 78 | int* p1; 79 | const int* p2; 80 | int* const p3; 81 | const int* const p4; 82 | 83 | char s1[] = "downdemo"; 84 | const char s2[] = "downdemo"; 85 | 86 | f(a); // ParamType 是 int&,T 是 int 87 | f(b); // ParamType 是 int&,T 是 int 88 | f(c); // ParamType 是 int&,T 是 int 89 | f(d); // ParamType 是 const int&,T 是 const int 90 | f(e); // ParamType 是 const int&,T 是 const int 91 | // 因为 top-level const 和 low-level const 都保留 92 | // 对于指针只要记住 const 的情况和实参类型一样 93 | f(p1); // ParamType 是 int* &,T 是 int* 94 | f(p2); // ParamType 是 const int* &,T 是 const int* 95 | f(p3); // ParamType 是 int* const&,T 是 int* const 96 | f(p4); // ParamType 是 const int* const &,T 是 const int* const 97 | // 数组类型对于 T& 的情况比较特殊,不会退化到指针 98 | f(s1); // ParamType 是 char(&)[9],T 是 char[9] 99 | f(s2); // ParamType 是 const char(&)[9],T 是 const char[9] 100 | ``` 101 | 102 | * 如果把 ParamType 从 T& 改为 const T&,区别只是 ParamType 一定为 top-level const,ParamType 去掉 top-level const 和引用符就是 T 的类型,即 T 一定不为 top-level const 引用类型 103 | 104 | ```cpp 105 | template 106 | void f(const T& x); 107 | 108 | int a; 109 | int& b; 110 | int&& c; 111 | const int d; 112 | const int& e; 113 | 114 | int* p1; 115 | const int* p2; 116 | int* const p3; 117 | const int* const p4; 118 | 119 | char s1[] = "downdemo"; 120 | const char s2[] = "downdemo"; 121 | 122 | // 以下情况 ParamType 都是 const int&,T 都是 int 123 | f(a); 124 | f(b); 125 | f(c); 126 | f(d); 127 | f(e); 128 | // 数组类型类似 129 | f(s1); // ParamType 是 const char(&)[9],T 是 char[9] 130 | f(s2); // ParamType 是 const char(&)[9],T 是 char[9] 131 | // 对于指针只要记住,T 的指针符后一定无 const 132 | f(p1); // ParamType 是 int* const &,T 是 int* 133 | f(p2); // ParamType 是 const int* const &,T 是 const int* 134 | f(p3); // ParamType 是 int* const&,T 是 int* 135 | f(p4); // ParamType 是 const int* const &,T 是 const int* 136 | ``` 137 | 138 | * 对应数组类型的模板参数类型应声明为 `T(&)[N]`,即数组类型 `T[N]` 的引用 139 | 140 | ```cpp 141 | namespace jc { 142 | 143 | template 144 | constexpr int f(T (&)[N]) noexcept { 145 | return N; 146 | } 147 | 148 | } // namespace jc 149 | 150 | int main() { 151 | const char s[] = "downdemo"; 152 | static_assert(jc::f(s) == 9); 153 | } 154 | ``` 155 | 156 | ### 情形 3:ParamType 是指针类型 157 | 158 | * 与情形 2 类似,ParamType 一定是 non-const 指针(传参时忽略 top-level const)类型,去掉指针符就是 T 的类型,即 T 一定不为指针类型 159 | 160 | ```cpp 161 | template 162 | void f(T* x); 163 | 164 | int a; 165 | const int b; 166 | 167 | int* p1; 168 | const int* p2; 169 | int* const p3; // 传参时与 p1 类型一致 170 | const int* const p4; // 传参时与 p2 类型一致 171 | 172 | char s1[] = "downdemo"; 173 | const char s2[] = "downdemo"; 174 | 175 | f(&a); // ParamType 是 int*,T 是 int 176 | f(&b); // ParamType 是 const int*,T 是 const int 177 | 178 | f(p1); // ParamType 是 int*,T 是 int 179 | f(p2); // ParamType 是 const int*,T 是 const int 180 | f(p3); // ParamType 是 int*,T 是 int 181 | f(p4); // ParamType 是 const int*,T 是 const int 182 | 183 | // 数组类型会转为指针类型 184 | f(s1); // ParamType 是 char*,T 是 char 185 | f(s2); // ParamType 是 const char*,T 是 const char 186 | ``` 187 | 188 | * 如果 ParamType 是 const-pointer,和上面实际上是同一个模板,ParamType 多出 top-level const,T 不变 189 | 190 | ```cpp 191 | template 192 | void f(T* const x); 193 | 194 | int a; 195 | const int b; 196 | 197 | int* p1; // 传参时与 p3 类型一致 198 | const int* p2; // 传参时与 p4 类型一致 199 | int* const p3; 200 | const int* const p4; 201 | 202 | char s1[] = "downdemo"; 203 | const char s2[] = "downdemo"; 204 | 205 | f(&a); // ParamType 是 int* const,T 是 int 206 | f(&b); // ParamType 是 const int* const,T 是 const int 207 | 208 | f(p1); // ParamType 是 int* const,T 是 int 209 | f(p2); // ParamType 是 const int* const,T 是 const int 210 | f(p3); // ParamType 是 int* const,T 是 int 211 | f(p4); // ParamType 是 const int* const,T 是 const int 212 | 213 | f(s1); // ParamType 是 char* const,T 是 char 214 | f(s2); // ParamType 是 const char* const,T 是 const char 215 | ``` 216 | 217 | * 如果 ParamType 是 pointer to const,则只有一种结果,T 一定是不带 const 的非指针类型 218 | 219 | ```cpp 220 | template 221 | void f(const T* x); 222 | 223 | template 224 | void g(const T* const x); 225 | 226 | int a; 227 | const int b; 228 | 229 | int* p1; 230 | const int* p2; 231 | int* const p3; 232 | const int* const p4; 233 | 234 | char s1[] = "downdemo"; 235 | const char s2[] = "downdemo"; 236 | 237 | // 以下情况 ParamType 都是 const int*,T 都是 int 238 | f(&a); 239 | f(&b); 240 | f(p1); 241 | f(p2); 242 | f(p3); 243 | f(p4); 244 | // 以下情况 ParamType 都是 const int* const,T 都是 int 245 | g(&a); 246 | g(&b); 247 | g(p1); 248 | g(p2); 249 | g(p3); 250 | g(p4); 251 | // 以下情况 ParamType 都是 const char*,T 都是 char 252 | f(s1); 253 | f(s2); 254 | g(s1); 255 | g(s2); 256 | ``` 257 | 258 | ### 情形 4:ParamType 是转发引用 259 | 260 | * 如果 expr 是左值,T 和 ParamType 都推断为左值引用。这有两点非常特殊 261 | * 这是 T 被推断为引用的唯一情形 262 | * ParamType 使用右值引用语法,却被推断为左值引用 263 | * 如果 expr 是右值,则 ParamType 推断为右值引用类型,去掉 && 就是 T 的类型,即 T 一定不为引用类型 264 | 265 | ```cpp 266 | template 267 | void f(T&& x); 268 | 269 | int a; 270 | const int b; 271 | const int& c; 272 | int&& d = 1; // d 是右值引用,也是左值,右值引用是只能绑定右值的引用而不是右值 273 | 274 | char s1[] = "downdemo"; 275 | const char s2[] = "downdemo"; 276 | 277 | f(a); // ParamType 和 T 都是 int& 278 | f(b); // ParamType 和 T 都是 const int& 279 | f(c); // ParamType 和 T 都是 const int& 280 | f(d); // ParamType 和 T 都是 const int& 281 | f(1); // ParamType 是 int&&,T 是 int 282 | 283 | f(s1); // ParamType 和 T 都是 char(&)[9] 284 | f(s2); // ParamType 和 T 都是 const char(&)[9] 285 | ``` 286 | 287 | ### 特殊情形:expr 是函数名 288 | 289 | ```cpp 290 | template 291 | void f1(T x); 292 | 293 | template 294 | void f2(T& x); 295 | 296 | template 297 | void f3(T&& x); 298 | 299 | void g(int); 300 | 301 | f1(g); // T 和 ParamType 都是 void(*)(int) 302 | f2(g); // ParamType 是 void(&)(int),T 是 void()(int) 303 | f3(g); // T 和 ParamType 都是 void(&)(int) 304 | ``` 305 | 306 | ## 02 [auto](https://en.cppreference.com/w/cpp/language/auto) 类型推断机制 307 | 308 | * auto 类型推断几乎和模板类型推断一致 309 | * 调用模板时,编译器根据 expr 推断 T 和 ParamType 的类型。当变量用 auto 声明时,auto 就扮演了模板中的 T 的角色,变量的类型修饰符则扮演 ParamType 的角色 310 | * 为了推断变量类型,编译器表现得好比每个声明对应一个模板,模板的调用就相当于对应的初始化表达式 311 | 312 | ```cpp 313 | auto x = 1; 314 | const auto cx = x; 315 | const auto& rx = x; 316 | 317 | template // 用来推断 x 类型的概念上假想的模板 318 | void func_for_x(T x); 319 | 320 | func_for_x(1); // 假想的调用: param 的推断类型就是 x 的类型 321 | 322 | template // 用来推断 cx 类型的概念上假想的模板 323 | void func_for_cx(const T x); 324 | 325 | func_for_cx(x); // 假想的调用: param 的推断类型就是 cx 的类型 326 | 327 | template // 用来推断 rx 类型的概念上假想的模板 328 | void func_for_rx(const T& x); 329 | 330 | func_for_rx(x); // 假想的调用: param 的推断类型就是 rx 的类型 331 | ``` 332 | 333 | * auto 的推断适用模板推断机制的三种情形:T&、T&& 和 T 334 | 335 | ```cpp 336 | auto x = 1; // int x 337 | const auto cx = x; // const int cx 338 | const auto& rx = x; // const int& rx 339 | auto&& uref1 = x; // int& uref1 340 | auto&& uref2 = cx; // const int& uref2 341 | auto&& uref3 = 1; // int&& uref3 342 | ``` 343 | 344 | * auto 对数组和指针的推断也和模板一致 345 | 346 | ```cpp 347 | const char name[] = "downdemo"; // 数组类型是 const char[9] 348 | auto arr1 = name; // const char* arr1 349 | auto& arr2 = name; // const char (&arr2)[9] 350 | 351 | void g(int, double); // 函数类型是 void(int, double) 352 | auto f1 = g; // void (*f1)(int, double) 353 | auto& f2 = g; // void (&f2)(int, double) 354 | ``` 355 | 356 | * auto 推断唯一不同于模板实参推断的情形是 C++11 的初始化列表。下面是同样的赋值功能 357 | 358 | ```cpp 359 | // C++98 360 | int x1 = 1; 361 | int x2(1); 362 | 363 | // C++11 364 | int x3 = {1}; 365 | int x4{1}; 366 | ``` 367 | 368 | * 但换成 auto 声明,这些赋值的意义就不一样了 369 | 370 | ```cpp 371 | auto x1 = 1; // int x1 372 | auto x2(1); // int x2 373 | auto x3 = {1}; // std::initializer_list x3 374 | auto x4{1}; // C++11 为 std::initializer_list x4,C++14 为 int x4 375 | ``` 376 | 377 | * 如果初始化列表中元素类型不同,则无法推断 378 | 379 | ```cpp 380 | auto x = {1, 2, 3.0}; // 错误:不能为 std::initializer_list 推断 T 381 | ``` 382 | 383 | * C++14 禁止对 auto 用 [std::initializer_list](https://en.cppreference.com/w/cpp/utility/initializer_list) 直接初始化,而必须用 =,除非列表中只有一个元素,这时不会将其视为 [std::initializer_list](https://en.cppreference.com/w/cpp/utility/initializer_list) 384 | 385 | ```cpp 386 | auto x1 = {1, 2}; // C++14 中必须用 =,否则报错 387 | auto x2{1}; // 允许单元素的直接初始化,不会将其视为 initializer_list 388 | ``` 389 | 390 | * 模板不支持模板参数为 T 而 expr 为初始化列表的推断,不会将其假设为 [std::initializer_list](https://en.cppreference.com/w/cpp/utility/initializer_list),这就是 auto 推断和模板推断唯一的不同之处 391 | 392 | ```cpp 393 | auto x = {1, 2, 3}; // x 类型是 std::initializer_list 394 | 395 | template // 等价于 x 声明的模板 396 | void f(T x); 397 | 398 | f({1, 2, 3}); // 错误:不能推断 T 的类型 399 | ``` 400 | 401 | * 不过将模板参数为 [std::initializer_list](https://en.cppreference.com/w/cpp/utility/initializer_list) 则可以推断 T 402 | 403 | ```cpp 404 | template 405 | void f(std::initializer_list initList); 406 | 407 | f({1, 2, 3}); // T 被推断为 int,initList 类型是 std::initializer_list 408 | ``` 409 | 410 | ### C++14 的 auto 411 | 412 | * C++14 中,auto 可以作为函数返回类型,并且 lambda 可以将参数声明为 auto,这种 lambda 称为泛型 lambda 413 | 414 | ```cpp 415 | auto f() { return 1; } 416 | auto g = [](auto x) { return x; }; 417 | ``` 418 | 419 | * 但此时 auto 仍然使用的是模板实参推断的机制,因此不能为 auto 返回类型返回一个初始化列表,即使是单元素 420 | 421 | ```cpp 422 | auto f() { return {1}; } // 错误 423 | ``` 424 | 425 | * 泛型 lambda 同理 426 | 427 | ```cpp 428 | std::vector v{2, 4, 6}; 429 | auto f = [&v](const auto& x) { v = x; }; 430 | f({1, 2, 3}); // 错误 431 | ``` 432 | 433 | * 如果返回的表达式递归调用函数,则不会发生推断 434 | 435 | ```cpp 436 | auto f(int n) { 437 | if (n <= 1) { 438 | return 1; // OK:返回类型被推断为 int 439 | } else { 440 | return n * f(n - 1); // OK:f(n - 1) 为 int,所以 n * f(n - 1) 也为 int 441 | } 442 | } 443 | 444 | auto g(int n) { 445 | if (n > 1) { 446 | return n * g(n - 1); // 错误:g(n - 1) 类型未知 447 | } else { 448 | return 1; 449 | } 450 | } 451 | ``` 452 | 453 | * 没有返回值时 auto 返回类型会推断为 void,若不能匹配 void 则出错 454 | 455 | ```cpp 456 | auto f1() {} // OK:返回类型是 void 457 | auto f2() { return; } // OK:返回类型是 void 458 | auto* f3() {} // 错误:auto*不能推断为 void 459 | ``` 460 | 461 | ### C++17 的 auto 462 | 463 | * C++17 中,auto 可以作为非类型模板参数 464 | 465 | ```cpp 466 | namespace jc { 467 | 468 | template 469 | constexpr T add(T n) { 470 | return n + N; 471 | } 472 | 473 | template 474 | constexpr T add(T n) { 475 | return n + N; 476 | } 477 | 478 | } // namespace jc 479 | 480 | static_assert(jc::add<2>(3) == 5); 481 | static_assert(jc::add(3) == 3); 482 | 483 | int main() {} 484 | ``` 485 | 486 | * C++17 引入了[结构化绑定(structured binding)](https://en.cppreference.com/w/cpp/language/structured_binding) 487 | 488 | ```cpp 489 | #include 490 | #include 491 | #include 492 | 493 | namespace jc { 494 | 495 | struct A { 496 | int n = 42; 497 | std::string s = "hello"; 498 | }; 499 | 500 | A f() { return {}; } 501 | 502 | } // namespace jc 503 | 504 | int main() { 505 | const auto&& [n, s] = jc::f(); 506 | assert(n == 42); 507 | assert(s == "hello"); 508 | 509 | int a[] = {1, 2, 3}; 510 | auto& [x, y, z] = a; 511 | assert(&x == a); 512 | assert(&y == a + 1); 513 | assert(&z == a + 2); 514 | 515 | auto t = std::make_tuple(true, 'c'); 516 | auto& [b, c] = t; // auto& b = std::get<0>(t); auto& c = std::get<1>(t); 517 | assert(&b == &std::get<0>(t)); 518 | assert(&c == &std::get<1>(t)); 519 | } 520 | ``` 521 | 522 | * 特化 [std::tuple_size](https://en.cppreference.com/w/cpp/utility/tuple/tuple_size)、[std::tuple_element](https://en.cppreference.com/w/cpp/utility/tuple/tuple_element)、[std::get(std::tuple)](https://en.cppreference.com/w/cpp/utility/tuple/get) 即可生成一个 tuple-like 类 523 | 524 | ```cpp 525 | #include 526 | #include 527 | #include 528 | 529 | namespace jc { 530 | 531 | struct A {}; 532 | 533 | } // namespace jc 534 | 535 | namespace std { 536 | 537 | template <> 538 | struct std::tuple_size { 539 | static constexpr int value = 2; 540 | }; 541 | 542 | template <> 543 | struct std::tuple_element<0, jc::A> { 544 | using type = int; 545 | }; 546 | 547 | template <> 548 | struct std::tuple_element<1, jc::A> { 549 | using type = std::string; 550 | }; 551 | 552 | template 553 | auto get(jc::A); 554 | 555 | template <> 556 | auto get<0>(jc::A) { 557 | return 42; 558 | } 559 | 560 | template <> 561 | auto get<1>(jc::A) { 562 | return "hello"; 563 | } 564 | 565 | } // namespace std 566 | 567 | int main() { 568 | auto&& [x, y] = jc::A{}; 569 | static_assert(std::is_same_v); 570 | static_assert(std::is_same_v); 571 | assert(x == 42); 572 | assert(y == "hello"); 573 | } 574 | ``` 575 | 576 | ## 03 [decltype](https://en.cppreference.com/w/cpp/language/decltype) 577 | 578 | * decltype 会推断出直觉预期的类型 579 | 580 | ```cpp 581 | const int i = 0; // decltype(i) 为 const int 582 | 583 | struct Point { 584 | int x, y; // decltype(Point::x) 和 decltype(Point::y) 为 int 585 | }; 586 | 587 | A a; // decltype(a) 为 A 588 | bool f(const A& x); // decltype(x) 为 const A&,decltype(f) 为 bool(const A&) 589 | if (f(a)) { // decltype(f(a)) 为 bool 590 | } 591 | 592 | int a[]{1, 2, 3}; // decltype(a) 为 int[3] 593 | ``` 594 | 595 | * decltype 一般用来声明与参数类型相关的返回类型。比如下面模板的参数是容器和索引,而返回类型取决于元素类型 596 | 597 | ```cpp 598 | template 599 | auto f(Container& c, Index i) -> decltype(c[i]) { 600 | return c[i]; // auto 只表示使用类型推断,推断的是 decltype 601 | } 602 | ``` 603 | 604 | * C++14 允许省略尾置返回类型,只留下 auto 605 | 606 | ```cpp 607 | template 608 | auto f(Container& c, Index i) { 609 | return c[i]; 610 | } 611 | ``` 612 | 613 | * 但直接使用会发现问题 614 | 615 | ```cpp 616 | std::vector v{1, 2, 3}; 617 | f(v, 1) = 42; // 返回 v[1] 然后赋值为 42,但不能通过编译 618 | ``` 619 | 620 | * operator[] 返回元素引用,类型为 int&,但 auto 推断为 int,因此上面的操作相当于给一个整型值赋值,显然是错误的 621 | * 为了得到期望的返回类型,需要对返回类型使用 decltype 的推断机制,C++14 允许将返回类型声明为 decltype(auto) 来实现这点 622 | 623 | ```cpp 624 | template 625 | decltype(auto) f(Container& c, Index i) { 626 | return c[i]; 627 | } 628 | ``` 629 | 630 | * decltype(auto) 也可以作为变量声明类型 631 | 632 | ```cpp 633 | int i = 1; 634 | const int& j = i; 635 | decltype(auto) x = j; // const int& x = j; 636 | ``` 637 | 638 | * 但还有一些问题,容器传的是 non-const 左值引用,这就无法接受右值 639 | 640 | ```cpp 641 | std::vector make_v(); // 工厂函数 642 | auto i = f(make_v(), 5); 643 | ``` 644 | 645 | * 为了同时匹配左值和右值而又不想重载,只需要模板参数写为转发引用 646 | 647 | ```cpp 648 | template 649 | decltype(auto) f(Container&& c, Index i) { 650 | return std::forward(c)[i]; // 传入的实参是右值时,将 c 转为右值 651 | } 652 | 653 | // C++11 版本 654 | template 655 | auto f(Container&& c, Index i) -> decltype(std::forward(c)[i]) { 656 | authenticate_user(); 657 | return std::forward(c)[i]; 658 | } 659 | ``` 660 | 661 | ### decltype 的特殊情况 662 | 663 | * 如果表达式是解引用,decltype 会推断为引用类型 664 | 665 | ```cpp 666 | int* p; // decltype(*p) 是 int& 667 | ``` 668 | 669 | * 赋值表达式会产生引用,类型为赋值表达式中左值的引用类型 670 | 671 | ```cpp 672 | int a = 0; 673 | int b = 1; 674 | decltype(a = 1) c = b; // int& 675 | c = 3; 676 | std::cout << a << b << c; // 033 677 | ``` 678 | 679 | * 如果表达式加上一层或多层括号,编译器会将其看作表达式,变量是一种可以作为赋值语句左值的特殊表达式,因此也得到引用类型。decltype((variable)) 结果永远是引用,declytpe(variable) 只有当变量本身是引用时才是引用 680 | 681 | ```cpp 682 | int i; // decltype((i)) 是 int& 683 | ``` 684 | 685 | * 在返回类型为 decltype(auto) 时,这可能导致返回局部变量的引用 686 | 687 | ```cpp 688 | decltype(auto) f1() { 689 | int x = 0; 690 | return x; // decltype(x) 是 int,因此返回 int 691 | } 692 | 693 | decltype(auto) f2() { 694 | int x = 0; 695 | return (x); // decltype((x)) 是 int&,因此返回了局部变量的引用 696 | } 697 | ``` 698 | 699 | ## 04 查看推断类型的方法 700 | 701 | * 实际开发中常用的方法是在 IDE 中将鼠标停放在变量上,现代 IDE 通常会显示出推断的类型 702 | * 利用报错信息,比如写一个声明但不定义的类模板,用这个模板创建实例时将出错,编译将提示错误原因 703 | 704 | ```cpp 705 | template 706 | class A; 707 | 708 | A xType; // 未定义类模板,错误信息将提示 x 类型 709 | // 比如对 int x 报错如下 710 | // error C2079 : “xType” 使用未定义的 class“A” 711 | ``` 712 | 713 | * 使用 [type_id](https://en.cppreference.com/w/cpp/language/typeid) 和 [std::type_info::name](https://en.cppreference.com/w/cpp/types/type_info/name) 获取类型,但得到的类型会忽略 cv 和引用限定符 714 | 715 | ```cpp 716 | template 717 | void f(T& x) { 718 | std::cout << "T = " << typeid(T).name() << '\n'; 719 | std::cout << "x = " << typeid(x).name() << '\n'; 720 | } 721 | ``` 722 | 723 | * 使用 [Boost.TypeIndex](https://www.boost.org/doc/libs/1_79_0/doc/html/boost_typeindex_header_reference.html#header.boost.type_index_hpp) 可以得到精确类型 724 | 725 | ```cpp 726 | #include 727 | #include 728 | 729 | template 730 | void f(const T& x) { 731 | using boost::typeindex::type_id_with_cvr; 732 | std::cout << "T = " << type_id_with_cvr().pretty_name() << '\n'; 733 | std::cout << "x = " << type_id_with_cvr().pretty_name() << '\n'; 734 | } 735 | ``` 736 | -------------------------------------------------------------------------------- /docs/05_rvalue_references_move_semantics_and_perfect_forwarding.md: -------------------------------------------------------------------------------- 1 | * 移动语义使编译器可以用开销较低的移动操作替换昂贵的拷贝操作(*但不是所有情况下移动都会比拷贝快*),是 move-only 类型对象的支持基础 2 | * 完美转发可以将某个函数模板的实参转发给其他函数,转发后的实参保持完全相同的值类别(*左值、右值*) 3 | * 右值引用是移动语义和完美转发的实现基础,它引入了一种新的引用符号(*&&*)来区别于左值引用 4 | * 这些名词很直观,但概念上容易与名称类似的函数混淆 5 | * 移动操作的函数要求传入的实参是右值,无法传入左值,因此需要一个能把左值转换为右值的办法,这就是 [std::move](https://en.cppreference.com/w/cpp/utility/move) 做的事。[std::move](https://en.cppreference.com/w/cpp/utility/move) 本身不进行移动,只是将实参强制转换为右值,以允许把转换的结果传给移动函数 6 | * 完美转发指的是,将函数模板的实参转发给另一个函数,同时保持实参传入给模板时的值类型(*传入的实参是左值则转发后仍是左值,是右值则转发后仍是右值*)。如果不做任何处理的话,不论是传入的是左值还是右值,在传入之后都会变为左值,因此需要一个转换到右值的操作。[std::move](https://en.cppreference.com/w/cpp/utility/move) 可以做到这点,但它对任何类型都会一视同仁地转为右值。这就需要一个折衷的办法,对左值实参不处理,对右值实参(*传入后会变为左值*)转换为右值,这就是 [std::foward](https://en.cppreference.com/w/cpp/utility/forward) 所做的事 7 | * 如果要表示参数是右值,则需要引入一种区别于左值的符号,这就是右值引用符号(*&&*)。右值引用即只能绑定到右值的引用,但其本身是左值(*引用都是左值*)。它只是为了区别于左值引用符号(*&*)而引入的一种符号标记 8 | * 在模板中,带右值引用符号(*T&&*)并不表示一定是右值引用(*这种不确定类型的引用称为转发引用*),因为模板参数本身可以带引用符号(*int&*),此时为了使结果合法(*int& && 是不合法的*),就引入了引用折叠机制(*int& && 折叠为 int&*) 9 | 10 | ## 23 [std::move](https://en.cppreference.com/w/cpp/utility/move) 和 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 只是一种强制类型转换 11 | 12 | * [std::move](https://en.cppreference.com/w/cpp/utility/move) 实现如下 13 | 14 | ```cpp 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace jc { 21 | 22 | template 23 | constexpr std::remove_reference_t&& move(T&& x) noexcept { 24 | return static_cast&&>(x); 25 | } 26 | 27 | constexpr int f(const std::string&) { return 1; } 28 | constexpr int f(std::string&&) { return 2; } 29 | 30 | } // namespace jc 31 | 32 | int main() { 33 | std::string s; 34 | static_assert(jc::f(s) == 1); 35 | assert(jc::f(std::string{}) == 2); 36 | static_assert(jc::f(static_cast(s)) == 2); 37 | static_assert(jc::f(jc::move(s)) == 2); 38 | static_assert(jc::f(std::move(s)) == 2); 39 | } 40 | ``` 41 | 42 | * [std::move](https://en.cppreference.com/w/cpp/utility/move) 会保留 cv 限定符 43 | 44 | ```cpp 45 | #include 46 | 47 | namespace jc { 48 | 49 | constexpr int f(int&&) { return 1; } 50 | constexpr int f(const int&) { return 2; } 51 | 52 | } // namespace jc 53 | 54 | int main() { 55 | const int i = 1; 56 | static_assert(jc::f(std::move(i)) == 2); 57 | } 58 | ``` 59 | 60 | * 这可能导致传入右值却执行拷贝操作 61 | 62 | ```cpp 63 | #include 64 | #include 65 | 66 | class A { 67 | public: 68 | /* 69 | * s 转为 const std::string&& 70 | * 调用 std::string(const std::string&) 71 | */ 72 | explicit A(const std::string s) : s_(std::move(s)) {} 73 | 74 | private: 75 | std::string s_; 76 | }; 77 | ``` 78 | 79 | * 因此如果希望移动 [std::move](https://en.cppreference.com/w/cpp/utility/move) 生成的值,传给 [std::move](https://en.cppreference.com/w/cpp/utility/move) 的就不要是 const 80 | 81 | ```cpp 82 | #include 83 | #include 84 | 85 | class A { 86 | public: 87 | /* 88 | * s 转为 std::string&& 89 | * 调用 std::string(std::string&&) 90 | */ 91 | explicit A(std::string s) : s_(std::move(s)) {} 92 | 93 | private: 94 | std::string s_; 95 | }; 96 | ``` 97 | 98 | * C++11 之前的转发很简单 99 | 100 | ```cpp 101 | #include 102 | 103 | void f(int&) { std::cout << 1; } 104 | void f(const int&) { std::cout << 2; } 105 | 106 | // 用多个重载转发给对应版本比较繁琐 107 | void g(int& x) { f(x); } 108 | 109 | void g(const int& x) { f(x); } 110 | 111 | // 同样的功能可以用一个模板替代 112 | template 113 | void h(T& x) { 114 | f(x); 115 | } 116 | 117 | int main() { 118 | int a = 1; 119 | const int b = 1; 120 | 121 | g(a); 122 | h(a); // 11 123 | g(b); 124 | h(b); // 22 125 | g(1); // 2 126 | // h(1); // 错误 127 | } 128 | ``` 129 | 130 | * C++11 引入了右值引用,但原有的模板无法转发右值。如果使用 [std::move](https://en.cppreference.com/w/cpp/utility/move) 则无法转发左值,因此为了方便引入了 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 131 | 132 | ```cpp 133 | #include 134 | #include 135 | 136 | void f(int&) { std::cout << 1; } 137 | void f(const int&) { std::cout << 2; } 138 | void f(int&&) { std::cout << 3; } 139 | 140 | // 用多个重载转发给对应版本比较繁琐 141 | void g(int& x) { f(x); } 142 | void g(const int& x) { f(x); } 143 | void g(int&& x) { f(std::move(x)); } 144 | 145 | // 用一个模板来替代上述功能 146 | template 147 | void h(T&& x) { 148 | f(std::forward(x)); 149 | } 150 | 151 | int main() { 152 | int a = 1; 153 | const int b = 1; 154 | 155 | g(a); 156 | h(a); // 11 157 | g(b); 158 | h(b); // 22 159 | g(std::move(a)); 160 | h(std::move(a)); // 33 161 | g(1); 162 | h(1); // 33 163 | } 164 | ``` 165 | 166 | * 看起来完全可以用 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 取代 [std::move](https://en.cppreference.com/w/cpp/utility/move),但 [std::move](https://en.cppreference.com/w/cpp/utility/move) 的优势在于清晰简单 167 | 168 | ```cpp 169 | h(std::forward(a)); // 3 170 | h(std::move(a)); // 3 171 | ``` 172 | 173 | ## 24 转发引用与右值引用的区别 174 | 175 | * 带右值引用符号不一定就是右值引用,这种不确定类型的引用称为转发引用 176 | 177 | ```cpp 178 | template 179 | void f(T&&) {} // T&&不一定是右值引用 180 | 181 | int a = 1; 182 | f(a); // T 推断为 int&,T&& 是 int& &&,折叠为 int&,是左值引用 183 | f(1); // T 推断为 int,T&& 是 int&&,右值引用 184 | auto&& b = a; // int& b = a,左值引用 185 | auto&& c = 1; // int&& c = 1,右值引用 186 | ``` 187 | 188 | * 转发引用必须严格按 T&& 的形式涉及类型推断 189 | 190 | ```cpp 191 | template 192 | void f(std::vector&&) {} // 右值引用而非转发引用 193 | 194 | std::vector v; 195 | f(v); // 错误 196 | 197 | template 198 | void g(const T&&) {} // 右值引用而非转发引用 199 | 200 | int i = 1; 201 | g(i); // 错误 202 | ``` 203 | 204 | * T&& 在模板中也可能不涉及类型推断 205 | 206 | ```cpp 207 | template > 208 | class vector { 209 | public: 210 | void push_back(T&& x); // 右值引用 211 | 212 | template 213 | void emplace_back(Args&&... args); // 转发引用 214 | }; 215 | 216 | std::vector v; // 实例化指定了 T 217 | 218 | // 对应的实例化为 219 | class vector> { 220 | public: 221 | void push_back(A&& x); // 不涉及类型推断,右值引用 222 | 223 | template 224 | void emplace_back(Args&&... args); // 转发引用 225 | }; 226 | ``` 227 | 228 | * auto&& 都是转发引用,因为一定涉及类型推断。完美转发中,如果想在转发前修改要转发的值,可以用 auto&& 存储结果,修改后再转发 229 | 230 | ```cpp 231 | template 232 | void f(T x) { 233 | auto&& res = do_something(x); 234 | do_something_else(res); 235 | set(std::forward(res)); 236 | } 237 | ``` 238 | 239 | * lambda 中也可以使用完美转发 240 | 241 | ```cpp 242 | auto f = [](auto&& x) { return g(std::forward(x)); }; 243 | 244 | // 转发任意数量实参 245 | auto f = [](auto&&... args) { 246 | return g(std::forward(args)...); 247 | }; 248 | ``` 249 | 250 | ## 25 对右值引用使用 [std::move](https://en.cppreference.com/w/cpp/utility/move),对转发引用使用 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 251 | 252 | * 右值引用只会绑定到可移动对象上,因此应该使用 [std::move](https://en.cppreference.com/w/cpp/utility/move)。转发引用用右值初始化时才是右值引用,因此应当使用 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 253 | 254 | ```cpp 255 | class A { 256 | public: 257 | A(A&& rhs) : s_(std::move(rhs.s_)), p_(std::move(rhs.p_)) {} 258 | 259 | template 260 | void f(T&& x) { 261 | s_ = std::forward(x); 262 | } 263 | 264 | private: 265 | std::string s_; 266 | std::shared_ptr p_; 267 | }; 268 | ``` 269 | 270 | * 如果希望只有在移动构造函数保证不抛异常时才能转为右值,则可以用 [std::move_if_noexcept](https://en.cppreference.com/w/cpp/utility/move_if_noexcept) 替代 [std::move](https://en.cppreference.com/w/cpp/utility/move) 271 | 272 | ```cpp 273 | #include 274 | #include 275 | 276 | struct A { 277 | A() = default; 278 | A(const A&) { std::cout << 1; } 279 | A(A&&) { std::cout << 2; } 280 | }; 281 | 282 | struct B { 283 | B() {} 284 | B(const B&) noexcept { std::cout << 3; } 285 | B(B&&) noexcept { std::cout << 4; } 286 | }; 287 | 288 | int main() { 289 | A a; 290 | A a2 = std::move_if_noexcept(a); // 1 291 | B b; 292 | B b2 = std::move_if_noexcept(b); // 4 293 | } 294 | ``` 295 | 296 | * 如果返回对象传入时是右值引用或转发引用,在返回时要用 [std::move](https://en.cppreference.com/w/cpp/utility/move) 或 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 转换。返回类型不需要声明为引用,按值传递即可 297 | 298 | ```cpp 299 | A f(A&& a) { 300 | do_something(a); 301 | return std::move(a); 302 | } 303 | 304 | template 305 | A g(T&& x) { 306 | do_something(x); 307 | return std::forward(x); 308 | } 309 | ``` 310 | 311 | * 返回局部变量时,不需要使用 [std::move](https://en.cppreference.com/w/cpp/utility/move) 来优化 312 | 313 | ```cpp 314 | A make_a() { 315 | A a; 316 | return std::move(a); // 画蛇添足 317 | } 318 | ``` 319 | 320 | * 局部变量会直接创建在为返回值分配的内存上,从而避免拷贝,这是 C++ 标准诞生时就有的 [RVO(return value optimization)](https://en.cppreference.com/w/cpp/language/copy_elision)。RVO 的要求十分严谨,它要求局部对象类型与返回值类型相同,且返回的就是局部对象本身,而使用了 [std::move](https://en.cppreference.com/w/cpp/utility/move) 反而不满足 RVO 的要求。此外 RVO 只是种优化,编译器可以选择不采用,但标准规定,即使编译器不省略拷贝,返回对象也会被作为右值处理,所以 [std::move 是多余的](https://www.ibm.com/developerworks/community/blogs/5894415f-be62-4bc0-81c5-3956e82276f3/entry/RVO_V_S_std_move?lang=en) 321 | 322 | ```cpp 323 | A make_a() { return A{}; } 324 | 325 | auto x = make_a(); // 只需要调用一次 A 的默认构造函数 326 | ``` 327 | 328 | ## 26 避免重载使用转发引用的函数 329 | 330 | * 如果函数参数接受左值引用,则传入右值时执行的仍是拷贝 331 | 332 | ```cpp 333 | #include 334 | #include 335 | 336 | std::vector v; 337 | 338 | void f(const std::string& s) { v.emplace_back(s); } 339 | 340 | int main() { 341 | // 传入右值,执行的依然是拷贝 342 | f(std::string{"hi"}); 343 | f("hi"); 344 | } 345 | ``` 346 | 347 | * 让函数接受转发引用即可解决此问题 348 | 349 | ```cpp 350 | #include 351 | #include 352 | #include 353 | 354 | std::vector v; 355 | 356 | template 357 | void f(T&& s) { 358 | v.emplace_back(std::forward(s)); 359 | } 360 | 361 | int main() { 362 | // 现在传入右值时变为移动操作 363 | f(std::string{"hi"}); 364 | f("hi"); 365 | } 366 | ``` 367 | 368 | * 但如果重载这个转发引用版本的函数,就会导致新的问题 369 | 370 | ```cpp 371 | #include 372 | #include 373 | #include 374 | 375 | std::vector v; 376 | 377 | template 378 | void f(T&& s) { 379 | v.emplace_back(std::forward(s)); 380 | } 381 | 382 | std::string make_string(int n) { return std::string("hi"); } 383 | 384 | void f(int n) { v.emplace_back(make_string(n)); } 385 | 386 | int main() { 387 | // 之前的调用仍然正常 388 | f(std::string{"hi"}); 389 | f("hi"); 390 | // 对于重载版本的调用也没问题 391 | f(1); // 调用重载版本 392 | // 但对于非 int(即使能转换到 int)参数就会出现问题 393 | int i = 1; 394 | /* 395 | * 转发引用是比 int 更精确的匹配 396 | * 为 std::vector 传入 short 397 | * 用 short 构造 std::string 导致错误 398 | */ 399 | f(i); 400 | } 401 | ``` 402 | 403 | * 转发引用几乎可以匹配任何类型,因此应该避免对其重载。此外,如果在构造函数中使用转发引用,会导致拷贝构造函数不能被正确匹配 404 | 405 | ```cpp 406 | #include 407 | #include 408 | 409 | std::string make_string(int n) { return std::string{"hi"}; } 410 | 411 | class A { 412 | public: 413 | A() = default; 414 | 415 | template 416 | explicit A(T&& x) : s_(std::forward(x)) {} 417 | 418 | explicit A(int x) : s_(make_string(x)) {} 419 | 420 | private: 421 | std::string s_; 422 | }; 423 | 424 | int main() { 425 | int i = 1; 426 | A a{i}; // 依然调用模板而出错,但还有一个更大的问题 427 | A b{"hi"}; // OK 428 | A c{b}; // 错误:调用的仍是模板,用 A 初始化 std::string 出错 429 | } 430 | ``` 431 | 432 | * 模板构造函数不会阻止合成拷贝和移动构造函数(会阻止合成默认构造函数),上述问题的实际情况如下 433 | 434 | ```cpp 435 | #include 436 | #include 437 | 438 | class A { 439 | public: 440 | template 441 | explicit A(T&& x) : s_(std::forward(x)) {} 442 | 443 | A(const A& rhs) = default; 444 | A(A&& rhs) = default; 445 | 446 | private: 447 | std::string s_; 448 | }; 449 | 450 | int main() { 451 | A a{"hi"}; // OK 452 | A b{a}; // 错误:T&& 比 const A& 更匹配,调用模板用 A 初始化 std::string 出错 453 | const A c{"hi"}; 454 | A d{c}; // OK 455 | } 456 | ``` 457 | 458 | * 上述问题在继承中会变得更为复杂,如果派生类的拷贝和移动操作调用基类的构造函数,同样会匹配到使用了转发引用的模板,从而导致编译错误 459 | 460 | ```cpp 461 | #include 462 | #include 463 | 464 | class A { 465 | public: 466 | template 467 | explicit A(T&& n) : s(std::forward(n)) {} 468 | 469 | private: 470 | std::string s; 471 | }; 472 | 473 | class B : public A { 474 | public: 475 | /* 476 | * 错误:调用基类模板而非拷贝构造函数 477 | * const B 不能转为 std::string 478 | */ 479 | B(const B& rhs) : A(rhs) {} 480 | 481 | /* 482 | * 错误:调用基类模板而非移动构造函数 483 | * B 不能转为 std::string 484 | */ 485 | B(B&& rhs) noexcept : A(std::move(rhs)) {} 486 | }; 487 | ``` 488 | 489 | ## 27 重载转发引用的替代方案 490 | 491 | * 上述问题的最直接解决方案是,不使用重载。其次是使用 C++98 的做法,不使用转发引用 492 | 493 | ```cpp 494 | #include 495 | 496 | class A { 497 | public: 498 | template 499 | explicit A(const T& x) : s_(x) {} 500 | 501 | private: 502 | std::string s_; 503 | }; 504 | 505 | int main() { 506 | A a{"hi"}; 507 | A b{a}; // OK 508 | } 509 | ``` 510 | 511 | * 直接按值传递也是一种简单的方式,而且解决了之前的问题 512 | 513 | ```cpp 514 | #include 515 | #include 516 | 517 | std::string make_string(int n) { return std::string{"hi"}; } 518 | 519 | class A { 520 | public: 521 | explicit A(std::string s) : s_(std::move(s)) {} 522 | explicit A(int n) : s_(make_string(n)) {} 523 | 524 | private: 525 | std::string s_; 526 | }; 527 | 528 | int main() { 529 | int i = 1; 530 | A a{i}; // OK,调用 int 版本的构造函数 531 | } 532 | ``` 533 | 534 | * 不过上述方法实际上是规避了使用转发引用,下面是几种允许转发引用的重载方法 535 | 536 | ### 标签分派(tag dispatching) 537 | 538 | * 标签分派的思路是,额外引入一个参数来打破转发引用的万能匹配 539 | 540 | ```cpp 541 | #include 542 | #include 543 | #include 544 | #include 545 | 546 | std::vector v; 547 | 548 | template 549 | void g(T&& s, std::false_type) { 550 | v.emplace_back(std::forward(s)); 551 | } 552 | 553 | std::string make_string(int n) { return std::string{"hi"}; } 554 | 555 | void g(int n, std::true_type) { v.emplace_back(make_string(n)); } 556 | 557 | template 558 | void f(T&& s) { 559 | g(std::forward(s), std::is_integral>()); 560 | } 561 | 562 | int main() { 563 | int i = 1; 564 | f(i); // OK:调用 int 版本 565 | } 566 | ``` 567 | 568 | ### 使用 [std::enable_if](https://en.cppreference.com/w/cpp/types/enable_if) 在特定条件下禁用模板 569 | 570 | * 标签分派用在构造函数上不太方便,这时可以使用 [std::enable_if](https://en.cppreference.com/w/cpp/types/enable_if) 强制编译器在不满足条件时禁用模板 571 | 572 | ```cpp 573 | #include 574 | #include 575 | 576 | class A { 577 | public: 578 | template >>> 580 | explicit A(T&& x) {} 581 | 582 | private: 583 | std::string s_; 584 | }; 585 | ``` 586 | 587 | * 但这只是在参数具有和类相同的类型时禁用模板,派生类调用基类的构造函数时,派生类和基类也是不同类型,不会禁用模板,因此还需要使用 [std::is_base_of](https://en.cppreference.com/w/cpp/types/is_base_of) 588 | 589 | ```cpp 590 | #include 591 | #include 592 | #include 593 | 594 | class A { 595 | public: 596 | template >>> 598 | explicit A(T&& x) {} 599 | 600 | private: 601 | std::string s_; 602 | }; 603 | 604 | class B : public A { 605 | public: 606 | B(const B& rhs) : A(rhs) {} // OK:不再调用模板 607 | B(B&& rhs) : A(std::move(rhs)) noexcept {} // OK:不再调用模板 608 | }; 609 | ``` 610 | 611 | * 接着在参数为整型时禁用模板,即可解决之前的所有问题。为了更方便调试,可以用 [static_assert](https://en.cppreference.com/w/c/error/static_assert) 预设错误信息,这个错误信息将在不满足预设条件时出现在诊断信息中 612 | 613 | ```cpp 614 | #include 615 | #include 616 | #include 617 | 618 | std::string make_string(int n) { return std::string{"hi"}; } 619 | 620 | class A { 621 | public: 622 | template > && 624 | !std::is_integral_v>>> 625 | explicit A(T&& x) : s_(std::forward(x)) { 626 | static_assert(std::is_constructible_v, 627 | "Parameter n can't be used to construct a std::string"); 628 | } 629 | 630 | explicit A(int n) : s_(make_string(n)) {} 631 | 632 | private: 633 | std::string s_; 634 | }; 635 | 636 | int main() { 637 | int i = 1; 638 | A a{1}; // OK:调用 int 版本的构造函数 639 | A b{"hi"}; // OK 640 | A c{b}; // OK 641 | } 642 | ``` 643 | 644 | ## 28 引用折叠 645 | 646 | * 引用折叠会出现在四种语境中:模板实例化、auto 类型推断、decltype 类型推断、typedef 或 using 别名声明 647 | * 引用的引用是非法的 648 | 649 | ```cpp 650 | int a = 1; 651 | int&& b = a; // 错误 652 | ``` 653 | 654 | * 当左值传给接受转发引用的模板时,模板参数就会推断为引用的引用 655 | 656 | ```cpp 657 | template 658 | void f(T&&); 659 | 660 | int i = 1; 661 | f(i); // T 为 int&,T& && 变成了引用的引用,于是需要引用折叠的机制 662 | ``` 663 | 664 | * 为了使实例化成功,编译器生成引用的引用时,将使用引用折叠的机制,规则如下 665 | 666 | ``` 667 | & + & → & 668 | & + && → & 669 | && + & → & 670 | && + && → && 671 | ``` 672 | 673 | * 引用折叠是 [std::forward](https://en.cppreference.com/w/cpp/utility/forward) 的支持基础 674 | 675 | ```cpp 676 | #include 677 | 678 | namespace jc { 679 | 680 | // 如果传递左值 A,T 推断为 A&,此时需要引用折叠 681 | template 682 | constexpr T&& forward(std::remove_reference_t& t) noexcept { 683 | return static_cast(t); 684 | } 685 | 686 | /* 687 | * 传递左值 A 时相当于 688 | * A&&& forward(std::remove_reference_t& x) { return static_cast(x); } 689 | * 简化后为 690 | * A& forward(A& x) { return static_cast(x); } 691 | * 传递右值 A 相当于 692 | * A&& forward(std::remove_reference_t& x) { return static_cast(x); } 693 | * 简化后为 694 | * A&& forward(A& x) { return static_cast(x); } 695 | */ 696 | 697 | } // namespace jc 698 | ``` 699 | 700 | * auto&& 与使用转发引用的模板原理一样 701 | 702 | ```cpp 703 | int a = 1; 704 | auto&& b = a; // a 是左值,auto 被推断为 int&,int& && 折叠为 int& 705 | ``` 706 | 707 | * decltype 同理,如果推断中出现了引用的引用,就会发生引用折叠 708 | * 如果在 typedef 的创建或求值中出现了引用的引用,就会发生引用折叠 709 | 710 | ```cpp 711 | template 712 | struct A { 713 | using RvalueRef = T&&; // typedef T&& RvalueRef 714 | }; 715 | 716 | int a = 1; 717 | A::RvalueRef b = a; // int& && 折叠为 int&,int& b = a 718 | ``` 719 | 720 | * 并且 top-level cv 限定符会被丢弃 721 | 722 | ```cpp 723 | using A = const int&; // low-level 724 | using B = int&&; // low-level 725 | static_assert(std::is_same_v); 726 | static_assert(std::is_same_v); 727 | ``` 728 | 729 | ## 29 移动不比拷贝快的情况 730 | 731 | * 在如下场景中,C++11 的移动语义没有优势 732 | * 无移动操作:待移动对象不提供移动操作,移动请求将变为拷贝请求 733 | * 移动不比拷贝快:待移动对象虽然有移动操作,但不比拷贝操作快 734 | * 移动不可用:本可以移动时,要求移动操作不能抛异常,但未加上 noexcept 声明 735 | * 除了上述情况,还有一些特殊场景无需使用移动语义,比如之前提到的 [RVO](https://en.cppreference.com/w/cpp/language/copy_elision) 736 | * 移动不一定比拷贝代价小得多。比如 [std::array](https://en.cppreference.com/w/cpp/container/array) 将元素存储在栈上,移动或拷贝对元素逐个执行,O(n) 复杂度,所以移动并不比拷贝快多少 737 | * 另一个移动不一定比拷贝快的例子是 [std::string](https://en.cppreference.com/w/cpp/string/basic_string),一种实现是使用 [SSO(small string optimization)](https://blogs.msmvps.com/gdicanio/2016/11/17/the-small-string-optimization/),在字符串很小时(一般是 15 字节)存储在自身内部,而不使用堆上分配的内存,因此对小型字符串的移动并不比拷贝快 738 | 739 | ## 30 无法完美转发的类型 740 | 741 | * 用相同实参调用原函数和转发函数,如果两者执行不同的操作,则称完美转发失败。完美转发失败源于模板类型推断不符合预期,会导致这个问题的类型包括:大括号初始化值、作为空指针的 0 和 NULL、只声明但未定义的整型 static const 数据成员、重载函数的名称和函数模板名称、位域 742 | 743 | ### 大括号初始化 744 | 745 | ```cpp 746 | void f(const std::vector& v) {} 747 | 748 | template 749 | void fwd(T&& x) { 750 | f(std::forward(x)); 751 | } 752 | 753 | f({1, 2, 3}); // OK,{1, 2, 3} 隐式转换为 std::vector 754 | fwd({1, 2, 3}); // 无法推断 T,导致编译错误 755 | 756 | // 解决方法是借用 auto 推断出 std::initializer_list 类型再转发 757 | auto x = {1, 2, 3}; 758 | fwd(x); // OK 759 | ``` 760 | 761 | ### 作为空指针的 0 或 NULL 762 | 763 | * 0 和 NULL 作为空指针传递给模板时,会推断为 int 而非指针类型 764 | 765 | ```cpp 766 | void f(int*) {} 767 | 768 | template 769 | void fwd(T&& x) { 770 | f(std::forward(x)); 771 | } 772 | 773 | fwd(NULL); // T 推断为 int,转发失败 774 | ``` 775 | 776 | 777 | ### 只声明但未定义的 static const 整型数据成员 778 | 779 | * 类内的 static 成员的声明不是定义,如果 static 成员声明为 const,则编译器会为这些成员值执行 const propagation,从而不需要为它们保留内存。对整型 static const 成员取址可以通过编译,但会导致链接期的错误。转发引用也是引用,在编译器生成的机器代码中,引用一般会被当成指针处理。程序的二进制代码中,从硬件角度看,指针和引用的本质相同 780 | 781 | ```cpp 782 | class A { 783 | public: 784 | static const int n = 1; // 仅声明 785 | }; 786 | 787 | void f(int) {} 788 | 789 | template 790 | void fwd(T&& x) { 791 | f(std::forward(x)); 792 | } 793 | 794 | f(A::n); // OK:等价于 f(1) 795 | fwd(A::n); // 错误:fwd 形参是转发引用,需要取址,无法链接 796 | ``` 797 | 798 | * 但并非所有编译器的实现都有此要求,上述代码可能可以链接。考虑到移植性,最好还是提供定义 799 | 800 | ```cpp 801 | // A.h 802 | class A { 803 | public: 804 | static const int n = 1; 805 | }; 806 | 807 | // A.cpp 808 | const int A::n; 809 | ``` 810 | 811 | ### 重载函数的名称和函数模板名称 812 | 813 | * 如果转发的是函数指针,可以直接将函数名作为参数,函数名会转换为函数指针 814 | 815 | ```cpp 816 | void g(int) {} 817 | 818 | void f(void (*pf)(int)) {} 819 | 820 | template 821 | void fwd(T&& x) { 822 | f(std::forward(x)); 823 | } 824 | 825 | f(g); // OK 826 | fwd(g); // OK 827 | ``` 828 | 829 | * 但如果要转发的函数名对应多个重载函数,则无法转发,因为模板无法从单独的函数名推断出函数类型 830 | 831 | ```cpp 832 | void g(int) {} 833 | void g(int, int) {} 834 | 835 | void f(void (*)(int)) {} 836 | 837 | template 838 | void fwd(T&& x) { 839 | f(std::forward(x)); 840 | } 841 | 842 | f(g); // OK 843 | fwd(g); // 错误:不知道转发的是哪一个函数指针 844 | ``` 845 | 846 | * 转发函数模板名称也会出现同样的问题,因为函数模板可以看成一批重载函数 847 | 848 | ```cpp 849 | template 850 | void g(T x) { 851 | std::cout << x; 852 | } 853 | 854 | void f(void (*pf)(int)) { pf(1); } 855 | 856 | template 857 | void fwd(T&& x) { 858 | f(std::forward(x)); 859 | } 860 | 861 | f(g); // OK 862 | fwd(g); // 错误 863 | ``` 864 | 865 | * 要让转发函数接受重载函数名称或模板名称,只能手动指定需要转发的重载版本或模板实例。不过完美转发本来就是为了接受任何实参类型,而要传入的函数指针类型一般是未知的 866 | 867 | ```cpp 868 | template 869 | void g(T x) { 870 | std::cout << x; 871 | } 872 | 873 | void f(void (*pf)(int)) { pf(1); } 874 | 875 | template 876 | void fwd(T&& x) { 877 | f(std::forward(x)); 878 | } 879 | 880 | using PF = void (*)(int); 881 | PF p = g; 882 | fwd(p); // OK 883 | fwd(static_cast(g)); // OK 884 | ``` 885 | 886 | ### 位域 887 | 888 | * 转发引用也是引用,实际上需要取址,但位域不允许直接取址 889 | 890 | ```cpp 891 | struct A { 892 | int a : 1; 893 | int b : 1; 894 | }; 895 | 896 | void f(int) {} 897 | 898 | template 899 | void fwd(T&& x) { 900 | f(std::forward(x)); 901 | } 902 | 903 | A x{}; 904 | f(x.a); // OK 905 | fwd(x.a); // 错误 906 | ``` 907 | 908 | * 实际上接受位域实参的函数也只能收到位域值的拷贝,因此不需要使用完美转发,换用传值或传 const 引用即可。完美转发中也可以通过强制转换解决此问题,虽然转换的结果是一个临时对象的拷贝而非原有对象,但位域本来就无法做到真正的完美转发 909 | 910 | ```cpp 911 | fwd(static_cast(x.a)); // OK 912 | ``` 913 | -------------------------------------------------------------------------------- /docs/07_the_concurrency_api.md: -------------------------------------------------------------------------------- 1 | ## 35 用 [std::async](https://en.cppreference.com/w/cpp/thread/async) 替代 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 2 | 3 | * 异步运行函数的一种选择是,创建一个 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 来运行 4 | 5 | ```cpp 6 | int f(); 7 | std::thread t{f}; 8 | ``` 9 | 10 | * 另一种方法是使用 [std::async](https://en.cppreference.com/w/cpp/thread/async),它返回一个持有计算结果的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 11 | 12 | ```cpp 13 | int f(); 14 | std::future ft = std::async(f); 15 | ``` 16 | 17 | * 如果函数有返回值,[std::thread](https://en.cppreference.com/w/cpp/thread/thread) 无法直接获取该值,而 [std::async](https://en.cppreference.com/w/cpp/thread/async) 返回的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 提供了 [get](https://en.cppreference.com/w/cpp/thread/future/get) 来获取该值。如果函数抛出异常,[get](https://en.cppreference.com/w/cpp/thread/future/get) 能访问异常,而 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 会调用 [std::terminate](https://en.cppreference.com/w/cpp/error/terminate) 终止程序 18 | 19 | ```cpp 20 | int f() { return 1; } 21 | auto ft = std::async(f); 22 | int res = ft.get(); 23 | ``` 24 | 25 | * 在并发的 C++ 软件中,线程有三种含义: 26 | * hardware thread 是实际执行计算的线程,计算机体系结构中会为每个 CPU 内核提供一个或多个硬件线程 27 | * software thread(OS thread 或 system thread)是操作系统实现跨进程管理,并执行硬件线程调度的线程 28 | * [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 是 C++ 进程中的对象,用作底层 OS thread 的 handle 29 | * OS thread 是一种有限资源,如果试图创建的线程超出系统所能提供的数量,就会抛出 [std::system_error](https://en.cppreference.com/w/cpp/error/system_error) 异常。这在任何时候都是确定的,即使要运行的函数不能抛异常 30 | 31 | ```cpp 32 | int f() noexcept; 33 | std::thread t{f}; // 若无线程可用,仍会抛出异常 34 | ``` 35 | 36 | * 解决这个问题的一个方法是在当前线程中运行函数,但这会导致负载不均衡,而且如果当前线程是一个 GUI 线程,将导致无法响应。另一个方法是等待已存在的软件线程完成工作后再新建 [std::thread](https://en.cppreference.com/w/cpp/thread/thread),但一种可能的问题是,已存在的软件线程在等待函数执行某个动作 37 | * 即使没有用完线程也可能发生 oversubscription 的问题,即准备运行(非阻塞)的 OS thread 数量超过了 hardware thread,此时线程调度器会为 OS thread 在 hardware thread 上分配 CPU 时间片。当一个线程的时间片用完,另一个线程启动时,就会发生语境切换。这种语境切换会增加系统的线程管理开销,尤其是调度器切换到不同的 CPU core 上的硬件线程时会产生巨大开销。此时,OS thread 通常不会命中 CPU cache(即它们几乎不含有对该软件线程有用的数据和指令),CPU core 运行的新软件线程还会污染 cache 上为旧线程准备的数据,旧线程曾在该 CPU core 上运行过,并很可能再次被调度到此处运行 38 | * 避免 oversubscription 很困难,因为 OS thread 和 hardware thread 的最佳比例取决于软件线程变为可运行状态的频率,而这是会动态变化的,比如一个程序从 I/O 密集型转换计算密集型。软件线程和硬件线程的最佳比例也依赖于语境切换的成本和使用 CPU cache 的命中率,而硬件线程的数量和 CPU cache 的细节(如大小、速度)又依赖于计算机体系结构,因此即使在一个平台上避免了 oversubscription 也不能保证在另一个平台上同样有效 39 | * 使用 [std::async](https://en.cppreference.com/w/cpp/thread/async) 则可以把 oversubscription 的问题丢给库作者解决 40 | 41 | ```cpp 42 | auto ft = std::async(f); // 由标准库的实现者负责线程管理 43 | ``` 44 | 45 | * 这个调用把线程管理的责任转交给了标准库实现。如果申请的软件线程多于系统可提供的,系统不保证会创建一个新的软件线程。相反,它允许调度器把函数运行在对返回的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait) 的线程中 46 | * 即使使用 [std::async](https://en.cppreference.com/w/cpp/thread/async),GUI 线程的响应性也仍然存在问题,因为调度器无法得知哪个线程迫切需要响应。这种情况下,可以将 [std::async](https://en.cppreference.com/w/cpp/thread/async) 的启动策略设定为 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch),这样可以保证函数会在调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait) 的线程中运行 47 | 48 | ```cpp 49 | auto ft = std::async(std::launch::async, f); 50 | ``` 51 | 52 | * [std::async](https://en.cppreference.com/w/cpp/thread/async) 分担了手动管理线程的负担,并提供了检查异步执行函数的结果的方式,但仍有几种不常见的情况需要使用 [std::thread](https://en.cppreference.com/w/cpp/thread/thread): 53 | * 需要访问底层线程 API:并发 API 通常基于系统的底层 API(pthread、Windows 线程库)实现,通过 [native_handle](https://en.cppreference.com/w/cpp/thread/thread/native_handle) 即可获取底层线程 handle 54 | * 需要为应用优化线程用法:比如开发一个服务器软件,运行时的 profile 已知并作为唯一的主进程部署在硬件特性固定的机器上 55 | * 实现标准库未提供的线程技术,比如线程池 56 | 57 | ## 36 用 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch) 指定异步求值 58 | * [std::async](https://en.cppreference.com/w/cpp/thread/async) 有两种标准启动策略: 59 | * [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch):函数必须异步运行,即运行在不同的线程上 60 | * [std::launch::deferred](https://en.cppreference.com/w/cpp/thread/launch):函数只在对返回的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait) 时运行。即执行会推迟,调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait) 时函数会同步运行,调用方会阻塞至函数运行结束 61 | * [std::async](https://en.cppreference.com/w/cpp/thread/async) 的默认启动策略不是二者之一,而是对二者求或的结果 62 | 63 | ```cpp 64 | auto ft1 = std::async(f); // 意义同下 65 | auto ft2 = std::async(std::launch::async | std::launch::deferred, f); 66 | ``` 67 | 68 | * 默认启动策略允许异步或同步运行函数,这种灵活性使得 [std::async](https://en.cppreference.com/w/cpp/thread/async) 和标准库的线程管理组件能负责线程的创建和销毁、负载均衡以及避免 oversubscription 69 | * 但默认启动策略存在一些潜在问题,比如给定线程 t 执行如下语句 70 | 71 | ```cpp 72 | auto ft = std::async(f); 73 | ``` 74 | 75 | * 潜在的问题有: 76 | * 无法预知 f 和 t 是否会并发运行,因为 f 可能被调度为推迟运行 77 | * 无法预知运行 f 的线程是否不同于对 ft 调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait) 的线程,如果调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait) 的线程是 t,就说明无法预知 f 是否会运行在与 t 不同的某线程上 78 | * 甚至很可能无法预知 f 是否会运行,因为无法保证在程序的每条路径上,ft 的 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait) 会被调用 79 | * 默认启动策略在调度上的灵活性会在使用 [thread_local](https://en.cppreference.com/w/cpp/keyword/thread_local) 变量时导致混淆,这意味着如果 f 读写此 thread-local storage(TLS)时,无法预知哪个线程的局部变量将被访问 80 | 81 | ```cpp 82 | /* 83 | * f 的 TLS 可能和一个独立线程相关 84 | * 但也可能与对 ft 调用 get 或 wait 的线程相关 85 | */ 86 | auto ft = std::async(f); 87 | ``` 88 | 89 | * 它也会影响使用 timeout 的 wait-based 循环,因为对返回的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 调用 [wait_for](https://en.cppreference.com/w/cpp/thread/future/wait_for) 或 [wait_until](https://en.cppreference.com/w/cpp/thread/future/wait_until) 会产生 [std::future_status::deferred](https://en.cppreference.com/w/cpp/thread/future_status) 值。这意味着以下循环看似最终会终止,但实际可能永远运行 90 | 91 | ```cpp 92 | using namespace std::literals; 93 | 94 | void f() { std::this_thread::sleep_for(1s); } 95 | 96 | auto ft = std::async(f); 97 | while (ft.wait_for(100ms) != std::future_status::ready) { 98 | // 循环至 f 运行完成,但这可能永远不会发生 99 | } 100 | ``` 101 | 102 | * 如果选用了 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch) 启动策略,f 和调用 [std::async](https://en.cppreference.com/w/cpp/thread/async) 的线程并发执行,则没有问题。但如果 f 被推迟执行,则 [wait_for](https://en.cppreference.com/w/cpp/thread/future/wait_for) 总会返回 [std::future_status::deferred](https://en.cppreference.com/w/cpp/thread/future_status),于是循环永远不会终止 103 | * 这类 bug 在开发和单元测试时很容易被忽略,只有在运行负载很重时才会被发现。解决方法很简单,检查返回的 [std::future](https://en.cppreference.com/w/cpp/thread/future),确定任务是否被推迟。但没有直接检查是否推迟的方法,替代的手法是,先调用一个 timeout-based 函数,比如 [wait_for](https://en.cppreference.com/w/cpp/thread/future/wait_for),这并不表示想等待任何事,而只是为了查看返回值是否为 [std::future_status::deferred](https://en.cppreference.com/w/cpp/thread/future_status) 104 | 105 | ```cpp 106 | auto ft = std::async(f); 107 | if (ft.wait_for(0s) == std::future_status::deferred) { // 任务被推迟 108 | // 使用 ft 的 wait 或 get 异步调用 f 109 | } else { // 任务未被推迟 110 | while (ft.wait_for(100ms) != std::future_status::ready) { 111 | // 任务未被推迟也未就绪,则做并发工作直至结束 112 | } 113 | // ft 准备就绪 114 | } 115 | ``` 116 | 117 | * 综上,[std::async](https://en.cppreference.com/w/cpp/thread/async) 使用默认启动策略创建要满足以下所有条件: 118 | * 任务不需要与对返回值调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait) 的线程并发执行 119 | * 读写哪个线程的 thread_local 变量没有影响 120 | * 要么保证对返回值调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 或 [wait](https://en.cppreference.com/w/cpp/thread/future/wait),要么接受任务可能永远不执行 121 | * 使用 [wait_for](https://en.cppreference.com/w/cpp/thread/future/wait_for) 或 [wait_until](https://en.cppreference.com/w/cpp/thread/future/wait_until) 的代码要考虑任务被推迟的可能 122 | * 只要一点不满足,就可能意味着想确保异步执行任务,这只需要指定启动策略为 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch) 123 | 124 | ```cpp 125 | auto ft = std::async(std::launch::async, f); // 异步执行 f 126 | ``` 127 | 128 | * 默认使用 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch) 启动策略的 [std::async](https://en.cppreference.com/w/cpp/thread/async) 将会是一个很方便的工具,实现如下 129 | 130 | ```cpp 131 | template 132 | auto really_async(F&& f, Args&&... args) 133 | -> std::future> { 134 | return std::async(std::launch::async, std::forward(f), 135 | std::forward(args)...); 136 | } 137 | ``` 138 | 139 | * 这个函数的用法和 [std::async](https://en.cppreference.com/w/cpp/thread/async) 一样 140 | 141 | ```cpp 142 | /* 143 | * 异步运行 f 144 | * 如果 std::async 抛出异常则 really_async 也抛出异常 145 | */ 146 | auto ft = really_async(f); 147 | ``` 148 | 149 | ## 37 RAII 线程管理 150 | 151 | * 每个 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 对象都处于可合并(joinable)或不可合并(unjoinable)的状态。一个可合并的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 对应于一个底层异步运行的线程,若底层线程处于阻塞、等待调度或已运行结束的状态,则此 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 可合并,否则不可合并。不可合并的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 包括: 152 | * 默认构造的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread):此时没有要运行的函数,因此没有对应的底层运行线程 153 | * 已移动的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread):移动操作导致底层线程被转用于另一个 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 154 | * 已 [join](https://en.cppreference.com/w/cpp/thread/thread/join) 或已 [join](https://en.cppreference.com/w/cpp/thread/thread/join) 的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 155 | * 如果可合并的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 对象的析构函数被调用,则程序的执行将终止 156 | 157 | ```cpp 158 | void f() {} 159 | 160 | void g() { 161 | std::thread t{f}; // t.joinable() == true 162 | } 163 | 164 | int main() { 165 | g(); // g 运行结束时析构 t,但 t 未 join,导致程序终止 166 | do_something(); // 调用前程序已被终止 167 | } 168 | ``` 169 | 170 | * 析构可合并的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 时,隐式 [join](https://en.cppreference.com/w/cpp/thread/thread/join) 或隐式 [detach](https://en.cppreference.com/w/cpp/thread/thread/detach) 的带来问题更大。隐式 [join](https://en.cppreference.com/w/cpp/thread/thread/join) 导致 g 运行结束时仍要保持等待 f 运行结束,这就会导致性能问题,且调试时难以追踪原因。隐式 [detach](https://en.cppreference.com/w/cpp/thread/thread/detach) 导致的调试问题更为致命 171 | 172 | ```cpp 173 | void f(int&) {} 174 | 175 | void g() { 176 | int i = 1; 177 | std::thread t(f, i); 178 | } // 如果隐式 detach,局部变量 i 被销毁,但 f 仍在使用局部变量的引用 179 | ``` 180 | 181 | * 完美销毁一个可合并的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 十分困难,因此规定销毁将导致终止程序。要避免程序终止,只要让可合并的线程在销毁时变为不可合并状态即可,C++20 提供了自动回收线程的 [std::jthread](https://en.cppreference.com/w/cpp/thread/jthread),C++20 之前使用 RAII 手法手动实现即可 182 | 183 | ```cpp 184 | #include 185 | #include 186 | 187 | class ThreadGuard { 188 | public: 189 | enum class DtorAction { kJoin, kDetach }; 190 | ThreadGuard(std::thread&& t, DtorAction a) : action_(a), t_(std::move(t)) {} 191 | ~ThreadGuard() { 192 | if (t_.joinable()) { 193 | if (action_ == DtorAction::kJoin) { 194 | t_.join(); 195 | } else { 196 | t_.detach(); 197 | } 198 | } 199 | } 200 | ThreadGuard(ThreadGuard&&) noexcept = default; 201 | ThreadGuard& operator=(ThreadGuard&&) = default; 202 | std::thread& get() { return t_; } 203 | 204 | private: 205 | DtorAction action_; 206 | std::thread t_; 207 | }; 208 | 209 | void f() { 210 | ThreadGuard t{std::thread([] {}), ThreadGuard::DtorAction::kJoin}; 211 | } 212 | 213 | int main() { 214 | f(); 215 | /* 216 | * g 运行结束时将内部的 std::thread 置为 join,变为不可合并状态 217 | * 析构不可合并的 std::thread 不会导致程序终止 218 | * 这种手法带来了隐式 join 和隐式 detach 的问题,但可以调试 219 | */ 220 | } 221 | ``` 222 | 223 | ## 38 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的析构行为 224 | 225 | * 可合并的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 对应一个底层系统线程,采用 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch) 启动策略的 [std::async](https://en.cppreference.com/w/cpp/thread/async) 返回的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 和系统线程也有类似的关系,因此可以认为 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 和 [std::future](https://en.cppreference.com/w/cpp/thread/future) 相当于系统线程的 handle 226 | * 销毁 [std::future](https://en.cppreference.com/w/cpp/thread/future) 有时表现为隐式 join,有时表现为隐式 detach,有时表现为既不隐式 join 也不隐式 detach,但它不会导致程序终止。这种不同表现行为是值得需要思考的。想象 [std::future](https://en.cppreference.com/w/cpp/thread/future) 处于信道的一端,callee 将 [std::promise](https://en.cppreference.com/w/cpp/thread/promise) 对象传给 caller,caller 用一个 [std::future](https://en.cppreference.com/w/cpp/thread/future) 来读取结果 227 | 228 | ```cpp 229 | std::promise ps; 230 | std::future ft = ps.get_future(); 231 | ``` 232 | 233 | * callee 的结果存储在哪? 234 | 235 | ``` 236 | future std::promise 237 | Caller <-------------------------------- Callee 238 | (typically) 239 | ``` 240 | 241 | * caller 调用 [get](https://en.cppreference.com/w/cpp/thread/future/get) 之前,callee 可能已经执行完毕,因此结果不可能存储在 callee 的 [std::promise](https://en.cppreference.com/w/cpp/thread/promise) 对象中。但结果也不可能存储在 caller 的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 中,因为 [std::future](https://en.cppreference.com/w/cpp/thread/future) 可以用来创建 [std::shared_future](https://en.cppreference.com/w/cpp/thread/shared_future) 242 | 243 | ```cpp 244 | std::shared_future sf{std::move(ft)}; 245 | // 更简洁的写法是用 std::future::share() 返回 std::shared_future 246 | // auto sf = ft.share(); 247 | ``` 248 | 249 | * 而 [std::shared_future](https://en.cppreference.com/w/cpp/thread/shared_future) 在原始的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 析构后仍然可以复制 250 | 251 | ```cpp 252 | auto sf2 = sf; 253 | auto sf3 = sf; 254 | ``` 255 | 256 | * 因此结果只能存储在外部某个位置,这个位置称为 shared state 257 | 258 | ``` 259 | future shared state std::promise 260 | Caller <---------------- Callee's Result <---------------- Callee 261 | (typically) 262 | ``` 263 | 264 | * shared state 通常用堆上的对象表示,但类型、接口和具体实现由标准库作者决定。shared state 决定了 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的析构函数行为: 265 | * 采用 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch) 启动策略的 [std::async](https://en.cppreference.com/w/cpp/thread/async) 返回的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 中,最后一个引用 shared state 的,析构函数会保持阻塞至任务执行完成。本质上,这样一个 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的析构函数是对异步运行的底层线程执行了一次隐式 join 266 | * 其他所有 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的析构函数只是简单地析构对象。对底层异步运行的任务,这相当于对线程执行了一次隐式 detach。对于被推迟的任务来说,如果这是最后一个 [std::future](https://en.cppreference.com/w/cpp/thread/future),就意味着被推迟的任务将不会再运行 267 | * 这些规则看似复杂,但本质就是一个正常行为和一个特殊行为。正常行为是析构函数会销毁 [std::future](https://en.cppreference.com/w/cpp/thread/future) 对象,它不 join 或 detach 任何东西,也没有运行任何东西,它只是销毁 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的成员变量。不过实际上它确实多做了一件事,就是减少了一次 shared state 中的引用计数,shared state 由 caller 的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 和 callee 的 [std::promise](https://en.cppreference.com/w/cpp/thread/promise) 共同操控。引用计数让库得知何时能销毁 shared state 268 | * [std::future](https://en.cppreference.com/w/cpp/thread/future) 的析构函数只在满足以下所有条件时发生特殊行为(阻塞至异步运行的任务结束): 269 | * [std::future](https://en.cppreference.com/w/cpp/thread/future) 引用的 shared state 由调用 [std::async](https://en.cppreference.com/w/cpp/thread/async) 创建 270 | * 任务的启动策略是 [std::launch::async](https://en.cppreference.com/w/cpp/thread/launch),这可以是运行时系统选择的或显式指定的 271 | * 这个 [std::future](https://en.cppreference.com/w/cpp/thread/future) 是最后一个引用 shared state 的。对于 [std::shared_future](https://en.cppreference.com/w/cpp/thread/shared_future),如果其他 [std::shared_future](https://en.cppreference.com/w/cpp/thread/shared_future) 和要被销毁的 [std::shared_future](https://en.cppreference.com/w/cpp/thread/shared_future) 引用同一个 shared state,则被销毁的 [std::shared_future](https://en.cppreference.com/w/cpp/thread/shared_future) 遵循正常行为(即简单地销毁数据成员) 272 | * 阻塞至异步运行的任务结束的特殊行为,在效果上相当于对运行着 [std::async](https://en.cppreference.com/w/cpp/thread/async) 创建的任务的线程执行了一次隐式 join。特别制定这个规则的原因是,标准委员会想避免隐式 detach 相关的问题,但又不想对可合并的线程一样直接让程序终止,于是妥协的结果就是执行一次隐式 join 273 | * [std::future](https://en.cppreference.com/w/cpp/thread/future) 没有提供 API 来判断 shared state 是否产生于 [std::async](https://en.cppreference.com/w/cpp/thread/async) 的调用,即无法得知析构时是否会阻塞至异步任务执行结束,因此含有 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的类型都可能在析构函数中阻塞 274 | 275 | ```cpp 276 | std::vector> v; // 该容器可能在析构函数中阻塞 277 | 278 | struct A { // 该类型对象可能会在析构函数中阻塞 279 | std::shared_future ft; 280 | }; 281 | ``` 282 | 283 | * 只有在 [std::async](https://en.cppreference.com/w/cpp/thread/async) 调用时出现的 shared state 才可能出现特殊行为,但还有其他创建 shared state,也就是说其他创建方式生成的 [std::future](https://en.cppreference.com/w/cpp/thread/future) 将可以正常析构 284 | 285 | ```cpp 286 | int f() { return 1; } 287 | std::packaged_task pt(f); 288 | auto ft = pt.get_future(); // ft 可以正常析构 289 | std::thread t(std::move(pt)); // 创建一个线程来执行任务 290 | int res = ft.get(); 291 | ``` 292 | 293 | * 析构行为正常的原因很简单 294 | 295 | ```cpp 296 | { 297 | std::packaged_task pt(f); 298 | auto ft = pt.get_future(); // ft 可以正常析构 299 | std::thread t(std::move(pt)); 300 | ... // t.join() 或 t.detach() 或无操作 301 | } // 如果t 不 join 不 detach,则此处 t 的析构程序终止 302 | // 如果 t 已 join,则 ft 析构时就无需阻塞 303 | // 如果 t 已 detach,则 ft 析构时就无需 detach 304 | // 因此 std::packaged_task 生成的 ft 一定可以正常析构 305 | ``` 306 | 307 | ## 39 用 [std::promise](https://en.cppreference.com/w/cpp/thread/promise) 和 [std::future](https://en.cppreference.com/w/cpp/thread/future) 之间的通信实现一次性通知 308 | 309 | * 让一个任务通知另一个异步任务发生了特定事件,一种实现方法是使用条件变量 310 | 311 | ```cpp 312 | #include 313 | #include 314 | #include 315 | #include 316 | #include 317 | 318 | std::condition_variable cv; 319 | std::mutex m; 320 | bool flag = false; 321 | std::string s = "hello"; 322 | 323 | void f() { 324 | std::unique_lock lk{m}; 325 | cv.wait(lk, [] { 326 | return flag; 327 | }); // lambda 返回 false 则阻塞,并在收到通知后重新检测 328 | std::cout << s; // 若返回 true 则继续执行 329 | } 330 | 331 | int main() { 332 | std::thread t{f}; 333 | { 334 | std::lock_guard l{m}; 335 | s += " world"; 336 | flag = true; 337 | cv.notify_one(); // 发出通知 338 | } 339 | t.join(); 340 | } 341 | ``` 342 | 343 | * 另一种方法是用 [std::promise::set_value](https://en.cppreference.com/w/cpp/thread/promise/set_value) 通知 [std::future::wait](https://en.cppreference.com/w/cpp/thread/future/wait) 344 | 345 | ```cpp 346 | #include 347 | #include 348 | #include 349 | 350 | std::promise p; 351 | 352 | void f() { 353 | p.get_future().wait(); // 阻塞至 p.set_value() 354 | std::cout << 1; 355 | } 356 | 357 | int main() { 358 | std::thread t{f}; 359 | p.set_value(); // 解除阻塞 360 | t.join(); 361 | } 362 | ``` 363 | 364 | * 这种方法非常简单,但也有缺点,[std::promise](https://en.cppreference.com/w/cpp/thread/promise) 和 [std::future](https://en.cppreference.com/w/cpp/thread/future) 之间的 shared state 是动态分配的,存在堆上的分配和回收成本。更重要的是,[std::promise](https://en.cppreference.com/w/cpp/thread/promise) 只能设置一次,因此它和 [std::future](https://en.cppreference.com/w/cpp/thread/future) 的之间的通信只能使用一次,而条件变量可以重复通知。因此这种方法一般用来创建暂停状态的 [std::thread](https://en.cppreference.com/w/cpp/thread/thread) 365 | 366 | ```cpp 367 | #include 368 | #include 369 | #include 370 | 371 | std::promise p; 372 | 373 | void f() { std::cout << 1; } 374 | 375 | int main() { 376 | std::thread t([] { 377 | p.get_future().wait(); 378 | f(); 379 | }); 380 | p.set_value(); 381 | t.join(); 382 | } 383 | ``` 384 | 385 | * 此时可能会想到使用 [std::jthread](https://en.cppreference.com/w/cpp/thread/jthread),但这并不安全 386 | 387 | ```cpp 388 | #include 389 | #include 390 | #include 391 | 392 | std::promise p; 393 | 394 | void f() { std::cout << 1; } 395 | 396 | int main() { 397 | std::jthread t{[&] { 398 | p.get_future().wait(); 399 | f(); 400 | }}; 401 | /* 402 | * 如果此处抛异常,则 set_value 不会被调用,wait 将永远不返回 403 | * 而 RAII 会在析构时调用 join,join 将一直等待线程完成 404 | * 但 wait 使线程永不完成 405 | * 因此如果此处抛出异常,析构函数永远不会完成,程序将失去效应 406 | */ 407 | p.set_value(); 408 | } 409 | ``` 410 | 411 | * [std::condition_variable::notify_all](https://en.cppreference.com/w/cpp/thread/condition_variable/notify_all) 可以一次通知多个任务,这也可以通过 [std::promise](https://en.cppreference.com/w/cpp/thread/promise) 和 [std::shared_future](https://en.cppreference.com/w/cpp/thread/shared_future) 之间的通信实现 412 | 413 | ```cpp 414 | #include 415 | #include 416 | #include 417 | #include 418 | 419 | std::promise p; 420 | 421 | void f(int x) { std::cout << x; } 422 | 423 | int main() { 424 | std::vector v; 425 | auto sf = p.get_future().share(); 426 | for (int i = 0; i < 10; ++i) { 427 | v.emplace_back([sf, i] { 428 | sf.wait(); 429 | f(i); 430 | }); 431 | } 432 | p.set_value(); 433 | for (auto& x : v) { 434 | x.join(); 435 | } 436 | } 437 | ``` 438 | 439 | ## 40 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 提供原子操作,volatile 禁止优化内存 440 | 441 | * Java 中的 volatile 变量提供了同步机制,C++ 的 volatile 变量和并发没有任何关系 442 | * [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 是原子类型,提供了原子操作 443 | 444 | ```cpp 445 | #include 446 | #include 447 | #include 448 | 449 | std::atomic i{0}; 450 | 451 | void f() { 452 | ++i; // 原子自增 453 | ++i; // 原子自增 454 | } 455 | 456 | void g() { std::cout << i; } 457 | 458 | int main() { 459 | std::thread t1{f}; 460 | std::thread t2{g}; // 结果只能是 0 或 1 或 2 461 | t1.join(); 462 | t2.join(); 463 | } 464 | ``` 465 | 466 | * volatile 变量是普通的非原子类型,则不保证原子操作 467 | 468 | ```cpp 469 | #include 470 | #include 471 | #include 472 | 473 | volatile int i{0}; 474 | 475 | void f() { 476 | ++i; // 读改写操作,非原子操作 477 | ++i; // 读改写操作,非原子操作 478 | } 479 | 480 | void g() { std::cout << i; } 481 | 482 | int main() { 483 | std::thread t1{f}; 484 | std::thread t2{g}; // 存在数据竞争,值未定义 485 | t1.join(); 486 | t2.join(); 487 | } 488 | ``` 489 | 490 | * 编译器或底层硬件对于不相关的赋值会重新排序以提高代码运行速度,[std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 可以限制重排序以保证顺序一致性 491 | 492 | ```cpp 493 | #include 494 | #include 495 | #include 496 | 497 | std::atomic a{false}; 498 | int x = 0; 499 | 500 | void f() { 501 | x = 1; // 一定在 a 赋值为 true 之前执行 502 | a = true; 503 | } 504 | 505 | void g() { 506 | if (a) { 507 | std::cout << x; 508 | } 509 | } 510 | 511 | int main() { 512 | std::thread t1{f}; 513 | std::thread t2{g}; 514 | t1.join(); 515 | t2.join(); // 不打印,或打印 1 516 | } 517 | ``` 518 | 519 | * volatile 不会限制代码的重新排序 520 | 521 | ```cpp 522 | #include 523 | #include 524 | #include 525 | 526 | volatile bool a{false}; 527 | int x = 0; 528 | 529 | void f() { 530 | x = 1; // 可能被重排在 a 赋值为 true 之后 531 | a = true; 532 | } 533 | 534 | void g() { 535 | if (a) { 536 | std::cout << x; 537 | } 538 | } 539 | 540 | int main() { 541 | std::thread t1{f}; 542 | std::thread t2{g}; 543 | t1.join(); 544 | t2.join(); // 不打印,或打印 0 或 1 545 | } 546 | ``` 547 | 548 | * volatile 的用处是告诉编译器正在处理的是特殊内存,不要对此内存上的操作进行优化。所谓优化指的是,如果把一个值写到内存某个位置,值会保留在那里,直到被覆盖,因此冗余的赋值就能被消除 549 | 550 | ```cpp 551 | int x = 42; 552 | int y = x; 553 | y = x; // 冗余的初始化 554 | 555 | // 优化为 556 | int x = 42; 557 | int y = x; 558 | ``` 559 | 560 | * 如果把一个值写到内存某个位置但从不读取,然后再次写入该位置,则第一次的写入可被消除 561 | 562 | ```cpp 563 | int x; 564 | x = 10; 565 | x = 20; 566 | 567 | // 优化为 568 | int x; 569 | x = 20; 570 | ``` 571 | 572 | * 结合上述两者 573 | 574 | ```cpp 575 | int x = 42; 576 | int y = x; 577 | y = x; 578 | x = 10; 579 | x = 20; 580 | 581 | // 优化为 582 | int x = 42; 583 | int y = x; 584 | x = 20; 585 | ``` 586 | 587 | * 原子类型的读写也是可优化的 588 | 589 | ```cpp 590 | std::atomic y{x.load()}; 591 | y.store(x.load()); 592 | 593 | // 优化为 594 | register = x.load(); // 将 x 读入寄存器 595 | std::atomic y{register}; // 用寄存器值初始化 y 596 | y.store(register); // 将寄存器值存入 y 597 | ``` 598 | 599 | * 这种冗余的代码不会直接被写出来,但往往会隐含在大量代码之中。这种优化只在常规内存中合法,特殊内存则不适用。一般主存就是常规内存,特殊内存一般用于 memory-mapped I/O ,即与外部设备(如外部传感器、显示器、打印机、网络端口)通信。这个需求的原因在于,看似冗余的操作可能是有实际作用的 600 | 601 | ```cpp 602 | int current_temperature; // 传感器中记录当前温度的变量 603 | current_temperature = 25; // 更新当前温度,这条语句不应该被消除 604 | current_temperature = 26; 605 | ``` 606 | -------------------------------------------------------------------------------- /docs/03_moving_to_modern_cpp.md: -------------------------------------------------------------------------------- 1 | ## 07 创建对象时注意区分 () 和 {} 2 | 3 | * 值初始化有如下方式 4 | 5 | ```cpp 6 | int a(0); 7 | int b = 0; 8 | int c{0}; 9 | int d = {0}; // 按 int d{0} 处理,后续讨论将忽略这种用法 10 | ``` 11 | 12 | * 使用等号不一定是赋值,也可能是拷贝,对于内置类型来说,初始化和赋值的区别只是学术争议,但对于类类型则不同 13 | 14 | ```cpp 15 | A a; // 默认构造 16 | A b = a; // 拷贝而非赋值 17 | a = b; // 拷贝而非赋值 18 | ``` 19 | 20 | * C++11 引入了统一初始化(uniform initialization),也可以叫大括号初始化(braced initialization)。大括号初始化可以方便地为容器指定初始元素 21 | 22 | ```cpp 23 | std::vector v{1, 2, 3}; 24 | ``` 25 | 26 | * 可以用大括号初始化或 = 为 non-static 数据成员指定默认值,但不能用小括号初始化指定 27 | 28 | ```cpp 29 | struct A { 30 | int x{0}; // OK 31 | int y = 0; // OK 32 | int z(0); // 错误 33 | }; 34 | ``` 35 | 36 | * 大括号初始化禁止内置类型的隐式收缩转换(implicit narrowing conversions),而小括号初始化和 = 不会 37 | 38 | ```cpp 39 | double x = 1.1; 40 | double y = 2.2; 41 | int a{x + y}; // 错误:大括号初始化不允许 double 到 int 的收缩转换 42 | int b(x + y); // OK:double 被截断为 int 43 | int c = x + y; // OK:double 被截断为 int 44 | ``` 45 | 46 | * 大括号初始化不存在 C++ 的最令人苦恼的解析(C++'s most vexing parse)问题 47 | 48 | ```cpp 49 | struct A { 50 | A() { std::cout << 1; } 51 | }; 52 | 53 | struct B { 54 | B(std::string) { std::cout << 2; } 55 | }; 56 | 57 | A a(); // 不调用 A 的构造函数,而是被解析成一个函数声明:A a(); 58 | std::string s{"hi"}; 59 | B b(std::string(s)); // 不构造 B,被解析成函数声明 B b(std::string) 60 | A a2{}; // 构造 A 61 | B b2{std::string(s)}; // 构造 B 62 | 63 | // C++11 之前的解决办法 64 | A a3; 65 | B b3((std::string(s))); 66 | ``` 67 | 68 | * 大括号初始化的缺陷在于,只要类型转换后可以匹配,大括号初始化总会优先匹配参数类型为 [std::initializer_list](https://en.cppreference.com/w/cpp/utility/initializer_list) 的构造函数,即使收缩转换会导致调用错误 69 | 70 | ```cpp 71 | #include 72 | #include 73 | 74 | struct A { 75 | A(int) { std::cout << 1; } 76 | A(std::string) { std::cout << 2; } 77 | A(std::initializer_list) { std::cout << 3; } 78 | }; 79 | 80 | int main() { 81 | A a{0}; // 3 82 | // A b{3.14}; // 错误:大括号初始化不允许 double 到 int 的收缩转换 83 | A c{"hi"}; // 2 84 | } 85 | ``` 86 | 87 | * 但特殊的是,参数为空的大括号初始化只会调用默认构造函数。如果想传入真正的空 [std::initializer_list](https://en.cppreference.com/w/cpp/utility/initializer_list) 作为参数,则要额外添加一层大括号或小括号 88 | 89 | ```cpp 90 | #include 91 | 92 | struct A { 93 | A() { std::cout << 1; } 94 | A(std::initializer_list) { std::cout << 2; } 95 | }; 96 | 97 | int main() { 98 | A a{}; // 1 99 | A b{{}}; // 2 100 | A c({}); // 2 101 | } 102 | ``` 103 | 104 | * 上述问题带来的实际影响很大,比如 [std::vector](https://en.cppreference.com/w/cpp/container/vector) 就存在参数为参数 [std::initializer_list](https://en.cppreference.com/w/cpp/utility/initializer_list) 的构造函数,这导致了参数相同时,大括号初始化和小括号初始化调用的却是不同版本的构造函数 105 | 106 | ```cpp 107 | std::vector v1(3, 6); // 元素为 6、6、6 108 | std::vector v2{3, 6}; // 元素为 3、6 109 | ``` 110 | 111 | * 这是一种失败的设计,并给模板作者带来了对大括号初始化和小括号初始化的选择困惑 112 | 113 | ```cpp 114 | template 115 | decltype(auto) f(Ts&&... args) { 116 | T x(std::forward(args)...); // 用小括号初始化创建临时对象 117 | return x; 118 | } 119 | 120 | template 121 | decltype(auto) g(Ts&&... args) { 122 | T x{std::forward(args)...}; // 用大括号初始化创建临时对象 123 | return x; 124 | } 125 | 126 | // 模板作者不知道调用者希望得到哪个结果 127 | auto v1 = f>(3, 6); // v1 元素为 6、6、6 128 | auto v2 = g>(3, 6); // v2 元素为 3、6 129 | ``` 130 | 131 | * [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 和 [std::make_unique](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique) 就面临了这个问题,而它们的选择是使用小括号初始化并在接口文档中写明这点 132 | 133 | ```cpp 134 | #include 135 | #include 136 | 137 | int main() { 138 | auto p = std::make_shared>(3, 6); 139 | for (auto x : *p) { 140 | std::cout << x; // 666 141 | } 142 | } 143 | ``` 144 | 145 | ## 08 用 [nullptr](https://en.cppreference.com/w/cpp/language/nullptr) 替代 0 和 [NULL](https://en.cppreference.com/w/cpp/types/NULL) 146 | 147 | * 字面值 0 本质是 int 而非指针,只有在使用指针的语境中发现 0 才会解释为空指针 148 | * [NULL](https://en.cppreference.com/w/cpp/types/NULL) 的本质是宏,没有规定的实现标准,一般在 C++ 中定义为 0,在 C 中定义为 `void*` 149 | 150 | ```cpp 151 | #ifndef NULL 152 | #ifdef __cplusplus 153 | #define NULL 0 154 | #else 155 | #define NULL ((void *)0) 156 | #endif 157 | #endif 158 | ``` 159 | 160 | * 在重载解析时,[NULL](https://en.cppreference.com/w/cpp/types/NULL) 作为参数不会优先匹配指针类型。而 [nullptr](https://en.cppreference.com/w/cpp/language/nullptr) 的类型是 [std::nullptr_t](https://en.cppreference.com/w/cpp/types/nullptr_t),[std::nullptr_t](https://en.cppreference.com/w/cpp/types/nullptr_t) 可以转换为任何原始指针类型 161 | 162 | ```cpp 163 | #include 164 | 165 | namespace jc { 166 | 167 | constexpr int f(bool) { return 1; } 168 | constexpr int f(int) { return 2; } 169 | constexpr int f(void*) { return 3; } 170 | 171 | static_assert(f(0) == 2); 172 | static_assert(f(NULL) == 2); 173 | static_assert(f(nullptr) == 3); 174 | 175 | } // namespace jc 176 | 177 | int main() {} 178 | ``` 179 | 180 | * 这点也会影响模板实参推断 181 | 182 | ```cpp 183 | template 184 | void f() {} 185 | 186 | f(0); // T 推断为 int 187 | f(NULL); // T 推断为 int 188 | f(nullptr); // T 推断为 std::nullptr_t 189 | ``` 190 | 191 | * 使用 [nullptr](https://en.cppreference.com/w/cpp/language/nullptr) 就可以避免推断出非指针类型 192 | 193 | ```cpp 194 | void f1(std::shared_ptr) {} 195 | void f2(std::unique_ptr) {} 196 | void f3(int*) {} 197 | 198 | template 199 | void g(F f, T x) { 200 | f(x); 201 | } 202 | 203 | g(f1, 0); // 错误 204 | g(f1, NULL); // 错误 205 | g(f1, nullptr); // OK 206 | 207 | g(f2, 0); // 错误 208 | g(f2, NULL); // 错误 209 | g(f2, nullptr); // OK 210 | 211 | g(f3, 0); // 错误 212 | g(f3, NULL); // 错误 213 | g(f3, nullptr); // OK 214 | ``` 215 | 216 | ## 09 用 [using 别名声明](https://en.cppreference.com/w/cpp/language/type_alias)替代 [typedef](https://en.cppreference.com/w/cpp/language/typedef) 217 | 218 | * [using 别名声明](https://en.cppreference.com/w/cpp/language/type_alias)比 [typedef](https://en.cppreference.com/w/cpp/language/typedef) 可读性更好,尤其是对于函数指针类型 219 | 220 | ```cpp 221 | using F = void (*)(int); // typedef void (*F)(int) 222 | ``` 223 | 224 | * C++11 还引入了[别名模板](https://en.cppreference.com/w/cpp/language/type_alias),它只能使用 [using 别名声明](https://en.cppreference.com/w/cpp/language/type_alias) 225 | 226 | ```cpp 227 | template 228 | using Vector = std::vector; // Vector 等价于 std::vector 229 | 230 | // C++11 之前的做法是在模板内部 typedef 231 | template 232 | struct V { // V::type 等价于 std::vector 233 | typedef std::vector type; 234 | }; 235 | 236 | // 在其他类模板中使用这两个别名的方式 237 | template 238 | struct A { 239 | Vector a; 240 | typename V::type b; 241 | }; 242 | ``` 243 | 244 | * C++11 引入了 [type traits](https://en.cppreference.com/w/cpp/header/type_traits),为了方便使用,C++14 为每个 [type traits](https://en.cppreference.com/w/cpp/header/type_traits) 都定义了[别名模板](https://en.cppreference.com/w/cpp/language/type_alias) 245 | 246 | ```cpp 247 | template 248 | struct remove_reference { 249 | using type = T; 250 | }; 251 | 252 | template 253 | struct remove_reference { 254 | using type = T; 255 | }; 256 | 257 | template 258 | struct remove_reference { 259 | using type = T; 260 | }; 261 | 262 | template 263 | using remove_reference_t = typename remove_reference::type; 264 | ``` 265 | 266 | * 为了简化生成值的 [type traits](https://en.cppreference.com/w/cpp/header/type_traits),C++14 还引入了[变量模板](https://en.cppreference.com/w/cpp/language/variable_template) 267 | 268 | ```cpp 269 | template 270 | struct is_same { 271 | static constexpr bool value = false; 272 | }; 273 | 274 | template 275 | constexpr bool is_same_v = is_same::value; 276 | ``` 277 | 278 | ## 10 用 [enum class](https://en.cppreference.com/w/cpp/language/enum#Scoped_enumerations) 替代 [enum](https://en.cppreference.com/w/cpp/language/enum#Unscoped_enumeration) 279 | 280 | * 一般在大括号中声明的名称,只在大括号的作用域内可见,但这对 enum 成员例外。enum 成员属于 enum 所在的作用域,因此作用域内不能出现同名实例 281 | 282 | ```cpp 283 | enum X { a, b, c }; 284 | int a = 1; // 错误:a 已在作用域内声明过 285 | ``` 286 | 287 | * C++11 引入了限定作用域的枚举类型,用 enum class 关键字表示 288 | 289 | ```cpp 290 | enum class X { a, b, c }; 291 | int a = 1; // OK 292 | X x = X::a; // OK 293 | X y = b; // 错误 294 | ``` 295 | 296 | * enum class 的另一个优势是不会进行隐式转换 297 | 298 | ```cpp 299 | enum X { a, b, c }; 300 | X x = a; 301 | if (x < 3.14) { // 不应该将枚举与浮点数进行比较,但这里合法 302 | } 303 | 304 | enum class Y { a, b, c }; 305 | Y y = Y::a; 306 | if (x < 3.14) { // 错误:不允许比较 307 | } 308 | if (static_cast(x) < 3.14) { // OK:enum class 允许强制转换为其他类型 309 | } 310 | ``` 311 | 312 | * C++11 之前的 enum 不允许前置声明,而 C++11 的 enum 和 enum class 都可以前置声明 313 | 314 | ```cpp 315 | enum Color; // C++11 之前错误 316 | enum class X; // OK 317 | ``` 318 | 319 | * C++11 之前不能前置声明 enum 的原因是,编译器为了节省内存,要在 enum 被使用前选择一个足够容纳成员取值的最小整型作为底层类型 320 | 321 | ```cpp 322 | enum X { a, b, c }; // 编译器选择底层类型为 char 323 | enum Status { // 编译器选择比 char 更大的底层类型 324 | good = 0, 325 | failed = 1, 326 | incomplete = 100, 327 | corrupt = 200, 328 | indeterminate = 0xFFFFFFFF 329 | }; 330 | ``` 331 | 332 | * 不能前置声明的一个弊端是,由于编译依赖关系,在 enum 中仅仅添加一个成员可能就要重新编译整个系统。如果在头文件中包含前置声明,修改 enum class 的定义时就不需要重新编译整个系统,如果 enum class 的修改不影响函数的行为,则函数的实现也不需要重新编译 333 | * C++11 支持前置声明的原因很简单,底层类型是已知的,用 [std::underlying_type](https://en.cppreference.com/w/cpp/types/underlying_type) 即可获取。也可以指定枚举的底层类型,如果不指定,enum class 默认为 int,enum 则不存在默认类型 334 | 335 | ```cpp 336 | enum class X : std::uint32_t; 337 | // 也可以在定义中指定 338 | enum class Y : std::uint32_t { a, b, c }; 339 | ``` 340 | 341 | * C++11 中使用 enum 更方便的场景只有一种,即希望用到 enum 的隐式转换时 342 | 343 | ```cpp 344 | enum X { name, age, number }; 345 | auto t = std::make_tuple("downdemo", 6, "42"); 346 | auto x = std::get(t); // name 可隐式转换为 get 的模板参数类型 size_t 347 | ``` 348 | 349 | * 如果用 enum class,则需要强制转换 350 | 351 | ```cpp 352 | enum class X { name, age, number }; 353 | auto t = std::make_tuple("downdemo", 6, "13312345678"); 354 | auto x = std::get(X::name)>(t); 355 | ``` 356 | 357 | * 可以用一个函数来封装转换的过程,但也不会简化多少 358 | 359 | ```cpp 360 | template 361 | constexpr auto f(E e) noexcept { 362 | return static_cast>(e); 363 | } 364 | 365 | auto x = std::get(t); 366 | ``` 367 | 368 | ## 11 用 =delete 替代 private 作用域来禁用函数 369 | 370 | * C++11 之前禁用拷贝的方式是将拷贝构造函数和拷贝赋值运算符声明在 private 作用域中 371 | 372 | ```cpp 373 | class A { 374 | private: 375 | A(const A&); // 不需要定义 376 | A& operator(const A&); 377 | }; 378 | ``` 379 | 380 | * C++11 中可以直接将要删除的函数用 =delete 声明,习惯上会声明在 public 作用域中,这样在使用删除的函数时,会先检查访问权再检查删除状态,出错时能得到更明确的诊断信息 381 | 382 | ```cpp 383 | class A { 384 | public: 385 | A(const A&) = delete; 386 | A& operator(const A&) = delete; 387 | }; 388 | ``` 389 | 390 | * private 作用域中的函数还可以被成员和友元调用,而 =delete 是真正禁用了函数,无法通过任何方法调用 391 | * 任何函数都可以用 =delete 声明,比如函数不想接受某种类型的参数,就可以删除对应类型的重载 392 | 393 | ```cpp 394 | void f(int); 395 | void f(double) = delete; // 拒绝 double 和 float 类型参数 396 | 397 | f(3.14); // 错误 398 | ``` 399 | 400 | * =delete 还可以禁止模板对某个类型的实例化 401 | 402 | ```cpp 403 | template 404 | void f(T x) {} 405 | 406 | template <> 407 | void f(int) = delete; 408 | 409 | f(1); // 错误:使用已删除的函数 410 | ``` 411 | 412 | * 类内的函数模板也可以用这种方式禁用 413 | 414 | ```cpp 415 | class A { 416 | public: 417 | template 418 | void f(T x) {} 419 | }; 420 | 421 | template <> 422 | void A::f(int) = delete; 423 | ``` 424 | 425 | * 当然,写在 private 作用域也可以起到禁用的效果 426 | 427 | ```cpp 428 | class A { 429 | public: 430 | template 431 | void f(T x) {} 432 | 433 | private: 434 | template <> 435 | void f(int); 436 | }; 437 | ``` 438 | 439 | * 但把模板和特化置于不同的作用域不太合逻辑,与其效仿 =delete 的效果,不如直接用 =delete 440 | 441 | ## 12 用 [override](https://en.cppreference.com/w/cpp/language/override) 标记被重写的虚函数 442 | 443 | * 虚函数的重写(override)很容易出错,因为要在派生类中重写虚函数,必须满足一系列要求 444 | * 基类中必须有此虚函数 445 | * 基类和派生类的函数名相同(析构函数除外) 446 | * 函数参数类型相同 447 | * const 属性相同 448 | * 函数返回值和异常说明相同 449 | * C++11 多出一条要求:引用修饰符相同。引用修饰符的作用是,指定成员函数仅在对象为左值(成员函数标记为 &)或右值(成员函数标记为 &&)时可用 450 | 451 | ```cpp 452 | namespace jc { 453 | 454 | struct A { 455 | constexpr int f() & { return 1; } // *this 是左值时才使用 456 | constexpr int f() && { return 2; } // *this 是右值时才使用 457 | }; 458 | 459 | constexpr A make_a() { return A{}; } 460 | 461 | } // namespace jc 462 | 463 | int main() { 464 | jc::A a; 465 | static_assert(a.f() == 1); 466 | static_assert(jc::make_a().f() == 2); 467 | } 468 | ``` 469 | 470 | * 对于这么多的要求难以面面俱到,比如下面代码没有任何重写但可以通过编译 471 | 472 | ```cpp 473 | struct A { 474 | public: 475 | virtual void f1() const; 476 | virtual void f2(int x); 477 | virtual void f3() &; 478 | void f4() const; 479 | }; 480 | 481 | struct B : A { 482 | virtual void f1(); 483 | virtual void f2(unsigned int x); 484 | virtual void f3() &&; 485 | void f4() const; 486 | }; 487 | ``` 488 | 489 | * 为了保证正确性,C++11 提供了 [override](https://en.cppreference.com/w/cpp/language/override) 来标记要重写的虚函数,如果未重写就不能通过编译 490 | 491 | ```cpp 492 | struct A { 493 | virtual void f1() const; 494 | virtual void f2(int x); 495 | virtual void f3() &; 496 | virtual void f4() const; 497 | }; 498 | 499 | struct B : A { 500 | virtual void f1() const override; 501 | virtual void f2(int x) override; 502 | virtual void f3() & override; 503 | void f4() const override; 504 | }; 505 | ``` 506 | 507 | * [override](https://en.cppreference.com/w/cpp/language/override) 是一个 contextual keyword,只在特殊语境中保留,[override](https://en.cppreference.com/w/cpp/language/override) 只有出现在成员函数声明末尾才有保留意义,因此如果以前的遗留代码用到了 [override](https://en.cppreference.com/w/cpp/language/override) 作为名字,不用改名就可以升到 C++11 508 | 509 | ```cpp 510 | struct A { 511 | void override(); // 在 C++98 和 C++11 中都合法 512 | }; 513 | ``` 514 | 515 | * C++11 还提供了另一个 contextual keyword,即 [final](https://en.cppreference.com/w/cpp/language/final),它可以用来指定虚函数禁止被重写 516 | 517 | ```cpp 518 | struct A { 519 | virtual void f() final; 520 | void g() final; // 错误:final 只能用于指定虚函数 521 | }; 522 | 523 | struct B : A { 524 | virtual void f() override; // 错误:f 不可重写 525 | }; 526 | ``` 527 | 528 | * [final](https://en.cppreference.com/w/cpp/language/final) 还可以用于指定某个类禁止被继承 529 | 530 | ```cpp 531 | struct A final {}; 532 | struct B : A {}; // 错误:A 禁止被继承 533 | ``` 534 | 535 | ## 13 用 [std::cbegin](https://en.cppreference.com/w/cpp/iterator/begin) 和 [std::cend](https://en.cppreference.com/w/cpp/iterator/end) 获取 const_iterator 536 | 537 | * 需要迭代器但不修改值时就应该使用 const_iterator,获取和使用 const_iterator 十分简单 538 | 539 | ```cpp 540 | std::vector v{2, 3}; 541 | auto it = std::find(std::cbegin(v), std::cend(v), 2); // C++14 542 | v.insert(it, 1); 543 | ``` 544 | 545 | * 上述功能很容易扩展成模板 546 | 547 | ```cpp 548 | template 549 | void f(C& c, const T& x, const T& y) { 550 | auto it = std::find(std::cbegin(c), std::cend(c), x); 551 | c.insert(it, y); 552 | } 553 | ``` 554 | 555 | * C++11 没有 [std::cbegin](https://en.cppreference.com/w/cpp/iterator/begin) 和 [std::cend](https://en.cppreference.com/w/cpp/iterator/end),手动实现即可 556 | 557 | ```cpp 558 | template 559 | auto cbegin(const C& c) -> decltype(std::begin(c)) { 560 | return std::begin(c); // c 是 const 所以返回 const_iterator 561 | } 562 | ``` 563 | 564 | ## 14 用 [noexcept](https://en.cppreference.com/w/cpp/language/noexcept_spec) 标记不抛异常的函数 565 | 566 | * C++98 中,必须指出一个函数可能抛出的所有异常类型,如果函数有所改动则 [exception specification](https://en.cppreference.com/w/cpp/language/except_spec) 也要修改,而这可能破坏代码,因为调用者可能依赖于原本的 [exception specification](https://en.cppreference.com/w/cpp/language/except_spec),所以 C++98 中的 [exception specification](https://en.cppreference.com/w/cpp/language/except_spec) 被认为不值得使用 567 | * C++11 中达成了一个共识,真正需要关心的是函数会不会抛出异常。一个函数要么可能抛出异常,要么绝对不抛异常,这种 maybe-or-never 形成了 C++11 [exception specification](https://en.cppreference.com/w/cpp/language/except_spec) 的基础,C++98 的 [exception specification](https://en.cppreference.com/w/cpp/language/except_spec) 在 C++17 移除 568 | * 函数是否要加上 noexcept 声明与接口设计相关,调用者可以查询函数的 noexcept 状态,查询结果将影响代码的异常安全性和执行效率。因此函数是否要声明为 noexcept 就和成员函数是否要声明为 const 一样重要,如果一个函数不抛异常却不为其声明 noexcept,这就是接口规范缺陷 569 | * noexcept 的一个额外优点是,它可以让编译器生成更好的目标代码。为了理解原因只需要考虑 C++98 和 C++11 表达函数不抛异常的区别 570 | 571 | ```cpp 572 | int f(int x) throw(); // C++98 573 | int f(int x) noexcept; // C++11 574 | ``` 575 | 576 | * 如果一个异常在运行期逃出函数,则 [exception specification](https://en.cppreference.com/w/cpp/language/except_spec) 被违反。在 C++98 中,调用栈会展开到函数调用者,执行一些无关的动作后中止程序。C++11 的一个微小区别是是,在程序中止前只是可能而非一定展开栈。这一点微小的区别将对代码生成造成巨大的影响 577 | * noexcept 声明的函数中,如果异常传出函数,优化器不需要保持栈在运行期的展开状态,也不需要在异常逃出时,保证其中所有的对象按构造顺序的逆序析构。而声明为 throw() 的函数就没有这样的优化灵活性。总结起来就是 578 | 579 | ```cpp 580 | RetType function(params) noexcept; // most optimizable 581 | RetType function(params) throw(); // less optimizable 582 | RetType function(params); // less optimizable 583 | ``` 584 | 585 | * 这个理由已经足够支持给任何已知不会抛异常的函数加上 noexcept,比如移动操作就是典型的不抛异常函数 586 | * [std::vector::push_back](https://en.cppreference.com/w/cpp/container/vector/push_back) 在容器空间不够容纳元素时,会扩展新的内存块,再把元素转移到新的内存块。C++98 的做法是逐个拷贝,然后析构旧内存的对象,这使得 [push_back](https://en.cppreference.com/w/cpp/container/vector/push_back) 提供强异常安全保证:如果拷贝元素的过程中抛出异常,则 [std::vector](https://en.cppreference.com/w/cpp/container/vector) 保持原样,因为旧内存元素还未被析构 587 | * [std::vector::push_back](https://en.cppreference.com/w/cpp/container/vector/push_back) 在 C++11 中的优化是把拷贝替换成移动,但为了不违反强异常安全保证,只有确保元素的移动操作不抛异常时才会用移动替代拷贝 588 | * swap 函数是需要 noexcept 声明的另一个例子,不过标准库的 swap 用 [noexcept 操作符](https://en.cppreference.com/w/cpp/language/noexcept)的结果决定 589 | 590 | ```cpp 591 | /* 592 | * 数组的 swap 由元素类型决定 noexcept 结果 593 | * 比如元素类型是 class A 594 | * 如果 swap(A, A) 不抛异常则该数组的 swap 也不抛异常 595 | */ 596 | template 597 | void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b))); 598 | 599 | // std::pair 的 swap 600 | template 601 | struct pair { 602 | void swap(pair& p) noexcept( 603 | noexcept(swap(first, p.first)) && noexcept(swap(second, p.second))); 604 | }; 605 | ``` 606 | 607 | * 虽然 noexcept 有优化的好处,但将函数声明为 noexcept 的前提是,保证函数长期具有 noexcept 性质,如果之后随意移除 noexcept 声明,就有破坏客户代码的风险 608 | * 大多数函数是异常中立的,它们本身不抛异常,但它们调用的函数可能抛异常,这样它们就允许抛出的异常传到调用栈的更深一层,因此异常中立函数天生永远不具备 noexcept 性质 609 | * 如果为了强行加上 noexcept 而修改实现就是本末倒置,比如调用一个会抛异常的函数是最简单的实现,为了不抛异常而环环相扣地来隐藏这点(比如捕获所有异常,将其替换成状态码或特殊返回值),大大增加了理解和维护的难度,并且这些复杂性的时间成本可能超过 noexcept 带来的优化 610 | * 对某些函数来说,noexcept 性质十分重要,内存释放函数和所有的析构函数都隐式 noexcept,这样就不必加 noexcept 声明。析构函数唯一未隐式 noexcept 的情况是,类中有数据成员的类型显式将析构函数声明 noexcept(false)。但这样的析构函数很少见,标准库中一个也没有 611 | * 有些库的接口设计者会把函数区分为 wide contract 和 narrow contract 612 | * wide contract 函数没有前置条件,不用关心程序状态,对传入的实参没有限制,一定不会有未定义行为,如果知道不会抛异常就可以加上 noexcept 613 | * narrow contract 函数有前置条件,如果条件被违反则结果未定义。但函数没有义务校验这个前置条件,它断言前置条件一定满足(调用者负责保证断言成立),因此加上 noexcept 声明也是合理的 614 | 615 | ```cpp 616 | // 假设前置条件是 s.size() <= 32 617 | void f(const std::string& s) noexcept; 618 | ``` 619 | 620 | * 但如果想在违反前置条件时抛出异常,由于函数的 noexcept 声明,异常就会导致程序中止,因此一般只为 wide contract 函数声明 noexcept 621 | * 在 noexcept 函数中调用可能抛异常的函数时,编译器不会帮忙给出警告。带 noexcept 声明的函数调用了不带 noexcept 声明的函数,这看起来自相矛盾,但也许被调用的函数在文档中写明了不会抛异常,也许它们来自 C 语言的库,也许来自还没来得及根据 C++11 标准做修订的 C++98 库 622 | 623 | ## 15 用 [constexpr](https://en.cppreference.com/w/cpp/language/constexpr) 表示编译期常量 624 | 625 | * constexpr 用于对象时就是一个加强版的 const,表面上看 constexpr 表示值是 const,且在编译期(严格来说是翻译期,包括编译和链接,如果不是编译器或链接器作者,无需关心这点区别)已知,但用于函数则有不同的意义 626 | * 编译期已知的值可能被放进只读内存,这对嵌入式开发是一个很重要的语法特性 627 | * constexpr 函数在调用时若传入的是编译期常量,则产出编译期常量,传入运行期才知道的值,则产出运行期值。constexpr 函数可以满足所有需求,因此不必为了有非编译期值的情况而写两个函数 628 | 629 | ```cpp 630 | #define CPP98 199711L 631 | #define CPP11 201103L 632 | #define CPP14 201402L 633 | #define CPP17 201703L 634 | #define CPP20 202002L 635 | 636 | // #if ((defined(_MSVC_LANG) && _MSVC_LANG > CPP11) || __cplusplus > CPP11) 637 | // #define JC_HAS_CXX14 638 | // #endif 639 | 640 | #ifndef JC_HAS_CXX14 641 | #ifdef _MSVC_LANG 642 | #if _MSVC_LANG > CPP11 643 | #define JC_HAS_CXX14 1 644 | #else 645 | #define JC_HAS_CXX14 0 646 | #endif 647 | #else 648 | #if __cplusplus > CPP11 649 | #define JC_HAS_CXX14 1 650 | #else 651 | #define JC_HAS_CXX14 0 652 | #endif 653 | #endif 654 | #endif // JC_HAS_CXX14 655 | 656 | namespace jc { 657 | 658 | constexpr int pow(int base, int exp) noexcept { 659 | #ifdef JC_HAS_CXX14 660 | auto res = 1; 661 | for (int i = 0; i < exp; ++i) { 662 | res *= base; 663 | } 664 | return res; 665 | #else // C++11 中,constexpr 函数只能包含一条语句 666 | return (exp == 0 ? 1 : base * pow(base, exp - 1)); 667 | #endif 668 | } 669 | 670 | } // namespace jc 671 | 672 | int main() { 673 | constexpr auto n = 4; 674 | static_assert(jc::pow(3, n) == 81); 675 | } 676 | ``` 677 | 678 | * constexpr 并不表示函数要返回 const 值,而是表示,如果参数都是编译期常量,则返回结果就可以当编译期常量使用,如果有一个不是编译期常量,返回值就在运行期计算 679 | 680 | ```cpp 681 | auto base = 3; // 运行期获取值 682 | auto exp = 10; // 运行期获取值 683 | auto baseToExp = pow(base, exp); // pow 在运行期被调用 684 | ``` 685 | 686 | * constexpr 函数必须传入和返回 [literal type](https://en.cppreference.com/w/cpp/named_req/LiteralType)。constexpr 构造函数可以让自定义类型也成为 [literal type](https://en.cppreference.com/w/cpp/named_req/LiteralType) 687 | 688 | ```cpp 689 | namespace jc { 690 | 691 | class Point { 692 | public: 693 | constexpr Point(double x = 0, double y = 0) noexcept : x_(x), y_(y) {} 694 | constexpr double x_value() const noexcept { return x_; } 695 | constexpr double y_value() const noexcept { return y_; } 696 | void set_x(double x) noexcept { 697 | x_ = x; // 修改了对象所以不能声明为 constexpr 698 | } 699 | void set_y(double y) noexcept { // C++11 的 constexpr 函数不能返回 void 700 | y_ = y; 701 | } 702 | 703 | private: 704 | double x_; 705 | double y_; 706 | }; 707 | 708 | constexpr Point midpoint(const Point& lhs, const Point& rhs) noexcept { 709 | return {(lhs.x_value() + rhs.x_value()) / 2, 710 | (lhs.y_value() + rhs.y_value()) / 2}; 711 | } 712 | 713 | } // namespace jc 714 | 715 | int main() { 716 | constexpr jc::Point p1{1.1, 2.2}; // 编译期执行 constexpr 构造函数 717 | constexpr jc::Point p2{3.3, 4.4}; // 同上 718 | constexpr auto mid = jc::midpoint(p1, p2); 719 | static_assert(mid.x_value() == (1.1 + 3.3) / 2); 720 | static_assert(mid.y_value() == (2.2 + 4.4) / 2); 721 | } 722 | ``` 723 | 724 | * C++14 允许对值进行了修改或无返回值的函数声明为 constexpr 725 | 726 | ```cpp 727 | namespace jc { 728 | 729 | class Point { 730 | public: 731 | constexpr Point(double x = 0, double y = 0) noexcept : x_(x), y_(y) {} 732 | constexpr double x_value() const noexcept { return x_; } 733 | constexpr double y_value() const noexcept { return y_; } 734 | constexpr void set_x(double x) noexcept { x_ = x; } 735 | constexpr void set_y(double y) noexcept { y_ = y; } 736 | 737 | private: 738 | double x_; 739 | double y_; 740 | }; 741 | 742 | constexpr Point midpoint(const Point& lhs, const Point& rhs) noexcept { 743 | return {(lhs.x_value() + rhs.x_value()) / 2, 744 | (lhs.y_value() + rhs.y_value()) / 2}; 745 | } 746 | 747 | constexpr Point reflection(const Point& p) noexcept { // p 关于原点的对称点 748 | Point res; 749 | res.set_x(-p.x_value()); 750 | res.set_y(-p.y_value()); 751 | return res; 752 | } 753 | 754 | } // namespace jc 755 | 756 | int main() { 757 | constexpr jc::Point p1{1.1, 2.2}; 758 | constexpr jc::Point p2{3.3, 4.4}; 759 | constexpr auto mid = jc::midpoint(p1, p2); 760 | static_assert(mid.x_value() == (1.1 + 3.3) / 2); 761 | static_assert(mid.y_value() == (2.2 + 4.4) / 2); 762 | constexpr auto reflection_mid = jc::reflection(mid); 763 | static_assert(reflection_mid.x_value() == -mid.x_value()); 764 | static_assert(reflection_mid.y_value() == -mid.y_value()); 765 | } 766 | ``` 767 | 768 | * 使用 constexpr 的前提是必须长期保证需要它,因为如果后续要删除 constexpr 可能会导致许多错误 769 | 770 | ## 16 用 [std::mutex](https://en.cppreference.com/w/cpp/thread/mutex) 或 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 保证 const 成员函数线程安全 771 | 772 | * 假设有一个表示多项式的类,它包含一个返回根的 const 成员函数 773 | 774 | ```cpp 775 | class Polynomial { 776 | public: 777 | std::vector roots() const { 778 | if (!roots_are_valid_) { 779 | // 计算 root_vals_ 780 | roots_are_valid_ = true; 781 | } 782 | return root_vals_; 783 | } 784 | 785 | private: 786 | mutable bool roots_are_valid_{false}; 787 | mutable std::vector root_vals_{}; 788 | }; 789 | ``` 790 | 791 | * 假如此时有两个线程对同一个对象调用成员函数,虽然函数声明为 const,但由于函数内部修改了数据成员,就可能产生数据竞争。最简单的解决方法是引入一个 [std::mutex](https://en.cppreference.com/w/cpp/thread/mutex) 792 | 793 | ```cpp 794 | class Polynomial { 795 | public: 796 | std::vector roots() const { 797 | std::lock_guard lk{m_}; 798 | if (!roots_are_valid_) { 799 | // 计算 root_vals_ 800 | roots_are_valid_ = true; 801 | } 802 | return root_vals_; 803 | } 804 | 805 | private: 806 | mutable std::mutex m_; 807 | mutable bool roots_are_valid_{false}; 808 | mutable std::vector root_vals_{}; 809 | }; 810 | ``` 811 | 812 | * 对一些简单的情况,使用原子变量 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 可能开销更低(取决于机器及 [std::mutex](https://en.cppreference.com/w/cpp/thread/mutex) 的实现) 813 | 814 | ```cpp 815 | class Point { 816 | public: 817 | double distance_from_origin() const noexcept { 818 | ++call_count_; // 计算调用次数 819 | return std::sqrt((x_ * x_) + (y_ * y_)); 820 | } 821 | 822 | private: 823 | mutable std::atomic call_count_{0}; 824 | double x_; 825 | double y_; 826 | }; 827 | ``` 828 | 829 | * 因为 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 的开销比较低,很容易想当然地用多个原子变量来同步 830 | 831 | ```cpp 832 | class A { 833 | public: 834 | int f() const { 835 | if (flag_) { 836 | return res_; 837 | } else { 838 | auto x = expensive_computation1(); 839 | auto y = expensive_computation2(); 840 | res_ = x + y; 841 | flag_ = true; // 设置标记 842 | return res_; 843 | } 844 | } 845 | 846 | private: 847 | mutable std::atomic flag_{false}; 848 | mutable std::atomic res_; 849 | }; 850 | ``` 851 | 852 | * 这样做可行,但如果多个线程同时观察到标记值为 false,每个线程都要继续进行运算,这个标记反而没起到作用。先设置标记再计算可以消除这个问题,但会引起一个更大的问题 853 | 854 | ```cpp 855 | class A { 856 | public: 857 | int f() const { 858 | if (flag_) { 859 | return res_; 860 | } else { 861 | flag_ = true; // 在计算前设置标记值为 true 862 | auto x = expensive_computation1(); 863 | auto y = expensive_computation2(); 864 | res_ = x + y; 865 | return res_; 866 | } 867 | } 868 | 869 | private: 870 | mutable std::atomic flag_{false}; 871 | mutable std::atomic res_; 872 | }; 873 | ``` 874 | 875 | * 假如线程 1 刚设置好标记,线程 2 此时正好检查到标记值为 true 并直接返回数据值,然后线程 1 接着计算结果,这样线程 2 的返回值就是错的 876 | * 因此如果要同步多个变量或内存区,最好还是使用 [std::mutex](https://en.cppreference.com/w/cpp/thread/mutex) 877 | 878 | ```cpp 879 | class A { 880 | public: 881 | int f() const { 882 | std::lock_guard lk{m_}; 883 | if (flag_) { 884 | return res_; 885 | } else { 886 | auto x = expensive_computation1(); 887 | auto y = expensive_computation2(); 888 | res_ = x + y; 889 | flag_ = true; 890 | return res_; 891 | } 892 | } 893 | 894 | private: 895 | mutable std::mutex m_; 896 | mutable bool flag_{false}; 897 | mutable int res_; 898 | }; 899 | ``` 900 | 901 | ## 17 特殊成员函数的隐式合成与抑制机制 902 | 903 | * C++11 中的特殊成员函数多了两个:移动构造函数和移动赋值运算符 904 | 905 | ```cpp 906 | struct A { 907 | A(A&& rhs); // 移动构造函数 908 | A& operator=(A&& rhs); // 移动赋值运算符 909 | }; 910 | ``` 911 | 912 | * 移动操作同样会在需要时生成,执行的是对 non-static 成员的移动操作,另外它们也会对基类部分执行移动操作 913 | * 移动操作并不确保真正移动,其核心是把 [std::move](https://en.cppreference.com/w/cpp/utility/move) 用于每个要移动的对象,根据返回值的重载解析决定执行移动还是拷贝。因此按成员移动分为两部分:对支持移动操作的类型进行移动,对不可移动的类型执行拷贝 914 | * 两种拷贝操作(拷贝构造函数和拷贝复制运算符)是独立的,声明其中一个不会阻止编译器生成另一个 915 | * 两种移动操作是不独立的,声明其中一个将阻止编译器生成另一个。理由是如果声明了移动构造函数,可能意味着实现上与编译器默认按成员移动的移动构造函数有所不同,从而可以推断移动赋值操作也应该与默认行为不同 916 | * 显式声明拷贝操作(即使声明为 =delete)会阻止自动生成移动操作(但声明为 =default 不阻止生成)。理由类似上条,声明拷贝操作可能意味着默认的拷贝方式不适用,从而推断移动操作也应该会默认行为不同 917 | * 反之亦然,声明移动操作也会阻止生成拷贝操作 918 | * C++11 规定,显式声明析构函数会阻止生成移动操作。这个规定源于 Rule of Three,即两种拷贝函数和析构函数应该一起声明。这个规则的推论是,如果声明了析构函数,则说明默认的拷贝操作也不适用,但 C++98 中没有重视这个推论,因此仍可以生成拷贝操作,而在 C++11 中为了保持不破坏遗留代码,保留了这个规则。由于析构函数和拷贝操作需要一起声明,加上声明了拷贝操作会阻止生成移动操作,于是 C++11 就有了这条规定 919 | * 最终,生成移动操作的条件必须满足:该类没有用户声明的拷贝、移动、析构中的任何一个函数 920 | * 总有一天这个规则会扩展到拷贝操作,因为 C++11 规定存在拷贝操作或析构函数时,仍能生成拷贝操作是被废弃的行为。C++11 提供了 =default 来表示使用默认行为,而不抑制生成其他函数 921 | * 这种手法对于多态基类很有用,多态基类一般会有虚析构函数,虚析构函数的默认实现一般是正确的,为了使用默认行为而不阻止生成移动操作,则应该使用 =default,同理,如果要使用默认的移动操作而不阻止生成拷贝操作,则应该给移动操作加上 =default 922 | 923 | ```cpp 924 | struct A { 925 | virtual ~A() = default; 926 | A(A&&) = default; 927 | A& operator=(A&&) = default; 928 | A(const A&) = default; 929 | A& operator=(const A&) = default; 930 | }; 931 | ``` 932 | 933 | * 事实上不需要思考太多限制,如果需要默认操作就使用 =default,虽然麻烦一些,但可以避免许多问题。对于如下类,没有声明任何特殊成员函数,编译器将在需要时自动合成 934 | 935 | ```cpp 936 | class StringTable { 937 | private: 938 | std::map values_; 939 | }; 940 | ``` 941 | 942 | * 假设过了一段时间后,想扩充一些行为,比如记录构造和析构日志 943 | 944 | ```cpp 945 | class StringTable { 946 | public: 947 | StringTable() { makeLogEntry("Creating StringTable object"); } 948 | ~StringTable() { makeLogEntry("Destroying StringTable object"); } 949 | 950 | private: 951 | std::map values_; 952 | }; 953 | ``` 954 | 955 | * 这时析构函数就会阻止生成移动操作,但针对移动操作的测试可以通过编译,因为在不可移动时会使用拷贝操作,而这很难被察觉。执行移动的代码实际变成了拷贝,而这一切只是源于添加了一个析构函数。避免这个问题也不是难事,只需要一开始把拷贝和移动操作声明为 =default 956 | * 另外还有默认构造函数和析构函数的生成未被提及,这里将统一总结 957 | * 默认构造函数:和 C++98 相同,只在类中不存在用户声明的构造函数时生成 958 | * 析构函数: 959 | * 和 C++98 基本相同,唯一的区别是默认为 noexcept 960 | * 和 C++98 相同,只有基类的析构函数为虚函数,派生类的析构函数才为虚函数 961 | * 拷贝构造函数: 962 | * 仅当类中不存在用户声明的拷贝构造函数时生成 963 | * 如果声明了移动操作,则拷贝构造函数被删除 964 | * 如果声明了拷贝赋值运算符或析构函数,仍能生成拷贝构造函数,但这是被废弃的行为 965 | * 拷贝赋值运算符: 966 | * 仅当类中不存在用户声明的拷贝赋值运算符时生成 967 | * 如果声明了移动操作,则拷贝赋值运算符被删除 968 | * 如果声明了拷贝构造函数或析构函数,仍能生成拷贝赋值运算符,但这是被废弃的行为 969 | * 移动操作:仅当类中不存在任何用户声明的拷贝操作、移动操作、析构函数时生成 970 | * 注意,这些机制中提到的是成员函数而非成员函数模板,模板并不会影响特殊成员函数的合成 971 | 972 | ```cpp 973 | struct A { 974 | template 975 | A(const T& rhs); // 从任意类型构造 976 | 977 | template 978 | A& operator=(const T& rhs); // 从任意类型赋值 979 | }; 980 | ``` 981 | 982 | * 上述模板不会阻止编译器生成拷贝和移动操作,即使模板的实例化和拷贝操作签名相同(即 T 是 A) 983 | -------------------------------------------------------------------------------- /docs/04_smart_pointers.md: -------------------------------------------------------------------------------- 1 | * 原始指针的缺陷有: 2 | * 声明中未指出指向的是单个对象还是一个数组 3 | * 没有提示使用完对象后是否需要析构,从声明中无法看出指针是否拥有对象 4 | * 不知道析构该使用 delete 还是其他方式(比如传入一个专门用于析构的函数) 5 | * 即使知道了使用 delete,也不知道 delete 的是单个对象还是数组(使用 delete[]) 6 | * 难以保证所有路径上只产生一次析构 7 | * 没有检查空悬指针的办法 8 | * 智能指针解决了这些问题,它封装了原始指针,行为看起来和原始指针类似但大大减少了犯错的可能 9 | * C++11 引入了三种智能指针:[std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr)、[std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr)、[std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 10 | 11 | ## 18 用 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 管理所有权唯一的资源 12 | 13 | * 使用智能指针时一般首选 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr),默认情况下它和原始指针尺寸相同 14 | * [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 对资源拥有唯一所有权,因此它是 move-only 类型,不允许拷贝。它常用作工厂函数的返回类型,这样工厂函数生成的对象在需要销毁时会被自动析构,而不需要手动析构 15 | 16 | ```cpp 17 | struct A {}; 18 | 19 | std::unique_ptr make_a() { return std::unique_ptr{new A}; } 20 | 21 | auto p = make_a(); 22 | ``` 23 | 24 | * [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 的析构默认通过 delete 内部的原始指针完成,但也可以自定义删除器,删除器需要一个 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 内部指针类型作为参数 25 | 26 | ```cpp 27 | struct A {}; 28 | 29 | auto f = [](A* p) { 30 | std::cout << "destroy\n"; 31 | delete p; 32 | }; 33 | 34 | std::unique_ptr make_a() { 35 | std::unique_ptr p{new A, f}; 36 | return p; 37 | } 38 | ``` 39 | 40 | * 使用 C++14 的 auto 返回类型,可以将删除器的 lambda 定义在工厂函数内,封装性更好一些 41 | 42 | ```cpp 43 | struct A {}; 44 | 45 | auto make_a() { 46 | auto f = [](A* p) { 47 | std::cout << "destroy\n"; 48 | delete p; 49 | }; 50 | std::unique_ptr p{new A, f}; 51 | return p; 52 | } 53 | ``` 54 | 55 | * 可以进一步扩展成支持继承体系的工厂函数 56 | 57 | ```cpp 58 | struct A { 59 | // 删除器对任何对象调用的是基类的析构函数,因此必须声明为虚函数 60 | virtual ~A() = default; 61 | }; 62 | // 基类的析构函数为虚函数,则派生类的析构函数默认为虚函数 63 | struct B : A {}; 64 | struct C : A {}; 65 | struct D : A {}; 66 | 67 | auto make_a(int i) { 68 | auto f = [](A* p) { 69 | std::cout << "destroy\n"; 70 | delete p; 71 | }; 72 | std::unique_ptr p{nullptr, f}; 73 | if (i == 1) { 74 | p.reset(new B); 75 | } else if (i == 2) { 76 | p.reset(new C); 77 | } else { 78 | p.reset(new D); 79 | } 80 | return p; 81 | } 82 | ``` 83 | 84 | * 默认情况下,[std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 和原始指针尺寸相同,如果自定义删除器则 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 会加上删除器的尺寸。一般无状态的函数对象(如无捕获的 lambda)不会浪费任何内存,作为删除器可以节约空间 85 | 86 | ```cpp 87 | struct A {}; 88 | 89 | auto f = [](A* p) { delete p; }; 90 | 91 | void g(A* p) { delete p; } 92 | 93 | struct X { 94 | void operator()(A* p) const { delete p; } 95 | }; 96 | 97 | std::unique_ptr p1{new A}; 98 | std::unique_ptr p2{new A, f}; 99 | std::unique_ptr p3{new A, g}; 100 | std::unique_ptr p4{new A, X{}}; 101 | 102 | static_assert(sizeof(p1) == sizeof(nullptr)); // 默认尺寸,即一个原始指针的尺寸 103 | static_assert(sizeof(p2) == sizeof(nullptr)); // 无捕获 lambda 不会浪费尺寸 104 | static_assert(sizeof(p3) == sizeof(nullptr) * 2); // 函数指针占一个原始指针尺寸 105 | // 无状态的函数对象,但如果函数对象有状态(如数据成员、虚函数)就会增加尺寸 106 | static_assert(sizeof(p4) == sizeof(nullptr)); 107 | ``` 108 | 109 | * [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 作为返回类型的另一个方便之处是,可以转为 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 110 | 111 | ```cpp 112 | // std::make_unique 的返回类型是 std::unique_ptr 113 | std::shared_ptr p = std::make_unique(42); 114 | ``` 115 | 116 | * [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 针对数组提供了一个特化版本,此版本提供 [operator[]](https://en.cppreference.com/w/cpp/memory/unique_ptr/operator_at),但不提供[单元素版本的 `operator*` 和 `operator->`](https://en.cppreference.com/w/cpp/memory/unique_ptr/operator*),这样对其指向的对象就不存在二义性 117 | 118 | ```cpp 119 | #include 120 | #include 121 | 122 | int main() { 123 | std::unique_ptr p{new int[3]{0, 1, 2}}; 124 | for (int i = 0; i < 3; ++i) { 125 | assert(p[i] == i); 126 | } 127 | } 128 | ``` 129 | 130 | ## 19 用 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 管理所有权可共享的资源 131 | 132 | * [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 内部有一个引用计数,用来存储资源被共享的次数。因为内部多了一个指向引用计数的指针,所以 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的尺寸是原始指针的两倍 133 | 134 | ```cpp 135 | int* p = new int{42}; 136 | auto q = std::make_shared(42); 137 | static_assert(sizeof(p) == sizeof(nullptr)); 138 | static_assert(sizeof(q) == sizeof(nullptr) * 2); 139 | ``` 140 | 141 | * [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 默认析构方式和 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 一样,也是 delete 内部的原始指针,同样可以自定义删除器,不过不需要在模板参数中指明删除器类型 142 | 143 | ```cpp 144 | class A {}; 145 | auto f = [](A* p) { delete p; }; 146 | 147 | std::unique_ptr p{new A, f}; 148 | std::shared_ptr q{new A, f}; 149 | ``` 150 | 151 | * 模板参数中不含删除器的设计为接口提供了更好的灵活度 152 | 153 | ```cpp 154 | std::shared_ptr p{new A, f}; 155 | std::shared_ptr q{new A, g}; 156 | // 使用不同的删除器但具有相同的类型,因此可以放进同一容器 157 | std::vector> v{p, q}; 158 | ``` 159 | 160 | * 删除器不影响 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的尺寸,因为删除器不是 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的一部分,而是位于堆上或自定义分配器的内存位置。[std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 有一个 control block,它包含了引用计数的指针和自定义删除器的拷贝,以及一些其他数据(比如弱引用计数) 161 | 162 | ``` 163 | std::shared_ptr 164 | ---------------------- ---------- 165 | | Ptr to T | ------------> | T Object | 166 | ---------------------- ---------- 167 | | Prt to Control Block |\ 168 | ---------------------- \-----------> Control Block 169 | --------------------------------------- 170 | | Reference Count | 171 | --------------------------------------- 172 | | Weak Count | 173 | --------------------------------------- 174 | | Other Data | 175 | | (eg. custom deleter, allocator, etc.) | 176 | --------------------------------------- 177 | ``` 178 | 179 | * alias constructor 可以使 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 拥有某个对象所有权,但内容指向另一对象 180 | 181 | ```cpp 182 | #include 183 | #include 184 | 185 | int main() { 186 | auto p = std::make_shared(0); 187 | std::shared_ptr q(p, new int(1)); 188 | assert(*q == 1); 189 | assert(p.use_count() == 2); 190 | assert(q.use_count() == 2); 191 | p.reset(); 192 | assert(p.use_count() == 0); 193 | assert(q.use_count() == 1); 194 | // 再次共享时引用计数为 0,但内部却可以保存值 195 | std::shared_ptr p2(p, new int(2)); 196 | assert(*p2 == 2); 197 | assert(p.use_count() == 0); 198 | assert(q.use_count() == 1); 199 | assert(p2.use_count() == 0); 200 | } 201 | ``` 202 | 203 | * [dynamic_pointer_cast](https://en.cppreference.com/w/cpp/memory/shared_ptr/pointer_cast) 的实现就使用了 alias constructor 204 | 205 | ```cpp 206 | template 207 | std::shared_ptr dynamic_pointer_cast(const std::shared_ptr& r) noexcept { 208 | if (auto p = 209 | dynamic_cast::element_type*>(r.get())) { 210 | return std::shared_ptr(r, p); // 返回值与源指针共享所有权 211 | } else { 212 | return std::shared_ptr(); 213 | } 214 | } 215 | ``` 216 | 217 | * [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 内部实现如下 218 | 219 | ```cpp 220 | template 221 | struct sp_element { 222 | using type = T; 223 | }; 224 | 225 | template 226 | struct sp_element { 227 | using type = T; 228 | }; 229 | 230 | template 231 | struct sp_element { 232 | using type = T; 233 | }; 234 | 235 | template 236 | class shared_ptr { 237 | using elem_type = typename sp_element::type; 238 | elem_type* px; // 内部指针 239 | shared_count pn; // 引用计数 240 | template 241 | friend class shared_ptr; 242 | template 243 | friend class weak_ptr; 244 | }; 245 | 246 | class shared_count { 247 | sp_counted_base* pi; 248 | int shared_count_id; 249 | friend class weak_count; 250 | }; 251 | 252 | class weak_count { 253 | sp_counted_base* pi; 254 | }; 255 | 256 | class sp_counted_base { 257 | int use_count; // 引用计数 258 | int weak_count; // 弱引用计数 259 | }; 260 | 261 | template 262 | class sp_counted_impl_p : public sp_counted_base { 263 | T* px; // 删除器 264 | }; 265 | ``` 266 | 267 | * [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 保证线程安全,因此引用计数的递增和递减是原子操作,原子操作一般比非原子操作慢 268 | 269 | ```cpp 270 | class sp_counted_base { 271 | private: 272 | std::atomic_int_least32_t use_count_; // 即 std::atomic 273 | std::atomic_int_least32_t weak_count_; 274 | 275 | public: 276 | sp_counted_base() : use_count_(1), weak_count_(1) {} 277 | virtual ~sp_counted_base() {} 278 | virtual void dispose() = 0; 279 | virtual void destroy() { delete this; } 280 | 281 | void add_ref_copy() { atomic_increment(&use_count_); } 282 | 283 | bool add_ref_lock() { return atomic_conditional_increment(&use_count_) != 0; } 284 | 285 | void release() { 286 | if (atomic_decrement(&use_count_) == 1) { 287 | dispose(); 288 | weak_release(); 289 | } 290 | } 291 | 292 | void weak_add_ref() { atomic_increment(&weak_count_); } 293 | 294 | long use_count() const { return use_count_.load(std::memory_order_acquire); } 295 | }; 296 | ``` 297 | 298 | * control block 在创建第一个 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 时确定,因此 control block 的创建发生在如下时机 299 | * 调用 [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 时:调用时生成一个新对象,此时显然不会有关于该对象的 control block 300 | * 从 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 构造 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 时:因为 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 没有 control block 301 | * 用原始指针构造 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 时 302 | * 这意味着用同一个原始指针构造多个 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr),将创建多个 control block,即有多个引用指针,当引用指针变为零时就会出现多次析构的错误 303 | 304 | ```cpp 305 | #include 306 | 307 | int main() { 308 | { 309 | int* i = new int{42}; 310 | std::shared_ptr p{i}; 311 | std::shared_ptr q{i}; 312 | } // 错误 313 | } 314 | ``` 315 | 316 | * 使用 [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 就不会有这个问题 317 | 318 | ```cpp 319 | auto p = std::make_shared(42); 320 | ``` 321 | 322 | * 但 [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 不支持自定义删除器,这时应该直接传递 new 的结果 323 | 324 | ```cpp 325 | auto f = [](int*) {}; 326 | std::shared_ptr p{new int(42), f}; 327 | ``` 328 | 329 | * 用类的 this 指针构造 [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 时,`*this` 的所有权不会被共享 330 | 331 | ```cpp 332 | #include 333 | #include 334 | 335 | class A { 336 | public: 337 | std::shared_ptr f() { return std::shared_ptr(this); } 338 | }; 339 | 340 | int main() { 341 | { 342 | auto p = std::make_shared(); 343 | auto q = p->f(); 344 | assert(p.use_count() == 1); 345 | assert(q.use_count() == 1); 346 | } // ERROR 347 | } 348 | ``` 349 | 350 | * 比如用类自身的 shared_ptr 传给回调函数,回调结束时,类对象就会被析构 351 | 352 | ```cpp 353 | #include 354 | 355 | template 356 | void callback(const T& p) { 357 | p->print(); 358 | } 359 | 360 | class A { 361 | public: 362 | void f() { callback(std::shared_ptr(this)); } 363 | void print() {} 364 | }; 365 | 366 | int main() { 367 | { 368 | auto p = std::make_shared(); 369 | p->f(); // 调用结束时析构 std::shared_ptr(this),p 中的对象被析构 370 | } // ERROR:再次析构 371 | } 372 | ``` 373 | 374 | * 为了解决这个问题,需要继承 [std::enable_shared_from_this](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this),通过其提供的 [shared_from_this](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this/shared_from_this) 获取 `*this` 的所有权 375 | 376 | ```cpp 377 | #include 378 | #include 379 | 380 | class A : public std::enable_shared_from_this { 381 | public: 382 | std::shared_ptr f() { return shared_from_this(); } 383 | }; 384 | 385 | int main() { 386 | { 387 | auto p = std::make_shared(); 388 | auto q = p->f(); 389 | assert(p.use_count() == 2); 390 | assert(q.use_count() == 2); 391 | } // OK 392 | } 393 | ``` 394 | 395 | * [shared_from_this](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this/shared_from_this) 的原理是为 `*this` 的 control block 创建一个新的 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 396 | 397 | ```cpp 398 | template 399 | class enable_shared_from_this { 400 | public: 401 | shared_ptr shared_from_this() { 402 | shared_ptr p(weak_this_); 403 | return p; 404 | } 405 | 406 | public: 407 | /* 408 | * 构造 shared_ptr 时 409 | * 如果 A 继承了 enable_shared_from_this 410 | * 则调用此函数 411 | */ 412 | template 413 | void _internal_accept_owner(const shared_ptr* ppx, Y* py) { 414 | if (weak_this_.expired()) { 415 | // alias constructor,共享 *ppx 的引用计数,但指向 py 416 | weak_this_ = shared_ptr(*ppx, py); 417 | } 418 | } 419 | 420 | private: 421 | weak_ptr weak_this_; 422 | }; 423 | 424 | template 425 | class shared_ptr { 426 | public: 427 | template 428 | explicit shared_ptr(Y* p) : px(p), pn() { 429 | boost::detail::sp_pointer_construct(this, p, pn); 430 | } 431 | 432 | template 433 | void sp_pointer_construct(boost::shared_ptr* ppx, Y* p, 434 | boost::detail::shared_count& pn) { 435 | boost::detail::shared_count(p).swap(pn); 436 | boost::detail::sp_enable_shared_from_this(ppx, p, p); 437 | } 438 | 439 | template 440 | void sp_enable_shared_from_this(boost::shared_ptr const* ppx, Y const* py, 441 | boost::enable_shared_from_this const* pe) { 442 | if (pe != 0) { 443 | pe->_internal_accept_owner(ppx, const_cast(py)); // ppx 和 py 一致 444 | } 445 | } 446 | 447 | void sp_enable_shared_from_this(...) { 448 | } // 如果第三个参数不能转为 enable_shared_from_this 则调用此函数 449 | 450 | shared_ptr(shared_ptr const& r, element_type* p) 451 | : px(p), pn(r.pn) {} // alias constructor,共享引用计数但指向不同对象 452 | 453 | private: 454 | using elem_type = typename sp_element::type; 455 | element_type* px; // 内部指针 456 | boost::detail::shared_count pn; // 控制块,包含引用计数、删除器 457 | } 458 | ``` 459 | 460 | * `*this` 必须有一个已关联的 control block,即有一个指向 `*this` 的 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr),否则行为未定义,抛出 [std::bad_weak_ptr](https://en.cppreference.com/w/cpp/memory/bad_weak_ptr) 异常 461 | 462 | ```cpp 463 | #include 464 | 465 | class A : public std::enable_shared_from_this { 466 | public: 467 | std::shared_ptr f() { return shared_from_this(); } 468 | }; 469 | 470 | int main() { 471 | auto p = new A; 472 | auto q = p->f(); // 抛出 std::bad_weak_ptr 异常 473 | } 474 | ``` 475 | 476 | * 为了只允许创建用 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 指向的对象,可以将构造函数放进 private 作用域,并提供一个返回 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 对象的工厂函数 477 | 478 | ```cpp 479 | #include 480 | 481 | class A : public std::enable_shared_from_this { 482 | public: 483 | static std::shared_ptr create() { return std::shared_ptr(new A); } 484 | std::shared_ptr f() { return shared_from_this(); } 485 | 486 | private: 487 | A() = default; 488 | }; 489 | 490 | int main() { 491 | auto p = A::create(); // 构造函数为 private,auto p = new A 将报错 492 | auto q = p->f(); // OK 493 | } 494 | ``` 495 | 496 | * MSVC 在 [std::enable_shared_from_this](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this) 中定义了一个别名 `_Esft_type`,在 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 中检测 `_Esft_type` 别名,如果存在则说明继承了 [std::enable_shared_from_this](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this),此时再用 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 初始化基类中名为 `_Wptr` 的 weak_ptr 497 | 498 | ```cpp 499 | #include 500 | 501 | // 检测 T* 能否转为 T::_Esft_type* 502 | template 503 | struct _enable_shared : false_type {}; 504 | 505 | template 506 | struct _enable_shared> 507 | : is_convertible*, typename T::_Esft_type*>::type {}; 508 | 509 | template 510 | class shared_ptr { 511 | public: 512 | template 513 | explicit shared_ptr(Y* px) { 514 | _Set_ptr_rep_and_enable_shared(px, new _Ref_count(px)); 515 | } 516 | 517 | template 518 | void _Set_ptr_rep_and_enable_shared(Y* const px, _Ref_count_base* const pn) { 519 | this->_Ptr = px; 520 | this->_Rep = pn; 521 | if constexpr (_enable_shared < Y >>) { 522 | if (px && px->_Wptr.expired()) { 523 | px->_Wptr = 524 | shared_ptr>(*this, const_cast*>(px)); 525 | } 526 | } 527 | } 528 | } 529 | ``` 530 | 531 | * 根据 MSVC 的检测方式自定义一个能被 MSVC 的 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 使用的 enable_shared_from_this 532 | 533 | ```cpp 534 | #include 535 | #include 536 | 537 | template 538 | class B { 539 | public: 540 | using _Esft_type = B; // 定义 _Esft_type 且派生类指针要能转为 _Esft_type* 541 | 542 | std::shared_ptr my_shared_from_this() { return std::shared_ptr(_Wptr); } 543 | 544 | private: 545 | template 546 | friend class std::shared_ptr; 547 | 548 | std::weak_ptr _Wptr; // 提供一个名为 _Wptr 的 weak_ptr 549 | }; 550 | 551 | class A : public B { 552 | public: 553 | std::shared_ptr f() { return my_shared_from_this(); } 554 | }; 555 | 556 | int main() { 557 | { 558 | auto p = std::make_shared(); 559 | auto q = p->f(); 560 | assert(p.use_count() == 2); 561 | assert(q.use_count() == 2); 562 | } // OK 563 | } 564 | ``` 565 | 566 | ## 20 用 [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 观测 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的内部状态 567 | * [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 不能解引用,它不是一种独立的智能指针,而是 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的一种扩充,它用 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 初始化,共享对象但不改变引用计数,主要作用是观察 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的内部状态 568 | 569 | ```cpp 570 | #include 571 | #include 572 | #include 573 | 574 | std::weak_ptr w; 575 | 576 | void f(std::weak_ptr w) { 577 | if (auto p = w.lock()) { 578 | std::cout << *p; 579 | } else { 580 | std::cout << "can't get value"; 581 | } 582 | } 583 | 584 | int main() { 585 | { 586 | auto p = std::make_shared(42); 587 | w = p; 588 | assert(p.use_count() == 1); 589 | assert(w.expired() == false); 590 | f(w); // 42 591 | auto q = w.lock(); 592 | assert(p.use_count() == 2); 593 | assert(q.use_count() == 2); 594 | } 595 | f(w); // can't get value 596 | assert(w.expired() == true); 597 | assert(w.lock() == nullptr); 598 | } 599 | ``` 600 | 601 | * [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 的另一个作用是解决循环引用问题 602 | 603 | ```cpp 604 | #include 605 | #include 606 | #include 607 | 608 | class B; 609 | class A { 610 | public: 611 | std::shared_ptr b; 612 | virtual ~A() { std::cout << "destroy A\n"; } 613 | }; 614 | 615 | class B { 616 | public: 617 | std::weak_ptr a; 618 | virtual ~B() { std::cout << "destroy B\n"; } 619 | }; 620 | 621 | int main() { 622 | { 623 | auto p = std::make_shared(); 624 | p->b = std::make_shared(); 625 | p->b->a = p; 626 | assert(p.use_count() == 1); 627 | } // p.use_count() 由 1 减为 0,从而正常析构 628 | // 若将 weak_ptr 改为 shared_ptr,p.use_count() 为 2,此处减为 1,不会析构 629 | // 此时 p->b 也不会析构,导致两次内存泄漏 630 | } 631 | ``` 632 | 633 | ## 21 用 [std::make_unique](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique)([std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared)) 创建 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr)([std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr)) 634 | 635 | * C++14 提供了 [std::make_unique](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique),C++11 可以手动实现一个基础功能版 636 | 637 | ```cpp 638 | template 639 | std::unique_ptr make_unique(Args&&... args) { 640 | return std::unique_ptr{new T{std::forward(args)...}}; 641 | } 642 | ``` 643 | 644 | * 这个基础函数不支持数组和自定义删除器,但这些不难实现。从这个基础函数可以看出,make 函数把实参完美转发给构造函数并返回构造出的智能指针。除了 [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 和 [std::make_unique](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique),还有一个 make 函数是 [std::allocate_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/allocate_shared),它的行为和 [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 一样,只不过第一个实参是分配器对象 645 | * 优先使用 make 函数的一个明显原因就是只需要写一次类型 646 | 647 | ```cpp 648 | auto p = std::make_unique(42); 649 | std::unique_ptr q{new int{42}}; 650 | ``` 651 | 652 | * 另一个原因与异常安全相关 653 | 654 | ```cpp 655 | void f(std::shared_ptr p, int n) {} 656 | int g() { return 1; } 657 | f(std::shared_ptr{new A}, g()); // 潜在的内存泄露隐患 658 | // g 可能运行于 new A 还未返回给 std::shared_ptr 的构造函数时 659 | // 此时如果 g 抛出异常,则 new A 就发生了内存泄漏 660 | ``` 661 | 662 | * 解决方法有两种,一是单独用一条语句创建 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr),二是使用 make 函数 663 | 664 | ```cpp 665 | std::shared_ptr p(new A); // 如果发生异常,删除器将析构 new 创建的对象 666 | f(std::move(p), g()); 667 | 668 | f(std::make_shared(), g()); // 不会发生内存泄漏,且只需要一次内存分配 669 | ``` 670 | 671 | * make 函数有两个限制,一是它无法定义删除器 672 | 673 | ```cpp 674 | auto f = [](A* p) { delete p; }; 675 | std::unique_ptr p{new A, f}; 676 | std::shared_ptr q{new A, f}; 677 | ``` 678 | 679 | * 使用自定义删除器,但又想避免内存泄漏,解决方法是单独用一条语句来创建 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 680 | 681 | ```cpp 682 | auto d = [](A* p) { delete p; }; 683 | std::shared_ptr p{new A, d}; // 如果发生异常,删除器将析构 new 创建的对象 684 | f(std::move(p), g()); 685 | ``` 686 | 687 | * make 函数的第二个限制是,make 函数中的完美转发使用的是小括号初始化,在持有 [std::vector](https://en.cppreference.com/w/cpp/container/vector) 类型时,设置初始化值不如大括号初始化方便。一个不算直接的解决方法是,先构造一个 [std::initializer_list](https://en.cppreference.com/w/cpp/utility/initializer_list) 再传入 688 | 689 | ```cpp 690 | auto p = std::make_unique>(3, 6); // vector 中是 3 个 6 691 | auto q = std::make_shared>(3, 6); // vector 中是 3 个 6 692 | 693 | auto x = {1, 2, 3, 4, 5, 6}; 694 | auto p2 = std::make_unique>(x); 695 | auto q2 = std::make_shared>(x); 696 | ``` 697 | 698 | * [std::make_unique](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique) 只存在这两个限制,但 [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 和 [std::allocate_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/allocate_shared) 还有两个限制 699 | * 如果类重载了 [operator new](https://en.cppreference.com/w/cpp/memory/new/operator_new) 和 [operator delete](https://en.cppreference.com/w/cpp/memory/new/operator_delete),其针对的内存尺寸一般为类的尺寸,而 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 还要加上 control block 的尺寸,因此 [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 不适用重载了 [operator new](https://en.cppreference.com/w/cpp/memory/new/operator_new) 和 [operator delete](https://en.cppreference.com/w/cpp/memory/new/operator_delete) 的类 700 | * [std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) 使 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 的 control block 和管理的对象在同一内存上分配(比用 new 构造智能指针在尺寸和速度上更优的原因),对象在引用计数为 0 时被析构,但其占用的内存直到 control block 被析构时才被释放,比如 [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 会持续指向 control block(为了检查引用计数以检查自身是否失效),control block 直到最后一个 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 和 [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 被析构时才释放 701 | * 假如对象尺寸很大,且最后一个 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 和 [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr) 析构之间的时间间隔不能忽略,就会产生对象析构和内存释放之间的延迟 702 | 703 | ```cpp 704 | auto p = std::make_shared(); 705 | … // 创建指向该对象的多个 std::shared_ptr 和 std::weak_ptr 并做一些操作 706 | … // 最后一个 std::shared_ptr 被析构,但 std::weak_ptr 仍存在 707 | … // 此时,大尺寸对象占用内存仍未被回收 708 | … // 最后一个 std::weak_ptr 被析构,control block 和对象占用的同一内存块被释放 709 | ``` 710 | 711 | * 如果 new 构造 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr),最后一个 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 被析构时,内存就能立即被释放 712 | 713 | ```cpp 714 | std::shared_ptr p(new ReallyBigType); 715 | … // 创建指向该对象的多个 std::shared_ptr 和 std::weak_ptr 并做一些操作 716 | … // 最后一个 std::shared_ptr 被析构,std::weak_ptr 仍存在,但 ReallyBigType 占用的内存立即被释放 717 | … // 此时,仅 control block 内存处于分配而未回收状态 718 | … // 最后一个 std::weak_ptr 被析构,control block 的内存块被释放 719 | ``` 720 | 721 | ## 22 用 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 实现 [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) 必须在源文件中提供析构函数定义 722 | 723 | * [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) 就是把数据成员提取到类中,用指向该类的指针替代原来的数据成员。因为数据成员会影响内存布局,将数据成员用一个指针替代可以减少编译期依赖,保持 ABI 兼容 724 | * 比如对如下类 725 | 726 | ```cpp 727 | // A.h 728 | #include 729 | #include 730 | 731 | class A { 732 | private: 733 | int i; 734 | std::string s; 735 | std::vector v; 736 | }; 737 | ``` 738 | 739 | * 使用 [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) 后 740 | 741 | ```cpp 742 | // A.h 743 | class A { 744 | public: 745 | A(); 746 | ~A(); 747 | 748 | private: 749 | struct X; 750 | X* x; 751 | }; 752 | 753 | // A.cpp 754 | #include 755 | #include 756 | 757 | #include "A.h" 758 | 759 | struct A::X { 760 | int i; 761 | std::string s; 762 | std::vector v; 763 | }; 764 | 765 | A::A() : x(new X) {} 766 | A::~A() { delete x; } 767 | ``` 768 | 769 | * 现在使用 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 替代原始指针,不再需要使用析构函数释放指针 770 | 771 | ```cpp 772 | // A.h 773 | #include 774 | 775 | class A { 776 | public: 777 | A(); 778 | 779 | private: 780 | struct X; 781 | std::unique_ptr x; 782 | }; 783 | 784 | // A.cpp 785 | #include 786 | #include 787 | 788 | #include "A.h" 789 | 790 | struct A::X { 791 | int i; 792 | std::string s; 793 | std::vector v; 794 | }; 795 | 796 | A::A() : x(std::make_unique()) {} 797 | ``` 798 | 799 | * 但调用上述代码会出错 800 | 801 | ```cpp 802 | // main.cpp 803 | #include "A.h" 804 | 805 | int main() { 806 | A a; // 错误:A::X 是不完整类型 807 | } 808 | ``` 809 | 810 | * 原因在于 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 析构时会在内部调用默认删除器,默认删除器的 delete 语句之前会用 [static_assert](https://en.cppreference.com/w/cpp/language/static_assert) 断言指针指向的不是非完整类型 811 | 812 | ```cpp 813 | // 删除器的实现 814 | template 815 | struct default_delete { // default deleter for unique_ptr 816 | constexpr default_delete() noexcept = default; 817 | 818 | template , int> = 0> 819 | default_delete(const default_delete&) noexcept {} 820 | 821 | void operator()(T* p) const noexcept { 822 | static_assert(0 < sizeof(T), "can't delete an incomplete type"); 823 | delete p; 824 | } 825 | }; 826 | ``` 827 | 828 | * 解决方法就是让析构 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 的代码看见完整类型,即让析构函数的定义位于要析构的类型的定义之后 829 | 830 | ```cpp 831 | // A.h 832 | #include 833 | 834 | class A { 835 | public: 836 | A(); 837 | ~A(); 838 | 839 | private: 840 | struct X; 841 | std::unique_ptr x; 842 | }; 843 | 844 | // A.cpp 845 | #include 846 | #include 847 | 848 | #include "A.h" 849 | 850 | struct A::X { 851 | int i; 852 | std::string s; 853 | std::vector v; 854 | }; 855 | 856 | A::A() : x(std::make_unique()) {} 857 | A::~A() = default; // 必须位于 A::X 的定义之后 858 | ``` 859 | 860 | * 使用 [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) 的类自然应该支持移动操作,但定义析构函数会阻止默认生成移动操作,因此会想到添加默认的移动操作声明 861 | 862 | ```cpp 863 | // A.h 864 | #include 865 | 866 | class A { 867 | public: 868 | A(); 869 | ~A(); 870 | A(A&&) = default; 871 | A& operator=(A&&) = default; 872 | 873 | private: 874 | struct X; 875 | std::unique_ptr x; 876 | }; 877 | 878 | // A.cpp 879 | #include 880 | #include 881 | 882 | #include "A.h" 883 | 884 | struct A::X { 885 | int i; 886 | std::string s; 887 | std::vector v; 888 | }; 889 | 890 | A::A() : x(std::make_unique()) {} 891 | A::~A() = default; // 必须位于 A::X 的定义之后 892 | ``` 893 | 894 | * 但调用移动操作会出现相同的问题 895 | 896 | ```cpp 897 | // main.cpp 898 | #include "A.h" 899 | 900 | int main() { 901 | A a; 902 | A b(std::move(a)); // 错误:使用了未定义类型 A::X 903 | A c = std::move(a); // 错误:使用了未定义类型 A::X 904 | } 905 | ``` 906 | 907 | * 原因也一样,移动操作会先析构原有对象,调用删除器时触发断言。解决方法也一样,让移动操作的定义位于要析构的类型的定义之后 908 | 909 | ```cpp 910 | // A.h 911 | #include 912 | 913 | class A { 914 | public: 915 | A(); 916 | ~A(); 917 | A(A&&); 918 | A& operator=(A&&); 919 | 920 | private: 921 | struct X; 922 | std::unique_ptr x; 923 | }; 924 | 925 | // A.cpp 926 | #include 927 | #include 928 | 929 | #include "A.h" 930 | 931 | struct A::X { 932 | int i; 933 | std::string s; 934 | std::vector v; 935 | }; 936 | 937 | A::A() : x(std::make_unique()) {} 938 | A::A(A&&) = default; 939 | A& A::operator=(A&&) = default; 940 | A::~A() = default; 941 | ``` 942 | 943 | * 编译器不会为 [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 这类 move-only 类型生成拷贝操作,即使可以生成也只是拷贝指针本身(浅拷贝),因此如果要提供拷贝操作,则需要自己编写 944 | 945 | ```cpp 946 | // A.h 947 | #include 948 | 949 | class A { 950 | public: 951 | A(); 952 | ~A(); 953 | A(A&&); 954 | A& operator=(A&&); 955 | A(const A&); 956 | A& operator=(const A&); 957 | 958 | private: 959 | struct X; 960 | std::unique_ptr x; 961 | }; 962 | 963 | // A.cpp 964 | #include 965 | #include 966 | 967 | #include "A.h" 968 | 969 | struct A::X { 970 | int i; 971 | std::string s; 972 | std::vector v; 973 | }; 974 | 975 | A::A() : x(std::make_unique()) {} 976 | A::A(A&&) = default; 977 | A& A::operator=(A&&) = default; 978 | A::~A() = default; 979 | A::A(const A& rhs) : x(std::make_unique(*rhs.x)) {} 980 | A& A::operator=(const A& rhs) { 981 | *x = *rhs.x; 982 | return *this; 983 | } 984 | ``` 985 | 986 | * 如果使用 [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr),则不需要关心上述所有问题 987 | 988 | ```cpp 989 | // A.h 990 | #include 991 | 992 | class A { 993 | public: 994 | A(); 995 | 996 | private: 997 | struct X; 998 | std::shared_ptr x; 999 | }; 1000 | 1001 | // A.cpp 1002 | #include 1003 | #include 1004 | 1005 | #include "A.h" 1006 | 1007 | struct A::X { 1008 | int i; 1009 | std::string s; 1010 | std::vector v; 1011 | }; 1012 | 1013 | A::A() : x(std::make_shared()) {} 1014 | ``` 1015 | 1016 | * 实现 [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) 时,[std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 尺寸更小,运行更快一些,但必须在实现文件中指定特殊成员函数,[std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 开销大一些,但不需要考虑因为删除器引发的一系列问题。但对于 [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) 来说,主类和数据成员类之间是专属所有权的关系,[std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr) 更合适。如果在需要共享所有权的特殊情况下,[std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) 更合适 1017 | --------------------------------------------------------------------------------