├── code ├── 02include指令 │ ├── array.txt │ ├── main2.cpp │ ├── 1.txt │ ├── main.cpp │ └── README.md ├── 01模板分文件 │ ├── test.h │ ├── test.cpp │ ├── test_template.cpp │ ├── test_template.h │ ├── main.cpp │ └── CMakeLists.txt ├── 03类模板分文件 │ ├── test_class.h │ ├── test_class.cpp │ ├── test_class_template.h │ ├── test_class_template.cpp │ ├── main.cpp │ ├── CMakeLists.txt │ └── CMakePresets.json ├── 04显式实例化解决模板分文件问题 │ ├── test_function_template.h │ ├── test_function_template.cpp │ ├── test_class_template.h │ ├── main.cpp │ ├── CMakeLists.txt │ └── test_class_template.cpp ├── 生成动态库 │ ├── 生成动态库.vcxproj.user │ ├── export_template.h │ ├── export_template.cpp │ ├── 生成动态库.vcxproj.filters │ └── 生成动态库.vcxproj ├── 生成静态库 │ ├── 生成静态库.vcxproj.user │ ├── export_template.h │ ├── export_template.cpp │ ├── 生成静态库.vcxproj.filters │ └── 生成静态库.vcxproj ├── 测试使用静态库 │ ├── 测试使用静态库.vcxproj.user │ ├── export_template.h │ ├── run_test.cpp.cpp │ ├── 测试使用静态库.vcxproj.filters │ └── 测试使用静态库.vcxproj └── 05显式实例化解决模板导出动态静态库问题 │ ├── 05显式实例化解决模板导出动态静态库问题.vcxproj.user │ ├── run_test.cpp.cpp │ ├── export_template.h │ ├── 05显式实例化解决模板导出动态静态库问题.vcxproj.filters │ ├── 05显式实例化解决模板导出动态静态库问题.sln │ └── 05显式实例化解决模板导出动态静态库问题.vcxproj ├── image ├── icon.webp └── 捐赠 │ ├── 支付宝10.jpg │ ├── 支付宝20.jpg │ ├── 支付宝88.88.jpg │ ├── README.md │ └── thanks.md ├── homework ├── README.md └── 08折叠表达式作业 │ ├── ooolize.md │ ├── mq日.md │ ├── 提莫大绅士.md │ ├── 我是男孩.md │ ├── roseveknif.md │ ├── saidfljwnzjasf.md │ └── mq卢瑟.md ├── md ├── 第一部分-基础知识 │ ├── 12总结.md │ ├── 07显式实例化解决模板导出静态动态库.md │ ├── 05模板偏特化.md │ ├── 03变量模板.md │ ├── 08折叠表达式.md │ ├── 06模板显式实例化解决模板分文件问题.md │ ├── 09待决名.md │ ├── 04模板全特化.md │ ├── 02类模板.md │ ├── 10了解与利用SFINAE.md │ ├── 11约束与概念.md │ └── 01函数模板.md ├── README.md ├── 第二部分-造轮子 │ ├── 使用模板包装C风格API进行调用.md │ └── Linux中封装POSIX接口编写thread类.md └── 扩展知识 │ └── CRTP的原理与使用.md ├── .gitignore ├── README.md ├── SUMMARY.md └── LICENSE /code/02include指令/array.txt: -------------------------------------------------------------------------------- 1 | 1,2,3,4,5 -------------------------------------------------------------------------------- /code/01模板分文件/test.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void f(); -------------------------------------------------------------------------------- /code/01模板分文件/test.cpp: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | void f() {} -------------------------------------------------------------------------------- /code/01模板分文件/test_template.cpp: -------------------------------------------------------------------------------- 1 | template 2 | void f_t(T) {} -------------------------------------------------------------------------------- /code/01模板分文件/test_template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | template 4 | void f_t(T); -------------------------------------------------------------------------------- /code/02include指令/main2.cpp: -------------------------------------------------------------------------------- 1 | int main(){ 2 | int arr[] = { 3 | #include"array.txt" 4 | }; 5 | } -------------------------------------------------------------------------------- /image/icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/Modern-Cpp-templates-tutorial/HEAD/image/icon.webp -------------------------------------------------------------------------------- /image/捐赠/支付宝10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/Modern-Cpp-templates-tutorial/HEAD/image/捐赠/支付宝10.jpg -------------------------------------------------------------------------------- /image/捐赠/支付宝20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/Modern-Cpp-templates-tutorial/HEAD/image/捐赠/支付宝20.jpg -------------------------------------------------------------------------------- /code/03类模板分文件/test_class.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct X{ 6 | void f(); 7 | }; -------------------------------------------------------------------------------- /image/捐赠/支付宝88.88.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/Modern-Cpp-templates-tutorial/HEAD/image/捐赠/支付宝88.88.jpg -------------------------------------------------------------------------------- /code/02include指令/1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/Modern-Cpp-templates-tutorial/HEAD/code/02include指令/1.txt -------------------------------------------------------------------------------- /code/03类模板分文件/test_class.cpp: -------------------------------------------------------------------------------- 1 | #include "test_class.h" 2 | 3 | void X::f(){ 4 | std::cout << "X::f\n"; 5 | } 6 | -------------------------------------------------------------------------------- /code/03类模板分文件/test_class_template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | template 6 | struct X2{ 7 | void f(); 8 | }; -------------------------------------------------------------------------------- /code/01模板分文件/main.cpp: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | #include "test_template.h" 3 | 4 | int main(){ 5 | f(); // 非模板,OK 6 | f_t(1); // 模板 链接错误 7 | } -------------------------------------------------------------------------------- /code/04显式实例化解决模板分文件问题/test_function_template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | void f_t(T); -------------------------------------------------------------------------------- /code/03类模板分文件/test_class_template.cpp: -------------------------------------------------------------------------------- 1 | #include "test_class_template.h" 2 | 3 | template 4 | void X2::f(){ 5 | std::cout << "X2::f\n"; 6 | } -------------------------------------------------------------------------------- /homework/README.md: -------------------------------------------------------------------------------- 1 | # 提交方式 2 | 3 | 直接 [fork](https://github.com/Mq-b/Modern-Cpp-templates-tutorial/fork) 本仓库。在本地先新建分支,转到分支,在 homework 文件夹中的具体题目文件夹中新建自己的文件或文件夹,写好后,提出 pr。 4 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/12总结.md: -------------------------------------------------------------------------------- 1 | # 总结 2 | 3 |   模板的知识远不止如此,希望各位以此为起点,尽情探索现代 C++ 模板的作用。本文档长期更新,如有任何内容的问题,欢迎访问 [Github 仓库](https://github.com/Mq-b/Modern-Cpp-templates-tutorial)。 4 | -------------------------------------------------------------------------------- /code/生成动态库/生成动态库.vcxproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /code/生成静态库/生成静态库.vcxproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /code/测试使用静态库/测试使用静态库.vcxproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /code/生成静态库/export_template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | void f(T); 8 | 9 | template 10 | struct X { 11 | void f(); 12 | }; -------------------------------------------------------------------------------- /code/测试使用静态库/export_template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | void f(T); 8 | 9 | template 10 | struct X { 11 | void f(); 12 | }; -------------------------------------------------------------------------------- /code/05显式实例化解决模板导出动态静态库问题/05显式实例化解决模板导出动态静态库问题.vcxproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /code/生成动态库/export_template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | void f(T); 8 | 9 | template 10 | struct __declspec(dllexport) X { 11 | void f(); 12 | }; -------------------------------------------------------------------------------- /code/测试使用静态库/run_test.cpp.cpp: -------------------------------------------------------------------------------- 1 | #include "export_template.h" 2 | #include 3 | 4 | 5 | int main(){ 6 | std::string s; 7 | f(1); 8 | //f(1.2); // Error!链接错误,没有这个符号 9 | f(s); 10 | Xx; 11 | x.f(); 12 | } -------------------------------------------------------------------------------- /code/05显式实例化解决模板导出动态静态库问题/run_test.cpp.cpp: -------------------------------------------------------------------------------- 1 | #include "export_template.h" 2 | #include 3 | 4 | int main(){ 5 | std::string s; 6 | f(1); 7 | //f(1.2); // Error!链接错误,没有这个符号 8 | f(s); 9 | Xx; 10 | x.f(); 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vitepress/cache/ 3 | .vitepress/dist/ 4 | .vscode 5 | .vs 6 | debug/ 7 | bin/ 8 | out/ 9 | build/ 10 | *.pdb 11 | *.exe 12 | *.lik 13 | *.dll 14 | *.log 15 | *.tlog 16 | *.exp 17 | *.ilk 18 | *.obj 19 | *.lib 20 | *.APS -------------------------------------------------------------------------------- /code/05显式实例化解决模板导出动态静态库问题/export_template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | void f(T); 8 | 9 | template 10 | struct __declspec(dllexport) X { 11 | void f(); 12 | }; -------------------------------------------------------------------------------- /code/02include指令/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(){ 4 | int arr[] = { 5 | #include"array.txt" 6 | }; 7 | for(int i = 0; i < sizeof(arr)/sizeof(int); ++i) 8 | std::cout<< arr[i] <<' '; 9 | std::cout<<'\n'; 10 | } -------------------------------------------------------------------------------- /code/02include指令/README.md: -------------------------------------------------------------------------------- 1 | ## GCC 指令 2 | 3 | **第一个示例** 4 | 5 | 编译,指定输出的可执行文件名: 6 | >`g++ main.cpp -o main` 7 | 8 | 执行可执行文件: 9 | > `./main` 10 | 11 | **第二个示例** 12 | 13 | 在终端显示预处理后的效果: 14 | 15 | >`g++ -E main2.cpp` 16 | 17 | 重定向到文件中: 18 | 19 | > `g++ -E main2.cpp > 1.txt` -------------------------------------------------------------------------------- /code/03类模板分文件/main.cpp: -------------------------------------------------------------------------------- 1 | #include "test_class.h" 2 | #include "test_class_template.h" 3 | 4 | int main(){ 5 | X x; 6 | x.f(); // 普通类没问题 7 | 8 | X2 x2; // 类模板没问题 9 | //x2.f(); // 链接错误 10 | } 11 | 12 | /*为什么我们可以使用类模板呢?因为其实类模板又没有真的分声明和定义(也做不到),它是在h文件定义的 13 | * 所以我们可以使用类模板,但是却没办法调用它的成员函数,因为它这个的确只有声明,实现在 cpp 文件。 14 | */ -------------------------------------------------------------------------------- /code/04显式实例化解决模板分文件问题/test_function_template.cpp: -------------------------------------------------------------------------------- 1 | #include"test_function_template.h" 2 | 3 | template 4 | void f_t(T) { std::cout << typeid(T).name() << '\n'; } 5 | 6 | template void f_t(int); // 显式实例化定义 实例化 f_t(int) 7 | template void f_t<>(char); // 显式实例化定义 实例化 f_t(char),推导出模板实参 8 | template void f_t(double); // 显式实例化定义 实例化 f_t(double),推导出模板实参 -------------------------------------------------------------------------------- /code/生成静态库/export_template.cpp: -------------------------------------------------------------------------------- 1 | #include "export_template.h" 2 | 3 | template 4 | void f(T) { 5 | std::cout << typeid(T).name() << '\n'; 6 | } 7 | 8 | template 9 | void X::f(){ 10 | std::cout << typeid(T).name() << '\n'; 11 | } 12 | 13 | template void f(int); 14 | template void f(std::string); 15 | 16 | template struct X; -------------------------------------------------------------------------------- /image/捐赠/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | cpp 4 | 5 | cpp 6 | 7 | cpp 8 | 9 |
10 | 11 | --- 12 | 13 |   我们会收集捐赠者进行感谢,所以请您捐赠了可以选择备注,或者联系我,或者直接在[**捐赠初始记录名单**](https://github.com/Mq-b/Modern-Cpp-templates-tutorial/discussions/5)中进行评论。 14 | -------------------------------------------------------------------------------- /code/04显式实例化解决模板分文件问题/test_class_template.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace N { 7 | 8 | template 9 | struct X { 10 | int a{}; 11 | void f(); 12 | void f2(); 13 | }; 14 | 15 | template 16 | struct X2 { 17 | int a{}; 18 | void f(); 19 | void f2(); 20 | }; 21 | }; -------------------------------------------------------------------------------- /code/生成动态库/export_template.cpp: -------------------------------------------------------------------------------- 1 | #include "export_template.h" 2 | 3 | template 4 | void f(T) { 5 | std::cout << typeid(T).name() << '\n'; 6 | } 7 | 8 | template 9 | void X::f(){ 10 | std::cout << typeid(T).name() << '\n'; 11 | } 12 | 13 | template __declspec(dllexport) void f(int); 14 | template __declspec(dllexport) void f(std::string); 15 | 16 | template struct X; -------------------------------------------------------------------------------- /code/01模板分文件/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project ("template") # 设置项目名称 2 | 3 | cmake_minimum_required (VERSION 3.8) # 设置 cmake 版本 4 | 5 | set(CMAKE_CXX_STANDARD 23) # 设置 C++ 标准 6 | 7 | SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) # 设置输出文件的目录 8 | 9 | add_executable (template "main.cpp" "test.cpp" "test_template.cpp") # 设置参与编译的翻译单元 10 | 11 | -------------------------------------------------------------------------------- /code/03类模板分文件/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 指定 cmake 最小版本号 2 | cmake_minimum_required (VERSION 3.8) 3 | 4 | # 指定项目名称 5 | 6 | project ("class_template") 7 | 8 | # 设置exe的输出目录${PROJECT_SOURCE_DIR}就是当前顶级目录,然后创建bin文件夹里生成exe 9 | SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) 10 | 11 | # 将源代码添加到此项目的可执行文件 12 | add_executable (class_template "main.cpp" "test_class_template.cpp" "test_class.cpp") 13 | 14 | if (CMAKE_VERSION VERSION_GREATER 3.12) 15 | set_property(TARGET class_template PROPERTY CXX_STANDARD 20) 16 | endif() -------------------------------------------------------------------------------- /code/04显式实例化解决模板分文件问题/main.cpp: -------------------------------------------------------------------------------- 1 | #include "test_function_template.h" 2 | #include "test_class_template.h" 3 | 4 | int main() { 5 | f_t(1); 6 | f_t(1.2); 7 | f_t('c'); 8 | //f_t("1"); // 没有显式实例化 f_t 版本,会有链接错误 9 | 10 | N::Xx; 11 | x.f(); 12 | //x.f2(); // 链接错误,没有显式实例化 X::f2() 成员函数 13 | N::Xx2{}; 14 | //x2.f(); // 链接错误,没有显式实例化 X::f() 成员函数 15 | 16 | N::X2x3; // 我们显式实例化了类模板 X2 也就自然而然实例化它所有的成员,f,f2 函数 17 | x3.f(); 18 | x3.f2(); 19 | 20 | // 类模板分文件 我们写了两个类模板 X X2,它们一个使用了成员函数显式实例化,一个类模板显式实例化,进行对比 21 | // 这主要在于我们所谓的类模板分文件,其实类模板定义还是在头文件中,只不过成员函数定义在 cpp 罢了。 22 | } -------------------------------------------------------------------------------- /code/04显式实例化解决模板分文件问题/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 指定 cmake 最小版本号 2 | cmake_minimum_required (VERSION 3.8) 3 | 4 | # 指定项目名称 5 | 6 | project ("explicit_instantiation") 7 | 8 | # 设置exe的输出目录${PROJECT_SOURCE_DIR}就是当前顶级目录,然后创建bin文件夹里生成exe 9 | SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) 10 | 11 | # 将源代码添加到此项目的可执行文件 12 | add_executable (explicit_instantiation "main.cpp" "test_function_template.cpp" "test_class_template.cpp") 13 | 14 | # 如果是 MSVC,就设置编译选项,为了解决 windows 全局未设置 utf8 用户 VS 打开项目的编码问题 15 | if(MSVC) 16 | target_compile_options(explicit_instantiation PRIVATE "/utf-8") 17 | endif() 18 | 19 | if (CMAKE_VERSION VERSION_GREATER 3.12) 20 | set_property(TARGET explicit_instantiation PROPERTY CXX_STANDARD 20) 21 | endif() -------------------------------------------------------------------------------- /code/04显式实例化解决模板分文件问题/test_class_template.cpp: -------------------------------------------------------------------------------- 1 | #include "test_class_template.h" 2 | 3 | template 4 | void N::X::f(){ 5 | std::cout << "f: " << typeid(T).name() << "a: " << this->a << '\n'; 6 | } 7 | 8 | template 9 | void N::X::f2() { 10 | std::cout << "f2: " << typeid(T).name() << "a: " << this->a << '\n'; 11 | } 12 | 13 | template void N::X::f(); // 显式实例化定义 成员函数,这不是显式实例化类模板 14 | 15 | template 16 | void N::X2::f() { 17 | std::cout << "X2 f: " << typeid(T).name() << "a: " << this->a << '\n'; 18 | } 19 | 20 | template 21 | void N::X2::f2() { 22 | std::cout << "X2 f2: " << typeid(T).name() << "a: " << this->a << '\n'; 23 | } 24 | 25 | template struct N::X2; // 类模板显式实例化定义 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | cpp 4 | 5 | # 现代 C++ 模板教程 6 | 7 | 本仓库用来存放 B 站课程 [**《现代 C++ 模板教程 2024》**](https://www.bilibili.com/cheese/play/ss12852)的教案、代码,和**作业**。 8 | 9 | 任何组织和个人遵守 [CC BY-NC-ND 4.0](LICENSE) 协议即可随意使用。 10 | 11 | [捐赠](/image/捐赠)、issues、pr 均会在 **[致谢列表](/image/捐赠/thanks.md)中铭记您的贡献**。 12 | 13 |
14 | 15 | --- 16 | 17 |   国内众多 C++ 教程古老且过时,学校教学则更是过分。我们需要新式的,教学符合时代的知识、代码风格、思维的课程! 18 | 19 |   本教程创新性的采用学习 + [提交作业](/homework/README.md)的方式,您需要视频学习后提交作业,而我们会进行批改和评论。 20 | 21 |   本教程假设读者的最低水平为:`C + class + STL`。 22 | 23 |   虽强调现代 C++,但同时对于老式模板写法也都会进行提及和教学。**因为不体会老式语法和写法的折磨,没有办法理解新特性的价值与意义**。 24 | 25 | 请确保您的编译器至少支持 C++20,优先使用 gcc13,clang16,msvc v19.latest。所有代码均测试三大编译器。 26 | -------------------------------------------------------------------------------- /homework/08折叠表达式作业/ooolize.md: -------------------------------------------------------------------------------- 1 | 7 | ### 题目 8 | 9 | ```c++ 10 | template 11 | auto Reverse(Args&&... args) { 12 | std::vector> res{}; 13 | bool tmp{ false }; 14 | (tmp = ... = (res.push_back(args), false)); 15 | return res; 16 | } 17 | ``` 18 | 19 | ### 作用 20 | 21 | 返回一个包含参数包逆序的vector 22 | 23 | ### 解析 24 | 25 | + ...在参数包左边 所以是一个 **左折叠**,其中 ```= ... =``` 符合二元的形式, 所以是一个**二元左折叠**。 26 | 27 | + 我们知道逗号表达式的值是**最右侧**子表达式的值. 所以```(res.push_back(args), false))```值一定是false。 28 | 29 | + 所以套一下公式```((((I 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN)```.展开如下: 30 | 31 | ``` 32 | ((((false = E1) = E2) = ...) = EN) // 实际EX的值都是false 33 | ``` 34 | 35 | + 我们知道赋值运算符的执行顺序是**从右向左**,所以按EN...E2,E1的顺序依次执行。自然就会将参数包中传入参数的逆序返回。 36 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [介绍](README.md) 4 | * [阅读须知](md/README.md) 5 | * [函数模板](md/第一部分-基础知识/01函数模板.md) 6 | * [类模板](md/第一部分-基础知识/02类模板.md) 7 | * [变量模板](md/第一部分-基础知识/03变量模板.md) 8 | * [模板全特化](md/第一部分-基础知识/04模板全特化.md) 9 | * [模板偏特化](md/第一部分-基础知识/05模板偏特化.md) 10 | * [模板显式实例化解决模板分文件问题](md/第一部分-基础知识/06模板显式实例化解决模板分文件问题.md) 11 | * [显式实例化解决模板导出静态动态库](md/第一部分-基础知识/07显式实例化解决模板导出静态动态库.md) 12 | * [折叠表达式](md/第一部分-基础知识/08折叠表达式.md) 13 | * [待决名](md/第一部分-基础知识/09待决名.md) 14 | * [了解与利用 SFINAE](md/第一部分-基础知识/10了解与利用SFINAE.md) 15 | * [约束与概念](md/第一部分-基础知识/11约束与概念.md) 16 | * [作业提交展示](homework/README.md) 17 | * [mq卢瑟](homework/08折叠表达式作业/mq卢瑟.md) 18 | * [rosevehif](homework/08折叠表达式作业/roseveknif.md) 19 | * [mq日](homework/08折叠表达式作业/mq日.md) 20 | * [ooolize](homework/08折叠表达式作业/ooolize.md) 21 | * [saidfljwnzjasf](homework/08折叠表达式作业/saidfljwnzjasf.md) 22 | * [总结](md/第一部分-基础知识/12总结.md) 23 | -------------------------------------------------------------------------------- /homework/08折叠表达式作业/mq日.md: -------------------------------------------------------------------------------- 1 | # 原题: 2 | 3 | ``` 4 | template 5 | auto Reverse(Args&&... args) { 6 | std::vector> res{}; 7 | bool tmp{ false }; 8 | (tmp = ... = (res.push_back(args), false)); 9 | return res; 10 | } 11 | ``` 12 | 13 | ## 解释: 14 | 15 | 首先看`Reverse(Args&&... args)` 名为`Reverse`的模板函数接受可变参数列表,可以接受任意个参数 16 | 17 | `std::vector> res{};` common_type_t确定所有参数的公共类型,将所有的参数转化为同一类型。用这个类型初始化了一个空的vector容器。 18 | 19 | `bool tmp{ false };` 初始化一个值为false的bool变量,后面用它来作为二元折叠的一部分 20 | 21 | `(tmp = ... = (res.push_back(args), false));` 这是一个二元左折叠,将每个args放入容器中 22 | 23 | > 这里的折叠展开来应当是: 24 | > 25 | > tmp=(res.push_back(arg0),flase)=(res.push_back(arg1),flase)...=(res.push_back(argn),flase) 26 | 27 | 逗号运算符返回最后一个表达式,这里的flase应该是作为表达式的值,实际上没什么用,只是为了用这个折叠表达式。 28 | 29 | `return res;` 最后将容器返回; 30 | -------------------------------------------------------------------------------- /md/README.md: -------------------------------------------------------------------------------- 1 | # 阅读须知 2 | 3 |   **本教程假设读者的最低水平为:`C + class + STL`**。 4 | 5 |   那么自然而然的,就不可能从一开始就讲的很深入,即使我完全清楚有各种例外和情况,我们需要考虑一个几乎是新人的开发者,对这些繁杂的规则的接受程度。我们要循序渐进。所以我会省略大量的规则和情况。 6 | 7 | ## 模板很有用吗?学习模板能带来什么? 8 | 9 | 显然,模板很有用,作为 C++ 中最大的一类语言特性,几乎自 C++ 诞生以来就开始演变。你可以在几乎所有的 C++ 开源库中看到模板,以及你的 C++ 标准库,几乎全部使用模板。 10 | 11 | 至于能带来什么?只有成功学大师和一些 xxx 才能明确的告诉你,只能说因人而异。你如果想要学了之后迎娶白富美走向人生巅峰,那还是算了吧 ~~。 12 | 13 | 可能很多人会有疑问以及各种说法,比如: 14 | 15 | > 我工作基本没写过模板,不需要特意去学这个 16 | 17 | 的确有一些开发者的工作几乎不使用模板,这些也都很正常(毕竟说自己用 C with class 的也一堆),以及维护上古项目,这里不过多评价。是否学习,完全是看自己的想法,没有人能强迫别人学习。如果你对自己有一定的要求和个人追求,那么模板,非学不可。 18 | 19 | - **如果不能熟练使用模板,那么阅读标准库、开源项目、其他三方库的代码,几乎是无稽之谈**。 20 | 21 | > 模板是高手用的,很难,普通开发者没必要学 22 | 23 | 其实说到底,模板也只是 C++ 的一类语法罢了,无非还是一些基本形式,大部分的使用并不会有多少难度,只是某些东西一起使用可能会涉及很多繁杂的规则。 24 | 25 | 说一个最常见的需求,**泛型**,难道你不使用 C++ 的模板,还去使用 C 的 `void*` 去完成泛型? 26 | 27 | ## 应该如何学习模板? 28 | 29 | 多用,用的越多越熟练,没有需求那就给自己创造需求。 30 | 31 | 最大的需求是什么?阅读标准库代码。 32 | 33 | 各位完全可以养成一个习惯,不管使用任何的库,有事没事右键点进去看看实现,自然而然的会思考,会有进步,当然了,循序渐进。 34 | -------------------------------------------------------------------------------- /homework/08折叠表达式作业/提莫大绅士.md: -------------------------------------------------------------------------------- 1 | ## Qustion 2 | 3 | > 以下代码使用的折叠表达式语法,以及它的效果,详细解析,使用 Markdown 语法。 4 | 5 | ```cpp 6 | template 7 | auto Reverse(Args&&... args) { 8 | std::vector> res{}; 9 | bool tmp{ false }; 10 | (tmp = ... = (res.push_back(args), false)); 11 | return res; 12 | } 13 | ``` 14 | 15 | ## Answer 16 | 17 | ### 二元左折叠 18 | 19 | 1. 二元折叠,因为`= ... =` 符合 `(运算符 ... 运算符)` 的形式 20 | 2. 左折叠,因为 `...` 在形参包(args)左边 21 | 22 | ### 详细解析 23 | 24 | > 二元左折叠语法,根据`二元左折叠 (I 运算符 ... 运算符 E) 成为 ((((I 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN) (其中 N 是包展开中的元素数量)` 25 | 26 | - 运算符是`=` 27 | - 初值是`tmp` 28 | - 表达式为`(res.push_back(args), false)` 29 | 30 | ### 执行效果 31 | 以样例数据展开表达式 32 | ```cpp 33 | auto res = Reverse(1, 2, 3); // (((tmp = (res.push_back(1), false))= (res.push_back(2), false))= (res.push_back(3), false)) 34 | ``` 35 | 36 | 而`(((tmp = (res.push_back(1), false))= (res.push_back(2), false))= (res.push_back(3), false))`这个表达式,会先执行`res.push_back(3), false`,然后再执行`res.push_back(2), false`,最后再执行`res.push_back(1), false`。所以最后执行结果为 `res{3,2,1}`; 37 | -------------------------------------------------------------------------------- /code/测试使用静态库/测试使用静态库.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | 源文件 20 | 21 | 22 | 23 | 24 | 头文件 25 | 26 | 27 | -------------------------------------------------------------------------------- /code/生成动态库/生成动态库.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | 头文件 20 | 21 | 22 | 23 | 24 | 源文件 25 | 26 | 27 | -------------------------------------------------------------------------------- /code/生成静态库/生成静态库.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | 头文件 20 | 21 | 22 | 23 | 24 | 源文件 25 | 26 | 27 | -------------------------------------------------------------------------------- /code/05显式实例化解决模板导出动态静态库问题/05显式实例化解决模板导出动态静态库问题.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | 源文件 20 | 21 | 22 | 23 | 24 | 头文件 25 | 26 | 27 | -------------------------------------------------------------------------------- /homework/08折叠表达式作业/我是男孩.md: -------------------------------------------------------------------------------- 1 | # 原题 2 | 3 | ```cpp 4 | template 5 | auto Reverse(Args&&... args) { 6 | std::vector> res{}; 7 | bool tmp{false}; 8 | (tmp = ... = (res.push_back(args), false)); 9 | return res; 10 | } 11 | ``` 12 | 13 | *简述代码使用的折叠表达式的语法、效果、详细解析。* 14 | 15 | 16 | 17 | # 作业 18 | 19 | #### 分析 20 | 21 | ```cpp 22 | (tmp = ... = (res.push_back(args), false)); 23 | ``` 24 | 25 | 代码中存在`= ... =`,符合`运算符 ... 运算符`形势,即为二元折叠。又因`...`在“形参包(args)”左边,折叠表达式为一个二元左折叠。 26 | 27 | #### 根据教案 28 | 29 | > 二元左折叠`(I 运算符 ... 运算符 E)`成为`((((I 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN)`。 30 | 31 | #### 展开为 32 | 33 | ```cpp 34 | ((((tmp = (res.push_back(args0), false)) = (res.push_back(args1), false)) = ...) = (res.push_back(argsN), false)); 35 | ``` 36 | 37 | #### 参考以下调用 38 | 39 | ```cpp 40 | auto vec = Reverse(3.5, 1, 2); 41 | 42 | for (auto& it : vec) { 43 | cout << it << ", "; 44 | } 45 | ``` 46 | 47 | **折叠表达式展开为** 48 | 49 | ```cpp 50 | (((tmp = (res.push_back(3.5), false)) = (res.push_back(1), false)) = (res.push_back(2), false)); 51 | ``` 52 | 53 | 赋值表达式从右向左,所以从后向前push到res。逗号表达式返回右边的false,tmp为flase(没什么用)。 54 | 55 | **输出为** 56 | 57 | `2, 1, 3.5` -------------------------------------------------------------------------------- /homework/08折叠表达式作业/roseveknif.md: -------------------------------------------------------------------------------- 1 | ## 折叠表达式作业 2 | 3 | 题目如下: 4 | 5 | ```cpp 6 | template 7 | auto Reverse(Args&&... args) { 8 | std::vector> res{}; 9 | bool tmp{ false }; 10 | (tmp = ... = (res.push_back(args), false)); 11 | return res; 12 | } 13 | ``` 14 | 15 | 首先**看函数名也知道是逆序传入的参数包**,主要是分析代码。 16 | 17 | --- 18 | 19 | 考虑以下调用: 20 | 21 | ```cpp 22 | auto vec = Reverse(1, 2, 3, 4.); 23 | for (const auto n : vec) { 24 | std::cout << n << ' '; 25 | } 26 | ``` 27 | 28 | 对于 29 | 30 | ```cpp 31 | std::vector> res{}; 32 | ``` 33 | 34 | `std::common_type_t` 用于寻找传入类型的公共类型,这里传入的是 3个 int 以及 1个 double 类型参数,故 res 的类型是 `std::vector` 35 | 36 | 对于 37 | 38 | ```cpp 39 | bool tmp{ false }; 40 | (tmp = ... = (res.push_back(args), false)); 41 | ``` 42 | 43 | 首先定义一个 bool 变量,单纯用于后续包展开。 44 | 45 | 观察折叠表达式,按照**国王的教诲**,观察到 `= ... =` ,其次 `...` 在参数包 `args` 左侧,故该表达式是**二元左折叠表达式**。 46 | 47 | 展开后大致为: 48 | 49 | ```cpp 50 | ((((tmp = (res.push_back(1), false)) = (res.push_back(2), false)) = (res.push_back(3), false)) = (res.push_back(4.), false)); 51 | ``` 52 | 53 | 考虑 [求值顺序](https://en.cppreference.com/w/cpp/language/eval_order), 54 | 55 | 对于 `,` 运算符,从左到右求值,这里只是 `push_back(arg)` 并且返回 false 给左侧整体进行赋值。 56 | 57 | 对于 `=` 运算符,右侧的值计算和副作用先于左侧。所以该表达式整体也从右侧开始执行,按传入的参数包逆序 `push_back` 入 `res`。 58 | 59 | 最后的输出是: 60 | 61 | ``` 62 | 4 3 2 1 63 | ``` 64 | -------------------------------------------------------------------------------- /homework/08折叠表达式作业/saidfljwnzjasf.md: -------------------------------------------------------------------------------- 1 | ### 题目 2 | >说出以下代码使用的折叠表达式语法,以及它的效果,详细解析,使用 Markdown 语法。 3 | >```cpp 4 | >template 5 | >auto Reverse(Args&&... args) { 6 | > std::vector> res{}; 7 | > bool tmp{ false }; 8 | > (tmp = ... = (res.push_back(args), false)); 9 | > return res; 10 | >} 11 | >``` 12 | 13 | ### 解析 14 | 首先我们找到折叠表达式所在的语句 15 | ```cpp 16 | (tmp = ... = (res.push_back(args), false)); 17 | ``` 18 | 这里表达式符合 `( 初值 运算符 ... 运算符 形参包 )` 这个形式,属于二元左折叠。 19 | 那么表达式会被展开成下面这种形式的语句: 20 | ```cpp 21 | ((((tmp = (res.push_back(args[0]), false)) = (res.push_back(args[1]), false)) = (...)) = (res.push_back(args[N-1]), false)) = (res.push_back(args[N]), false); 22 | ``` 23 | 现在我们知道展开后的表达式语句长什么样子了,但是这个语句是如何先让 `args[N]` 被 push_back 到 `res` 中呢? 24 | 因为这里的 `=` 是一个简单赋值,又因为: 25 | >每个简单赋值表达式 E1 = E2 和每个复合赋值表达式 E1 @= E2 中,E2 的每个值计算和副作用都按顺序早于 E1 的每个值计算和副作用。 26 | 27 | 所以这里会先追加 `args[N]` 到 `res` 容器尾。 28 | 其实这里我不确定是不是这个原因,因为在 cppreference 里又提到: 29 | >对于优先级相同的运算符: 30 | > 31 | >拥有相同优先级的运算符以其结合性的方向与各参数绑定。例如表达式 `a = b = c` 会被分析为 `a = (b = c)` 而非 `(a = b) = c`,因为赋值具有从右到左结合性 32 | 33 | 而语句中的 `,` 运算符被包裹在`()`中以保证优先级高于 `=` 运算符,并且让作为左实参的 `std::vector::push_back` 调用作为弃值表达式来实施它的副作用,让 `flase` 完成一个合法的简单赋值。 34 | 35 | 用 C++ Insights [验证](https://cppinsights.io/s/1b5d6eab)一下我们的想法。在 C++ Insights 中我们得到了如下结果: 36 | >```cpp 37 | >template<> 38 | >std::vector > Reverse(int && __args0, int && __args1, int && __args2, int && __args3) 39 | >{ 40 | > std::vector > res = std::vector >{}; 41 | > bool tmp = {false}; 42 | > (((tmp = (res.push_back(__args0) , false)) = (res.push_back(__args1) , false)) = (res.push_back(__args2) , false)) = (res.push_back(__args3) , false); 43 | > return std::vector >(static_cast > &&>(res)); 44 | >} 45 | >int main() 46 | >{ 47 | > std::vector > v = Reverse(1, 2, 3, 4); 48 | > return 0; 49 | >} 50 | >``` 51 | 可以看到,这里对于折叠表达式的展开和刚刚的结论是一致的。 52 | -------------------------------------------------------------------------------- /homework/08折叠表达式作业/mq卢瑟.md: -------------------------------------------------------------------------------- 1 | mq卢瑟的作业如下: 2 | 3 | 首先作业题目是: 4 | 5 | ```c++ 6 | // 说出以下代码使用的折叠表达式语法,以及它的效果,详细解析,使用 Markdown 语法。 7 | template 8 | auto Reverse(Args&&... args) { 9 | std::vector> res{}; 10 | bool tmp{ false }; 11 | (tmp = ... = (res.push_back(args), false)); 12 | return res; 13 | } 14 | ``` 15 | 16 | 假设 main 函数按照下面方式调用 Reverse 函数: 17 | 18 | ```c++ 19 | auto arr = Reverse(1.1, 3); 20 | ``` 21 | --- 22 | 23 | 对于代码 `std::vector> res{};` ,解释如下: 24 | 25 | > `01函数模板.md` 中讲过" std::common_type_t 的作用很简单,就是确定我们传入的共用类型,说白了就是这些东西都能隐式转换到哪个,那就会返回那个类型。" 26 | > 所以,根据我main函数中传入的"1.1,3",这里res的类型是 `std::vector` 27 | 28 | 对于代码 `(tmp = ... = (res.push_back(args), false));` ,解释如下: 29 | 30 | ... 在 形参包左侧,所以这是一个二元左折叠表达式,根据下面的展开方式: 31 | 32 | `((((I 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN)` 33 | 34 | 实例化展开后是这样: 35 | 36 | ```c++ 37 | std::vector Reverse(double && args0, int && args1) 38 | { 39 | std::vector res = std::vector{}; 40 | bool tmp = {false}; 41 | (tmp = (res.push_back(args0) , false)) = (res.push_back(args1) , false); 42 | return res; 43 | } 44 | ``` 45 | 46 | > 注:这里的 `tmp` 纯属是为了创造一个符合语法的折叠表达式展开效果,它本身并没有什么其他意义。 47 | 48 | 根据: 49 | loserhomework 的卢瑟日经 [赋值运算符求值问题](https://github.com/Mq-b/Loser-HomeWork/blob/main/src/%E5%8D%A2%E7%91%9F%E6%97%A5%E7%BB%8F/%E8%B5%8B%E5%80%BC%E8%BF%90%E7%AE%97%E7%AC%A6%E6%B1%82%E5%80%BC%E9%A1%BA%E5%BA%8F%E9%97%AE%E9%A2%98.md) 中提到的: 50 | > 每个简单赋值表达式 E1 = E2 和每个复合赋值表达式 E1 @= E2 中,E2 的每个值计算和副作用都按顺序早于 E1 的每个值计算和副作用。 51 | 52 | `(tmp = (res.push_back(args0) , false)) = (res.push_back(args1) , false);` 这条语句可以看成是 E1 = E2 类型的赋值表达式。其中: 53 | E1 是 `(tmp = (res.push_back(args0) , false))` 54 | E2 是 `(res.push_back(args1) , false)` 55 | 因此 E2 : `(res.push_back(args1) , false)` 先被计算。 56 | > 注意,这是一个逗号表达式,根据"逗号表达式是从左往右执行的,返回最右边的值作为整个逗号表达式的值",所以 args1 会先被 push_back 到 res 这个容器中,然后返回 false 。 57 | 58 | 对于 E1 : `(tmp = (res.push_back(args0) , false))` ,它也可以看成是 E1 = E2 类型的赋值表达式,同上。 59 | 60 | 所以容器中的元素顺序是:3 , 1.1。 61 | 62 | ***所以该函数是将传入的参数进行逆序。*** 63 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/07显式实例化解决模板导出静态动态库.md: -------------------------------------------------------------------------------- 1 | # 显式实例化解决模板导出静态库动态库 2 | 3 | ## 前言 4 | 5 | 我们使用显式实例化解决模板导出动态静态库这个问题,原因什么的不再过多赘述,总结一句话: 6 | 7 | - **模板需要实例化才能生成实际的代码,既然不能隐式实例化,那就显式实例化**。 8 | 9 | 我们以 `windows` 环境 `VS2022`,`sln` 解决方案为例。 10 | 11 | > 不再使用 cmake 主要是本课程也不是教你 cmake 的,这些东西有点麻烦,不如直接用 VS 按钮点点点设置就是。之前用 cmake 项目主要在于 **`add_executable`**,比较明确,说明编译哪些文件。 12 | 13 | 我们创建了一个[解决方案](/code/05显式实例化解决模板导出动态静态库问题/05显式实例化解决模板导出动态静态库问题.sln),其中有四个项目: 14 | 15 | 1. [**测试动态库使用**](/code/05显式实例化解决模板导出动态静态库问题/) 16 | 2. [**测试静态库使用**](/code/测试使用静态库/) 17 | 3. [**用模板生成动态库**](/code/生成动态库/) 18 | 4. [**用模板生成静态库**](/code/生成静态库/) 19 | 20 | > 生成动态库和静态库用的代码几乎是一模一样的,只是去掉了 `__declspec(dllexport)`。 21 | 22 | > 测试动态库和静态库使用的主要区别在于项目配置,代码上的区别是去掉 `__declspec(dllexport)`。 23 | 24 | ## 模板生成动态库与测试 25 | 26 | [**`export_template.h`**](/code/生成动态库/export_template.h) 27 | 28 | ```cpp 29 | #pragma once 30 | 31 | #include 32 | #include 33 | 34 | template 35 | void f(T); 36 | 37 | template 38 | struct __declspec(dllexport) X { 39 | void f(); 40 | }; 41 | ``` 42 | 43 | [**`export_template.cpp`**](/code/生成动态库/export_template.cpp) 44 | 45 | ```cpp 46 | #include "export_template.h" 47 | 48 | template 49 | void f(T) { // 函数模板定义 50 | std::cout << typeid(T).name() << '\n'; 51 | } 52 | 53 | template 54 | void X::f(){ // 类模板中的成员函数模板定义 55 | std::cout << typeid(T).name() << '\n'; 56 | } 57 | 58 | template __declspec(dllexport) void f(int); 59 | template __declspec(dllexport) void f(std::string); 60 | 61 | template struct X; // 类模板显式实例化 62 | ``` 63 | 64 | 这很简单,和之前分文件写法的区别只是用了[`__declspec(dllexport)`](https://learn.microsoft.com/zh-cn/cpp/build/exporting-from-a-dll-using-declspec-dllexport?view=msvc-170)。 65 | 66 | > 可以使用 __declspec(dllexport) 关键字从 DLL 中导出数据、函数、类或类成员函数。 67 | 68 | 以上示例中的函数模板、类模板**显式实例化**,不要放在 `.h` 文件中,因为 69 | **一个显式实例化定义在程序中最多只能出现一次**;如果放在 `.h` 文件中,被多个翻译单元使用,就会产生问题。 70 | 71 | > 当显式实例化函数模板、变量模板 (C++14 起)、类模板的成员函数或静态数据成员,或成员函数模板时,**只需要它的声明可见**。 72 | 73 | > **类模板、类模板的成员类或成员类模板在显式实例化之前必须出现完整定义**,除非之前已经出现了拥有相同模板实参的显式特化 74 | 75 | 我们将生成的 `.dll` 与 `.lib` 文件放在了[指定目录](/code/05显式实例化解决模板导出动态静态库问题/lib/dll/)下,配置了项目的查找路径以供使用。 76 | 77 | [**`run_test.cpp.cpp`**](/code/05显式实例化解决模板导出动态静态库问题/run_test.cpp.cpp) 78 | 79 | ```cpp 80 | #include "export_template.h" 81 | #include 82 | 83 | int main(){ 84 | std::string s; 85 | f(1); 86 | //f(1.2); // Error!链接错误,没有这个符号 87 | f(s); 88 | Xx; 89 | x.f(); 90 | } 91 | ``` 92 | 93 | ## 模板生成静态库与测试 94 | 95 | 没有多大的区别,原理都一样,代码也差不多,不必再讲。 96 | 97 | ## 总结 98 | 99 | 我们没有讲述诸如:项目如何配置,**怎么配置项目生成动态库、静态库**,这也不是我们的重点。在视频中我们会从头构建这些。但是文本的话,不必要从头教学这些基本知识。 100 | 101 | 我们关注代码上即可。 102 | 103 | 静态库也没有单独提,因为的确没啥区别。 104 | 105 | 另外如果你直接打开我们的项目,无法编译或许很正常,请自己根据当前环境,处理编码问题,以及生成动态静态库,配置,使用。 106 | -------------------------------------------------------------------------------- /image/捐赠/thanks.md: -------------------------------------------------------------------------------- 1 | # 感谢赞助商 2 | 3 | --- 4 | 5 | 感谢赞助者对我们的项目给予的慷慨支持! 6 | 7 | - 他们的赞助让我们能够继续推动项目的发展,我们非常感激他们的支持! 8 | 9 | - 他们的慷慨赞助使我们能够实现更多目标,让我们深感欣慰。 10 | 11 | - 他们的支持让我们的项目得以顺利进行,我们对他们的贡献深表感谢。 12 | 13 | - 他们的慷慨赞助为我们提供了宝贵的支持,让我们能够更好地服务我们的社区。 14 | 15 | 以下是赞助商名单: 16 | 17 | - 1. [**golezi**](https://github.com/golezi/) 捐赠:**20** 18 | - 2. [**mq五彩斑斓**](https://github.com/DataEraserC/) 捐赠:**20** 19 | - 3. **edf282c6e3** 捐赠:**10** 20 | - 4. [**mq卢瑟**](https://github.com/mq-loser) 捐赠:**1208** 上贡:“`rk68`”、“`VGNs99`” 21 | - 5. [**akchilov**](https://github.com/AzrBrk) 捐赠:**20** 22 | - 6. [**心洗**](https://github.com/MrShinshi) 上贡:**468** 23 | - 7. [**mq松鼠**](https://github.com/CSTGluigi) 捐赠:**50** 24 | - 8. [**mq红**](https://github.com/somniumchase) 捐赠:**50** 25 | - 9. [**$_0x00f0**](https://github.com/S-0xff0f) 捐赠:**睿站季度大会员(68)** 26 | - 10. [**涼風青葉**](https://github.com/Suzukaze7) 上贡:**80** 27 | - 11. [**mq星**]() 捐赠:**10** 28 | - 12. [**mq眠**]() 捐赠:**320** 29 | - 13. **神秘人1** 捐赠:**300** 30 | - 14. **神秘人2** 捐赠:**10** 31 | - 15. [**hypatiy**]() 捐赠:**1128** 32 | - 16. [**mq舒**]() 捐赠:**50** 33 | - 17. [**Yuzhiy05**]() 捐赠:**700**(大约) 34 | - 18. [**MSC☆Xiphoillumination**]() 捐赠:**1600**(大约) 35 | - 19. [**Da'Inihlus**]() 捐赠:**50**(大约) 36 | - 20. [**拉达姆**]() 捐赠:**7** 37 | - 21. [**mq日**]() 捐赠:**8.8** 38 | - 22. [**日向花音**]() 给零花钱:**133.3** 39 | - 23. [**Schumacher RyoSukE**]() 赞助:**33.3** 40 | - 24. [**等疾风**](https://github.com/Codesire-Deng) 赞助:**50** 41 | - 25. [**白月光**]() 赞助:**6.66**。 42 | - 26. **不愿透露姓名的神秘人** 赞助:**500** 43 | - 27. [**PantheraLeo**](https://github.com/PantheraLeo14) 赞助:**45** 44 | - 28. [**噗叽**](https://github.com/puji4810) 纳税:**50** 45 | - 29. [**水夕**](https://github.com/sakria9) 上贡:**149** 46 | - 30. [**叠别拷打我**](https://github.com/Yuria-Shikibe) 赞助:**88.88** 47 | - 31. [**群饿龙🍔🍟🍹🍜🍗🥖🎂🥗**](https://github.com/StinkyTooFool) 赞助:**118.88** 48 | - 32. [**寻宝游戏**]() 赞助:**197.76** 49 | - 33. [**我是男孩**](https://github.com/zhuzhu9) 上贡:**80** 50 | - 34. [**卖女孩的小火柴**](https://github.com/weasyd) 捐赠:**10** 51 | - 35. [**saidfljwnzjasf**](https://github.com/KSARK) 捐赠:**118.88** 52 | - 36. [**懒狗**](https://github.com/RaDsZ2z) 上贡:**50** 53 | 54 | > 早期其实还有非常之多,多早呢,刚做视频的时候,有些人就是直接加我 qq 就给我转钱,我不知道谁是谁,以及还有一些大赞助行为,是早期,我也没记录,而且也不该算作本项目的。 55 | > - 神秘人:指不知道是谁,直接转钱,比如通过支付宝,询问无果。 56 | > - 不愿透露姓名的神秘人:指知道是谁,不愿透露 57 | > 部分超链接为空,因为我也不知道。 58 | 59 | 60 | --- 61 | 62 | 如果您对我们的项目感兴趣,并希望给予支持,请访问我们的[**赞助页面**](README.md)了解更多详情。您的支持将使我们能够继续推动项目的发展,为社区带来更多价值。 63 | 64 | 感谢您的关注和支持! 65 | 66 | # 感谢技术支持 67 | 68 | 另外有一些开发者为我们提供了 `pr`、`issue` 等技术问题提供帮助。 69 | 70 | 您的专业知识和技术能力为我们提供了坚实的后盾,让我们克服了许多技术难题,同时也为项目的未来发展奠定了坚实的基础。您的奉献精神和对技术的热爱不仅仅体现在工作中,更是对整个行业的一种推动和促进。 71 | 72 | 在此,我们再次向您表达最真诚的感谢之情。我们将继续珍惜与您的合作,并期待未来能够有更多的机会共同创造更加美好的未来。 73 | 74 | 再次感谢您的支持与帮助! 75 | 76 | 技术支持名单: 77 | 78 | - [**聚聚**](https://github.com/frederick-vs-ja) 79 | - **[YiRanMushroom](https://github.com/YiRanMushroom)** 80 | 81 | - **[ooolize](https://github.com/ooolize)** 82 | - **[dzyxdd](https://github.com/dzyxdd)** 83 | - **[Side-Cloud](https://github.com/Side-Cloud)** 84 | - **[rightrightright](https://github.com/rightrightright)** 85 | - **[Suzukaze7](https://github.com/Suzukaze7)** 86 | - **[MrShinshi](https://github.com/MrShinshi)** 87 | - [**mq卢瑟**](https://github.com/mq-loser) 88 | 89 | --- 90 | 91 | ## 课程销售 92 | 93 | 截止 `2024-5-31` 课程 b 站销售数量累计 `303`,销售额 `47,436.5` 人民币。 94 | 95 | B站分成、纳税、渠道,收益大约为课程价格的 52%,若为 iOS 购买,售价的则为三分之一。 96 | 97 | 教案长期开源更新维护。 98 | -------------------------------------------------------------------------------- /code/03类模板分文件/CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "configurePresets": [ 4 | { 5 | "name": "windows-base", 6 | "hidden": true, 7 | "generator": "Ninja", 8 | "binaryDir": "${sourceDir}/out/build/${presetName}", 9 | "installDir": "${sourceDir}/out/install/${presetName}", 10 | "cacheVariables": { 11 | "CMAKE_C_COMPILER": "cl.exe", 12 | "CMAKE_CXX_COMPILER": "cl.exe" 13 | }, 14 | "condition": { 15 | "type": "equals", 16 | "lhs": "${hostSystemName}", 17 | "rhs": "Windows" 18 | } 19 | }, 20 | { 21 | "name": "x64-debug", 22 | "displayName": "x64 Debug", 23 | "inherits": "windows-base", 24 | "architecture": { 25 | "value": "x64", 26 | "strategy": "external" 27 | }, 28 | "cacheVariables": { 29 | "CMAKE_BUILD_TYPE": "Debug" 30 | } 31 | }, 32 | { 33 | "name": "x64-release", 34 | "displayName": "x64 Release", 35 | "inherits": "x64-debug", 36 | "cacheVariables": { 37 | "CMAKE_BUILD_TYPE": "Release" 38 | } 39 | }, 40 | { 41 | "name": "x86-debug", 42 | "displayName": "x86 Debug", 43 | "inherits": "windows-base", 44 | "architecture": { 45 | "value": "x86", 46 | "strategy": "external" 47 | }, 48 | "cacheVariables": { 49 | "CMAKE_BUILD_TYPE": "Debug" 50 | } 51 | }, 52 | { 53 | "name": "x86-release", 54 | "displayName": "x86 Release", 55 | "inherits": "x86-debug", 56 | "cacheVariables": { 57 | "CMAKE_BUILD_TYPE": "Release" 58 | } 59 | }, 60 | { 61 | "name": "linux-debug", 62 | "displayName": "Linux Debug", 63 | "generator": "Ninja", 64 | "binaryDir": "${sourceDir}/out/build/${presetName}", 65 | "installDir": "${sourceDir}/out/install/${presetName}", 66 | "cacheVariables": { 67 | "CMAKE_BUILD_TYPE": "Debug" 68 | }, 69 | "condition": { 70 | "type": "equals", 71 | "lhs": "${hostSystemName}", 72 | "rhs": "Linux" 73 | }, 74 | "vendor": { 75 | "microsoft.com/VisualStudioRemoteSettings/CMake/1.0": { 76 | "sourceDir": "$env{HOME}/.vs/$ms{projectDirName}" 77 | } 78 | } 79 | }, 80 | { 81 | "name": "macos-debug", 82 | "displayName": "macOS Debug", 83 | "generator": "Ninja", 84 | "binaryDir": "${sourceDir}/out/build/${presetName}", 85 | "installDir": "${sourceDir}/out/install/${presetName}", 86 | "cacheVariables": { 87 | "CMAKE_BUILD_TYPE": "Debug" 88 | }, 89 | "condition": { 90 | "type": "equals", 91 | "lhs": "${hostSystemName}", 92 | "rhs": "Darwin" 93 | }, 94 | "vendor": { 95 | "microsoft.com/VisualStudioRemoteSettings/CMake/1.0": { 96 | "sourceDir": "$env{HOME}/.vs/$ms{projectDirName}" 97 | } 98 | } 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /code/05显式实例化解决模板导出动态静态库问题/05显式实例化解决模板导出动态静态库问题.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33815.320 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "测试使用动态库", "05显式实例化解决模板导出动态静态库问题.vcxproj", "{0F5C65D9-5EA4-42BE-9214-28FFFB856E78}" 7 | EndProject 8 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "生成动态库", "..\生成动态库\生成动态库.vcxproj", "{DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}" 9 | EndProject 10 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "生成静态库", "..\生成静态库\生成静态库.vcxproj", "{6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}" 11 | EndProject 12 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "测试使用静态库", "..\测试使用静态库\测试使用静态库.vcxproj", "{3A7ECD3D-612F-4ACF-981C-F068D2D7673C}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|x64 = Debug|x64 17 | Debug|x86 = Debug|x86 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {0F5C65D9-5EA4-42BE-9214-28FFFB856E78}.Debug|x64.ActiveCfg = Debug|x64 23 | {0F5C65D9-5EA4-42BE-9214-28FFFB856E78}.Debug|x64.Build.0 = Debug|x64 24 | {0F5C65D9-5EA4-42BE-9214-28FFFB856E78}.Debug|x86.ActiveCfg = Debug|Win32 25 | {0F5C65D9-5EA4-42BE-9214-28FFFB856E78}.Debug|x86.Build.0 = Debug|Win32 26 | {0F5C65D9-5EA4-42BE-9214-28FFFB856E78}.Release|x64.ActiveCfg = Release|x64 27 | {0F5C65D9-5EA4-42BE-9214-28FFFB856E78}.Release|x64.Build.0 = Release|x64 28 | {0F5C65D9-5EA4-42BE-9214-28FFFB856E78}.Release|x86.ActiveCfg = Release|Win32 29 | {0F5C65D9-5EA4-42BE-9214-28FFFB856E78}.Release|x86.Build.0 = Release|Win32 30 | {DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}.Debug|x64.ActiveCfg = Debug|x64 31 | {DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}.Debug|x64.Build.0 = Debug|x64 32 | {DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}.Debug|x86.ActiveCfg = Debug|Win32 33 | {DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}.Debug|x86.Build.0 = Debug|Win32 34 | {DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}.Release|x64.ActiveCfg = Release|x64 35 | {DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}.Release|x64.Build.0 = Release|x64 36 | {DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}.Release|x86.ActiveCfg = Release|Win32 37 | {DD5EFDAC-C055-48A7-A432-B2B85EF92CB5}.Release|x86.Build.0 = Release|Win32 38 | {6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}.Debug|x64.ActiveCfg = Debug|x64 39 | {6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}.Debug|x64.Build.0 = Debug|x64 40 | {6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}.Debug|x86.ActiveCfg = Debug|Win32 41 | {6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}.Debug|x86.Build.0 = Debug|Win32 42 | {6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}.Release|x64.ActiveCfg = Release|x64 43 | {6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}.Release|x64.Build.0 = Release|x64 44 | {6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}.Release|x86.ActiveCfg = Release|Win32 45 | {6403154D-05BB-4F71-BC0F-A06DF3C7EAFB}.Release|x86.Build.0 = Release|Win32 46 | {3A7ECD3D-612F-4ACF-981C-F068D2D7673C}.Debug|x64.ActiveCfg = Debug|x64 47 | {3A7ECD3D-612F-4ACF-981C-F068D2D7673C}.Debug|x64.Build.0 = Debug|x64 48 | {3A7ECD3D-612F-4ACF-981C-F068D2D7673C}.Debug|x86.ActiveCfg = Debug|Win32 49 | {3A7ECD3D-612F-4ACF-981C-F068D2D7673C}.Debug|x86.Build.0 = Debug|Win32 50 | {3A7ECD3D-612F-4ACF-981C-F068D2D7673C}.Release|x64.ActiveCfg = Release|x64 51 | {3A7ECD3D-612F-4ACF-981C-F068D2D7673C}.Release|x64.Build.0 = Release|x64 52 | {3A7ECD3D-612F-4ACF-981C-F068D2D7673C}.Release|x86.ActiveCfg = Release|Win32 53 | {3A7ECD3D-612F-4ACF-981C-F068D2D7673C}.Release|x86.Build.0 = Release|Win32 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {7B327365-3754-4D45-8C38-5AD720E027C6} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/05模板偏特化.md: -------------------------------------------------------------------------------- 1 | # 模板偏特化 2 | 3 | 如果你认真学习了我们上一节内容,本节应当是十分简单的。 4 | 5 | 模板偏特化这个语法让**模板实参具有一些相同特征**可以自定义,而不是像全特化那样,必须是**具体的**什么类型,什么值。 6 | 7 | 比如:指针类型,这是一类类型,有 `int*`、`double*`、`char*`,以及自定义类型的指针等等,它们都属于指针这一类类型;可以使用偏特化对指针这一类类型进行定制。 8 | 9 | - ***模板偏特化使我们可以对具有相同的一类特征的类模板、变量模板进行定制行为。*** 10 | 11 | ## 变量模板偏特化 12 | 13 | ```cpp 14 | template 15 | const char* s = "?"; // 主模板 16 | 17 | template 18 | const char* s = "pointer"; // 偏特化,对指针这一类类型 19 | 20 | template 21 | const char* s = "array"; // 偏特化,但是只是对 T[] 这一类类型,而不是数组类型,因为 int[] 和 int[N] 不是一个类型 22 | 23 | std::cout << s << '\n'; // ? 24 | std::cout << s << '\n'; // pointer 25 | std::cout << s << '\n'; // pointer 26 | std::cout << s << '\n'; // array 27 | std::cout << s << '\n'; // array 28 | std::cout << s << '\n'; // ? 29 | ``` 30 | 31 | 语法就是正常写主模板那样,然后再定义这个 `s` 的时候,指明模板实参。或者你也可以定义常量的模板形参的模板,偏特化,都是一样的写法。 32 | 33 | 不过与全特化不同,全特化不会写 `template`,它是直接 `template<>`,然后指明具体的模板实参。 34 | 35 | 它与全特化最大的不同在于,全特化基本必写 `template<>`,而且定义的时候(如 `s`)是指明具体的类型,而不是一类类型(T*、T[])。 36 | 37 | --- 38 | 39 | 我们再举个例子: 40 | 41 | ```cpp 42 | template 43 | const char* s = "?"; 44 | 45 | template 46 | const char* s = "T == int"; 47 | 48 | std::cout << s << '\n'; // ? 49 | std::cout << s << '\n'; // T == int 50 | std::cout << s << '\n'; // T == int 51 | ``` 52 | 53 | 这种偏特化也是可以的,多个模板实参的情况下,对第一个模板实参为 `int` 的情况进行偏特化。 54 | 55 | 其他的各种形式无非都是我们提到的这两个示例的变种,类模板也不例外。 56 | 57 | ## 类模板偏特化 58 | 59 | ```cpp 60 | template 61 | struct X{ 62 | void f_T_T2(); // 主模板,声明 63 | }; 64 | 65 | template 66 | void X::f_T_T2() {} // 类外定义 67 | 68 | template 69 | struct X{ 70 | void f_void_T(); // 偏特化,声明 71 | }; 72 | 73 | template 74 | void X::f_void_T() {} // 类外定义 75 | 76 | X x; 77 | x.f_T_T2(); // OK! 78 | X x2; 79 | x2.f_void_T(); // OK! 80 | ``` 81 | 82 | 稍微提一下类外的写法,不过其实**通常不推荐写到类外**,目前还好;很多情况涉及大量模板的时候,类内声明写到类外非常的麻烦。 83 | 84 | --- 85 | 86 | 我们再举一个偏特化类模板中的类模板,全特化和偏特化一起使用的示例: 87 | 88 | ```cpp 89 | template 90 | struct X{ 91 | template 92 | struct Y{}; 93 | }; 94 | 95 | template<> 96 | template 97 | struct X::Y { // 对 X 的情况下的 Y 进行偏特化 98 | void f()const{} 99 | }; 100 | 101 | int main(){ 102 | X::Yy; 103 | y.f(); // OK X 和 Y 104 | X::Yy2; 105 | y2.f(); // Error! 主模板模板实参不对 106 | X::Yy3; 107 | y3.f(); // Error!成员函数模板模板实参不对 108 | } 109 | ``` 110 | 111 | > 此示例无法在 gcc [通过编译](https://godbolt.org/z/rvYhf9K6M),这是**编译器 BUG 需要注意**。 112 | 113 | 语法形式是简单的,不做过多的介绍。 114 | 115 | 其实和全特化没啥区别。 116 | 117 | ## 实现 `std::is_same_v` 118 | 119 | 我们再写一个小示例,实现这个简单的 C++ 标准库设施。 120 | 121 | ```cpp 122 | template // 主模板 123 | inline constexpr bool is_same_v = false; 124 | template // 偏特化 125 | inline constexpr bool is_same_v = true; 126 | ``` 127 | 128 | 这是对变量模板的偏特化,逻辑也很简单,如果两个模板类型参数的类型是一样的,就匹配到下面的偏特化,那么初始化就是 `true`,不然就是 `false`。 129 | 130 | 因为没有用到模板类型形参,所以我们只是写了 `class` 进行占位;这就和你声明函数的时候,如果形参没有用到,那么就不声明名字一样合理,比如 `void f(int)`。 131 | 132 | > 声明为 `inline` 的是因为 内联变量 (C++17 起)可以在被多个源文件包含的头文件中定义。也就是允许多次定义。 133 | 134 | ## 总结 135 | 136 | 我们在一开始的模板全特化花了很多时间讲解各种情况和细节,偏特化除了那个语法上,其他的各种形式并无区别,就不再介绍了。 137 | 138 | 本节我们给出了三个示例,也是最常见最基础的情况。我们要懂得变通,可能还有以此为基础的各种形式。值得注意的是,模板偏特化还可以和 `SFINAE`[^1] 一起使用,这会在我们后续的章节进行讲解,不用着急。 139 | 140 | 如还有需求,查看 [cppreference](https://zh.cppreference.com/w/cpp/language/partial_specialization)。 141 | 142 | 最后强调一句:***函数模板没有偏特化,只有类模板和变量模板可以***。 143 | 144 | [^1]:注:模板代换失败不是错误;在[后续章节](10了解与利用SFINAE.md)有详细讲解。 145 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/03变量模板.md: -------------------------------------------------------------------------------- 1 | # 变量模板 2 | 3 | 本节将介绍 C++14 变量模板 4 | 5 | ## 初识变量模板 6 | 7 | 变量模板不是变量,只有实例化的变量模板,编译器才会生成实际的变量。 8 | 9 | 变量模板实例化后简单的说**就是一个全局变量**,所以也不用担心生存期的问题。 10 | 11 | ### 定义变量模板 12 | 13 | ```cpp 14 | template 15 | T v; 16 | ``` 17 | 18 | 就这么简单,毫无难度。 19 | 20 | 当然了,既然是变量,自然可以有各种的修饰,比如 cv 限定,比如 `constexpr` ,当然也可以有初始化器,比如 `{}` 、`= xxx`。 21 | 22 | ```cpp 23 | template 24 | constexpr T v{}; 25 | ``` 26 | 27 | ### 使用变量模板 28 | 29 | ```cpp 30 | template 31 | constexpr T v{}; 32 | 33 | v; // 相当于 constexpr int v = 0; 34 | ``` 35 | 36 | 我们知道 `constexpr` 附带了 `const` 类型,所以其实: 37 | 38 | ```cpp 39 | std::cout << std::is_same_v),const int> << '\n'; 40 | ``` 41 | 42 | > [std::is_same_v](https://zh.cppreference.com/w/cpp/types/is_same) 其实也是个变量模板,在 C++17 引入。这里用来比较两个类型是否相同,如果相同返回 1,不相同返回 0。暂时不用纠结它是如何实现的,后续会手搓。 43 | 44 | 会打印 1,也就是 `v` 的类型其实就是 `const int`。 45 | 46 | --- 47 | 48 | 我们再提出一个问题,`v` 和 `v` 有什么关系吗? 49 | 50 | > 最早在函数模板中,我们强调了“**同一个函数模板生成的不同类型的函数,彼此之间没有任何关系**”,这句话放在类模板、变量模板中,也同样适用。 51 | 52 | ```cpp 53 | std::cout << &v << '\n'; 54 | std::cout << &v << '\n'; 55 | ``` 56 | 57 | 以上示例打印的地址不会相同。 58 | 59 | ## 有默认实参的模板形参 60 | 61 | 变量模板和函数模板、类模板一样,支持模板形参有默认实参。 62 | 63 | ```cpp 64 | template 65 | constexpr T v{}; 66 | 67 | int b = v<>; // v 就是 v 也就是 const int v = 0 68 | ``` 69 | 70 | 与函数模板和类模板不同,即使模板形参有默认实参,依然要求写明 `<>`。 71 | 72 | ## 常量模板形参 73 | 74 | 变量模板和函数模板、类模板一样,支持常量模板形参。 75 | 76 | ```cpp 77 | template 78 | constexpr int v = N; 79 | 80 | std::cout << v<10> << '\n'; // 等价 constexpr int v = 10; 81 | ``` 82 | 83 | 当然,它也可以有默认值: 84 | 85 | ```cpp 86 | template 87 | constexpr int v = N; 88 | 89 | std::cout << v<10> << '\n'; 90 | std::cout << v<> << '\n'; 91 | ``` 92 | 93 | ## 可变参数变量模板 94 | 95 | 变量模板和函数模板、类模板一样,支持形参包与包展开。 96 | 97 | ```cpp 98 | template 99 | constexpr std::size_t array[]{ values... }; 100 | 101 | int main() { 102 | for (const auto& i : array<1, 2, 3, 4, 5>) { 103 | std::cout << i << ' '; 104 | } 105 | } 106 | ``` 107 | 108 | array 是一个数组,我们传入的模板实参用来推导出这个数组的大小以及初始化。 109 | 110 | `{values...}` 展开就是`{1, 2, 3, 4, 5}`。 111 | 112 | ```cpp 113 | std::cout << std::is_same_v), const std::size_t[5]>; // 1 114 | ``` 115 | 116 | 在 msvc 与 gcc14 会输出 **`1`**,但是 gcc14 之前的版本、clang,却会[输出 `0`](https://godbolt.org/z/PoGcoTc44)。***msvc 与 gcc14 是正确的***。 117 | gcc 与 clang 不认为 `array<1, 2, 3, 4, 5>` 与 `const std::size_t[5]` 类型相同;它们认为 `array<1, 2, 3, 4, 5>` 与 `const std::size_t[]` [类型相同](https://godbolt.org/z/4a5j83TsT),这显然是个 **bug**。 118 | 119 | > 可以参见 [llvm issues](https://github.com/llvm/llvm-project/issues/79750). 120 | 121 | ## 类静态数据成员模板 122 | 123 | 在类中也可以使用变量模板。 124 | 125 | ### 类静态数据成员 126 | 127 | 讲模板之前先普及一下静态数据成员的基本知识,因为**网上很多资料都是乱讲**,所以有必要重复强调一下。 128 | 129 | ```cpp 130 | struct X{ 131 | static int n; 132 | }; 133 | ``` 134 | 135 | n 是一个 X 类的静态数据成员,它在 X 中声明,但是却没有定义,我们需要类外定义。 136 | 137 | ```cpp 138 | int X::n; 139 | ``` 140 | 141 | 或者在 C++17 以 inline 或者 constexpr 修饰。 142 | 143 | > 因为 C++17 规定了 **inline** 修饰静态数据成员,那么这就是在类内定义,不再需要类外定义。constexpr 在 C++17 修饰静态数据成员的时候,蕴含了 **inline**。 144 | 145 | ```cpp 146 | struct X { 147 | inline static int n; 148 | }; 149 | 150 | struct X { 151 | constexpr static int n = 1; // constexpr 必须初始化,并且它还有 const 属性 152 | }; 153 | ``` 154 | 155 | ### 模板 156 | 157 | 与其他静态成员一样,静态数据成员模板的需要一个定义。 158 | 159 | ```cpp 160 | struct limits{ 161 | template 162 | static const T min; // 静态数据成员模板的声明 163 | }; 164 | 165 | template 166 | const T limits::min = {}; // 静态数据成员模板的定义 167 | ``` 168 | 169 | 当然,如果支持 C++17 你也可以选择直接以 `inline` 修饰。 170 | 171 | ## 变量模板分文件 172 | 173 | 变量模板和函数模板、类模板一样,通常写法不支持分文件,原因都一样。 174 | 175 | ## 总结 176 | 177 | 变量模板其实很常见,在 C++17,所有元编程库的[类型特征](https://zh.cppreference.com/w/cpp/meta)均添加了 `_v` 的版本,就是使用的变量模板,比如 `std::is_same_v` 、`std::is_pointer_v` 等;我们在后面会详细讲解。 178 | 179 | 如果学到这里了,如果你注意到,函数模板、类模板、变量模板,很多语法是共通的,是越学越简单,代表你思考了。 180 | 181 | 后续还有很多内容是一起的,比如模板偏特化、全特化、显式实例化等。 182 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/08折叠表达式.md: -------------------------------------------------------------------------------- 1 | # 折叠表达式 2 | 3 | C++17 折叠表达式让我们能以更加简单的语法形式,更加轻松的进行**形参包展开**。 4 | 5 | 本节也将重新复习形参包的知识,希望各位不要忘记了。 6 | 7 | ## 语法 8 | 9 | ```txt 10 | ( 形参包 运算符 ... ) (1) 11 | ( ... 运算符 形参包 ) (2) 12 | ( 形参包 运算符 ... 运算符 初值 ) (3) 13 | ( 初值 运算符 ... 运算符 形参包 ) (4) 14 | ``` 15 | 16 | 1. 一元右折叠 17 | 2. 一元左折叠 18 | 3. 二元右折叠 19 | 4. 二元左折叠 20 | 21 | 折叠表达式的实例化按以下方式展开成表达式 e: 22 | 23 | 1. 一元右折叠 `(E 运算符 ...)` 成为 `(E1 运算符 (... 运算符 (EN-1 运算符 EN)))` 24 | 2. 一元左折叠 `(... 运算符 E)` 成为 `(((E1 运算符 E2) 运算符 ...) 运算符 EN)` 25 | 3. 二元右折叠 `(E 运算符 ... 运算符 I)` 成为 `(E1 运算符 (... 运算符 (EN−1 运算符 (EN 运算符 I))))` 26 | 4. 二元左折叠 `(I 运算符 ... 运算符 E)` 成为 `((((I 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN)` 27 | (其中 N 是包展开中的元素数量) 28 | 29 | - ***折叠表达式是左折叠还是右折叠,取决于 `...` 是在“形参包”的左边还是右边。*** 30 | 31 | ## 实现一个 print 函数 32 | 33 | > 34 | > - 以下代码来自[01函数模板.md](01函数模板.md) 中的可变参数模板的示例 35 | > 36 | >```cpp 37 | >template 38 | >void print(const Args&...args){ 39 | > int _[]{ (std::cout << args << ' ' ,0)... }; 40 | >} 41 | >print("luse", 1, 1.2); // luse 1 1.2 42 | >``` 43 | 44 | 运用折叠表达式,我们可以简化 `print`,而不再需要创建愚蠢的数组对象 `_`。 45 | 46 | ```cpp 47 | template 48 | void print(const Args&...args) { 49 | ((std::cout << args << ' '), ...); 50 | } 51 | print("luse", 1, 1.2); // luse 1 1.2 52 | ``` 53 | 54 | 这显然是 (1)**一元右折叠**,我们一步一步分析: 55 | 56 | `(std::cout << args << ' ')` 就是语法中指代的**形参包**(其实说的是含有形参包的运算符表达式)。那么 `,` 逗号就是运算符,最后 `...` 。然后最外层有括号 `()` 符合语法。 57 | 58 | 函数模板实例化、折叠表达式展开,大概就是: 59 | 60 | ```cpp 61 | void print(const char(&args0)[5], const int& args1, const double& args2) { 62 | (std::cout << args0 << ' '), ((std::cout << args1 << ' '), (std::cout << args2 << ' ')); 63 | } 64 | ``` 65 | 66 | 我不建议各位数括号,死记这个规则,知道大概的意思就行,运用逗号运算符进行这个折叠表达式还是简单的,多用用就好。 67 | 68 | ## 详细展示语法 69 | 70 | 折叠表达式这四种语法形式我们都需要学习,并且明白其区别,我们用一个一个示例来展示,你自然会感觉到区别,毕竟**运行结果不一样**。 71 | 72 | 你可以学不懂或用不到这所有形式,但我总得教。 73 | 74 | ### 一元折叠 75 | 76 | ```cpp 77 | template 78 | void print(const Args&...args) { 79 | (...,(std::cout << args << ' ')); 80 | } 81 | print("luse", 1, 1.2); // luse 1 1.2 82 | ``` 83 | 84 | 这个示例就是参考我们上面用折叠表达式实现的 `print`,只不过一开始的是“一元右折叠”,而我们这个示例是“**一元左折叠**”。 85 | 86 | 如你所见,这个 `print` 不管使用左折叠还是右折叠,运行结果是一样的,这是为什么呢? 87 | 88 | 实例化展开后是这样: 89 | 90 | ```cpp 91 | void print(const char(&args0)[5], const int& args1, const double& args2) { 92 | ((std::cout << args0 << ' '), (std::cout << args1 << ' ')), (std::cout << args2 << ' '); 93 | } 94 | ``` 95 | 96 | 其实这个括号根本不影响什么,我们可以得出结论:“*对于逗号运算符,一元左折叠和一元右折叠没有区别*”。 97 | 98 | --- 99 | 100 | 我们用一个常量模板参数的变量模板来展示在一些情况下左折叠和右折叠是会造成不同结果的: 101 | 102 | ```cpp 103 | template 104 | constexpr int v_right = (I - ...); // 一元右折叠 105 | 106 | template 107 | constexpr int v_left = (... - I); // 一元左折叠 108 | 109 | int main(){ 110 | std::cout << v_right<4, 5, 6> << '\n'; //(4-(5-6)) 5 111 | std::cout << v_left<4, 5, 6> << '\n'; //((4-5)-6) -7 112 | } 113 | ``` 114 | 115 | 这个示例很好,那么简单总结一下:左折叠和右折叠是需要注意的,它们的效果可能不同。 116 | 117 | 其实按照以上示例效果 `(4-(5-6))` `((4-5)-6)` 还可以总结一段简单的话:***右折叠就是先算右边,左折叠就是先算左边***。 118 | 119 | 我知道你肯定有疑问了: 120 | 121 | > 前面不是说了:“对于逗号运算符,一元左折叠和一元右折叠没有区别”,为啥这里还会有**谁先算**这种说法? 122 | 123 | 有这个想法就代表思考了,我们来讲一下。 124 | 125 | 逗号表达式其实也是右折叠先算右边,左折叠先算左边,但是但是,请注意:“**`,`**”,逗号不同于其他运算符; 126 | 127 | 比如 `(expr1,(expr2,expr3))` 和 `((expr1,expr2),expr3)` 因为逗号表达式的特性,从左往右顺序执行,逗号运算符本身不需要做什么运算,那么这些括号根本不影响什么,但是如果换成其他的运算符,比如 `-`,就不同了,`(expr1-(expr2-expr3))` 和 `((expr1-expr2)-expr3)` 显然不同,也就是前面的:`(4-(5-6))` `((4-5)-6)`。 128 | 129 | **“先算”这个词,不是各位想象的那种,一定要先进行运算产生结果,也可能不会有什么运算,比如逗号表达式;这个先算其实指代的是折叠表达式语法给加的括号。但是这个表达式到底是怎么样的运算,是要根据运算符的**。 130 | 131 | 到此,我们讲明白了为什么逗号表达式一元左右折叠效果一样,有些情况效果不一样;以及一元折叠的内容。 132 | 133 | > 补充:各位要明白一件事情,我们说的“逗号特殊”,不是它真的特殊,对于折叠表达式规则而言,这些运算符都是一样的处理的,根据你自己运算符的行为,彼此之间毫无区别。我为什么要说“逗号特殊”?这是一个抽象的指代,**指代的是大家看到可能不懂,奇怪,觉得特殊,而不是真的特殊**。`+` 运算符也是左折叠和右折叠效果一样,但是这个大家都懂,我就没提。另外上面提到的所谓的“逗号运算符本身什么都不做“是没有考虑你重载它的,这个大家知道就行。 134 | 135 | ### 二元折叠 136 | 137 | ```cpp 138 | template 139 | void print(Args&&... args){ 140 | (std::cout << ... << args) << '\n'; 141 | } 142 | print("luse", 1, 1.2); // luse11.2 143 | ``` 144 | 145 | 又是我们的老朋友 `print` 函数,不过这次,它又换了形式,这是一个**二元左折叠**。 146 | 147 | 判断一个折叠表达式是否是二元的,只需要看一点:**`运算符 ... 运算符`** 这种形式就是二元。 148 | 149 | 判断是左还是右,我们前面已经提了: 150 | 151 | > - ***折叠表达式是左折叠还是右折叠,取决于 `...` 是在“形参包”的左边还是右边。*** 152 | 153 | 根据语法套一下 `( 初值 运算符 ... 运算符 形参包 )`,我们给出的示例 print 中 `std::cout` 就是初值、 `运算符 ... 运算符` 就是 `<< ... <<` 、形参包就是 `args`。 154 | 155 | --- 156 | 157 | ```cpp 158 | // 二元右折叠 159 | template 160 | constexpr int v = (I + ... + 10); // 1 + (2 + (3 + (4 + 10))) 161 | // 二元左折叠 162 | template 163 | constexpr int v2 = (10 + ... + I); // (((10 + 1) + 2) + 3) + 4 164 | 165 | std::cout << v<1, 2, 3, 4> << '\n'; // 20 166 | std::cout << v2<1, 2, 3, 4> << '\n'; // 20 167 | ``` 168 | 169 | 其实和一元折叠中说的差不多不是吗? 170 | 171 | > **右折叠就是先算右边,左折叠就是先算左边**。 172 | 173 | 不过二元折叠表达式必然有一个“**初值**”(我们这里是 `10` ),是先计算的。 174 | 175 | ## 总结 176 | 177 | 省略了一些,但这一节整体我写的较为满意,规则和重点都聊的很清楚,其他的任何形式,无非都是以上的去雕花罢了。 178 | 179 | 我们可以布置一个课后作业,说出以下代码使用的折叠表达式语法,以及它的效果,详细解析,使用 Markdown 语法。 180 | 181 | ```cpp 182 | template 183 | auto Reverse(Args&&... args) { 184 | std::vector> res{}; 185 | bool tmp{ false }; 186 | (tmp = ... = (res.push_back(args), false)); 187 | return res; 188 | } 189 | ``` 190 | 191 | 提交到 [`homework`](/homework/) 文件夹中的 `08折叠表达式作业` 文件夹中(如果没有就创建),然后新建文件(命名需要是有意义的),写好后提交 pr。 192 | -------------------------------------------------------------------------------- /md/第二部分-造轮子/使用模板包装C风格API进行调用.md: -------------------------------------------------------------------------------- 1 | # 使用模板包装C风格API进行调用 2 | 3 | 这可以说是一个非常经典的需求,并且它涉及到的模板知识并不多,主要其实也就是可变参数,元组的处理,更多的还是一个思路写法,经典的 `void*` + 变参模板。 4 | 5 | 我们的写法完全参照 [MSVC STL](https://github.com/microsoft/STL) 实现的 [**`std::thread`**](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/thread)。在阅读本节内容之前,希望各位已经学习了现代 C++ 并发编程教程中,[**`std::thread` 的构造-源码解析**](https://github.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/blob/main/md/%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90/01thread%E7%9A%84%E6%9E%84%E9%80%A0%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md)。因为 std::thread 构造函数实际就是将我们传入的所有可调用对象、参数,包装为函数指针,和 `void*` 参数,调用 win32 的创建线程的函数 [**_beginthreadex**](https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/beginthread-beginthreadex?view=msvc-170)。 6 | 7 | 简单来说,我们需要写一个类包装一个这样的函数 `f` ,支持任意可调用对象与任意类型和个数的参数,最终都执行函数 `f`。 8 | 9 | ```cpp 10 | void f(unsigned(*start_address)(void*),void *args){ 11 | start_address(args); 12 | std::cout << "f\n"; 13 | } 14 | ``` 15 | 16 | 它和创建线程的函数很像,一个函数指针是要执行的函数,一个 `void*` 是参数。 17 | 18 | --- 19 | 20 | ## 答案与解释 21 | 22 | 答案如下: 23 | 24 | ```cpp 25 | struct X { 26 | template 27 | X(Fn&& func, Args&&... args) { 28 | using Tuple = std::tuple, std::decay_t...>; 29 | auto Decay_copied = std::make_unique(std::forward(func), std::forward(args)...); 30 | auto Invoker_proc = start(std::make_index_sequence<1 + sizeof...(Args)>{}); 31 | f(Invoker_proc, Decay_copied.release()); 32 | } 33 | template 34 | static constexpr auto start(std::index_sequence) noexcept { 35 | return &Invoke; 36 | } 37 | 38 | template 39 | static unsigned int Invoke(void* RawVals) noexcept { 40 | const std::unique_ptr FnVals(static_cast(RawVals)); 41 | Tuple& Tup = *FnVals.get(); 42 | std::invoke(std::move(std::get(Tup))...); 43 | return 0; 44 | } 45 | }; 46 | ``` 47 | 48 | > [测试结果](https://godbolt.org/z/xePPc8aoM)。 49 | 50 | 其实很简单,就三个函数而已。**这里的难点只是将我们的可调用对象转换为 `unsigned(*start_address)(void*)` 这样类型的函数指针以及处理可变参数罢了**。我们的做法也很简单,利用模板,做了一个代码生成,实际我们传递的是静态成员函数模板 `Invoke` 给函数 `f` 调用,当然,是实例化之后的,还用到了 `start` 函数。传递的所有参数则**使用了一个元组存储副本**,由独占的智能指针管理。最终都传递给函数 `f` 调用。 51 | 52 | 好,接下来我们来一句一句解析: 53 | 54 | 1. `using Tuple = std::tuple, std::decay_t...>;` 定义了一个元组别名,其元组的模板类型参数就是传递给构造函数的**所有对象的类型**。 55 | 56 | 2. `auto Decay_copied = std::make_unique(std::forward(func), std::forward(args)...);`定义一个独占智能指针,指向了一个元组,其存储了传递给构造函数的**所有的参数的副本**。 57 | 58 | 3. `auto Invoker_proc = start(std::make_index_sequence<1 + sizeof...(Args)>{});` 调用静态成员函数模板 `start` 得到一个普通函数指针。这里需要详细展开。传递了类型参数 `Tuple` ,已经使用 `std::make_index_sequence` 制造了一个可变参数序列,用来遍历元组。 59 | 60 | > **[`std::index_sequence`](https://en.cppreference.com/w/cpp/utility/integer_sequence) 和 `std::make_index_sequence` 的用法我们用一个简单[示例](https://godbolt.org/z/dv88aPGac)介绍一下。** 61 | 62 | ```cpp 63 | template 64 | static constexpr auto start(std::index_sequence) noexcept { 65 | return &Invoke; 66 | } 67 | ``` 68 | 69 | 将模板参数转发给 `Invoke` 进行实例化,获取这个静态成员函数模板的地址,也就是普通符合类型要求的函数指针了。 70 | 71 | ```cpp 72 | template 73 | static unsigned int Invoke(void* RawVals) noexcept { 74 | const std::unique_ptr FnVals(static_cast(RawVals)); 75 | Tuple& Tup = *FnVals.get(); 76 | std::invoke(std::move(std::get(Tup))...); 77 | return 0; 78 | } 79 | ``` 80 | 81 | 我们的 `Invoke` 利用模板生成代码,支持了所有可调用类型,以及遍历元组的参数。**无非是把 `void*` 指针转换为正确的类型再去使用罢了,而这个“正确的类型”,通过模板传递。**最终的调用,在 `std::invoke(std::move(std::get(Tup))...);` 这一行,如你所见,默认移动,和 `std::thread` 一样。 82 | 83 | 4. `f(Invoker_proc, Decay_copied.release());` 将函数指针 `Invoker_proc` 和存储了传递给构造函数的所有参数的副本的智能指针 `Decay_copied` 释放所有权,返回原始指针,用来调用 C 风格函数 f。 84 | 85 | --- 86 | 87 | ## 使用示例 88 | 89 | ```cpp 90 | void func(int& a){ 91 | std::cout << &a << '\n'; 92 | } 93 | 94 | int main(){ 95 | int a{}; 96 | std::cout << &a << '\n'; 97 | X{ func,a }; 98 | } 99 | ``` 100 | 101 | > [测试代码](https://godbolt.org/z/aT78bbWaz)。 102 | 103 | 和 `std::thread` 一样,上面代码无法通过编译,”*`invoke` 未找到匹配的重载函数*“。原因很简单,我们上面的代码展示了,最终的 `invoke` 调用是用了 `std::move` 的,参数被转换为右值表达式,形参类型是左值引用,左值引用不能引用右值表达式自然不行了。 104 | 105 | ```cpp 106 | void func(const int& a){ 107 | std::cout << &a << '\n'; 108 | } 109 | 110 | int main(){ 111 | int a{}; 112 | std::cout << &a << '\n'; 113 | X{ func,a }; 114 | } 115 | ``` 116 | 117 | > [测试代码](https://godbolt.org/z/qTPofrW6z)。 118 | 119 | 我们还可以将 `func` 的形参类型改为 `const int&` ,这可以通过编译,因为 `const int&` 可以引用右值表达式。当然了,打印的**地址不同**。原因也很简单,我们说了,智能指针**存储的是参数副本**,元组类型是使用 [`std::decay_t`](https://zh.cppreference.com/w/cpp/types/decay) 删除了 CV 与引用限定的。 120 | 121 | ```cpp 122 | void func(const int& a){ 123 | std::cout << &a << '\n'; 124 | } 125 | 126 | int main(){ 127 | int a{}; 128 | std::cout << &a << '\n'; 129 | X{ func,std::ref(a) }; 130 | } 131 | ``` 132 | 133 | > [测试代码](https://godbolt.org/z/9srsc8dfT)。 134 | 135 | 同样的,和 `std::thread` 一样使用 `std::ref` 即可解决。 136 | 137 | ## 总结 138 | 139 | 如果感受到难度,重新细读[**`std::thread` 的构造-源码解析**](https://github.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/blob/main/md/%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90/01thread%E7%9A%84%E6%9E%84%E9%80%A0%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) ,或者再把[**第一部分-基础初始**](../第一部分-基础知识/)知识好好学习。 140 | 141 | 如果您还是不能全部理解,那也不用过度担心,请收藏,需要的时候照着使用,写多了很多时候往往突然就能懂了,**关键在于应用**。 142 | -------------------------------------------------------------------------------- /md/扩展知识/CRTP的原理与使用.md: -------------------------------------------------------------------------------- 1 | # CRTP 奇特重现模板模式 2 | 3 | 它的范式基本上都是:父类是模板,再定义一个类类型继承它。因为类模板不是类,只有实例化后的类模板才是实际的类类型,所以我们需要实例化它,**显式指明模板类型参数,而这个类型参数就是我们定义的类型,也就是子类了**。 4 | 5 | ```cpp 6 | template 7 | class Base {}; 8 | 9 | class X : public Base {}; 10 | ``` 11 | 12 | 这种范式是完全合法合理的,并无问题,首先不要对这种东西感到害怕或觉得非常神奇,也只不过是基本的语法规则所允许的。即使不使用 `CRTP` ,这些写法也是完全合理的,并无问题。 13 | 14 | --- 15 | 16 | CRTP 可用于在父类暴露接口,而子类实现该接口,以此实现“***编译期多态***”,或称“***静态多态***”。 示例如下: 17 | 18 | ```cpp 19 | template 20 | class Base { 21 | public: 22 | // 公开的接口函数 供外部调用 23 | void addWater(){ 24 | // 调用子类的实现函数。要求子类必须实现名为 impl() 的函数 25 | // 这个函数会被调用来执行具体的操作 26 | static_cast(this)->impl(); 27 | } 28 | }; 29 | 30 | class X : public Base { 31 | public: 32 | // 子类实现了父类接口 33 | void impl() const{ 34 | std::cout<< "X 设备加了 50 毫升水\n"; 35 | } 36 | }; 37 | ``` 38 | 39 | 使用方式也很简单,我们直接创建子类对象,调用 `addWater` 函数即可: 40 | 41 | ```cpp 42 | X x; 43 | x.addWater(); 44 | ``` 45 | 46 | > [运行](https://godbolt.org/z/o373avza5)测试。 47 | 48 | 那么好,问题来了,**为什么呢?** `static_cast(this)` 是做了什么,它为什么可以这样? 49 | 50 | - 很显然 `static_cast(this)` 是进行了一个类型转换,将 `this` 指针(也就是**父类的指针**),转换为通过模板参数传递的类型,也就是**子类的指针**。 51 | - **这个转换是安全合法的**。因为 this 指针实际上指向一个 X 类型的对象,X 类型对象继承了 `Base` 的部分,X 对象也就包含了 `Base` 的部分,所以这个转换在编译期是有效的,并且是合法的。 52 | - 当你调用 `x.addWater()` 时,实际上是 X 对象调用了父类 `Base` 的成员函数。这个成员函数内部使用 `static_cast(this)`,将 this 从 `Base*` 转换为 `X*`,然后调用 X 中的 impl() 函数。这种转换是合法且安全的,且 X 确实实现了 impl() 函数。 53 | 54 | 当然了,我们给出的示例是十分简单的,不过大多的使用的确也就是如此了,我们可以再优化一点,比如不让子类的接口暴露出来: 55 | 56 | ```cpp 57 | template 58 | class Base { 59 | public: 60 | void addWater(){ 61 | static_cast(this)->impl(); 62 | } 63 | }; 64 | 65 | class X : public Base { 66 | // 设置友元,让父类得以访问 67 | friend Base; 68 | // 私有接口,禁止外部访问 69 | void impl() const{ 70 | std::cout<< "X 设备加了 50 毫升水\n"; 71 | } 72 | }; 73 | ``` 74 | 75 | ## 使用 CRTP 模式实现静态多态性并复用代码 76 | 77 | 虚函数的价值在于,作为一个参数传入其他函数时 可以复用那个函数里的代码,而不需要在需求频繁变动与增加的时候一直修改。 78 | 79 | ```cpp 80 | class BaseVirtual { 81 | public: 82 | virtual void addWater(int amount) = 0; // 纯虚函数声明 83 | }; 84 | 85 | class XVirtual : public BaseVirtual { 86 | public: 87 | void addWater(int amount) override { 88 | std::cout << "XVirtual 设备加了 " << amount << " 毫升水\n"; 89 | } 90 | }; 91 | 92 | class YVirtual : public BaseVirtual { 93 | public: 94 | void addWater(int amount) override { 95 | std::cout << "YVirtual 设备加了 " << amount << " 毫升水\n"; 96 | } 97 | }; 98 | 99 | // 接口,父类的引用 100 | void processWaterAdditionVirtual(BaseVirtual& r, int amount) { 101 | if (amount > 0) { 102 | r.addWater(amount); 103 | } else { 104 | std::cerr << "无效数量: " << amount << '\n'; // 错误处理 105 | } 106 | } 107 | 108 | int main(){ 109 | XVirtual xVirtual; 110 | YVirtual yVirtual; 111 | 112 | processWaterAdditionVirtual(xVirtual, 50); 113 | processWaterAdditionVirtual(yVirtual, 100); 114 | processWaterAdditionVirtual(xVirtual, -10); 115 | } 116 | ``` 117 | 118 | > [运行](https://godbolt.org/z/Wjfx1TfMh)测试。 119 | 120 | **CRTP 同样可以,并且还是静态类型安全,这不成问题:** 121 | 122 | ```cpp 123 | template 124 | class Base { 125 | public: 126 | void addWater(int amount) { 127 | static_cast(this)->impl_addWater(amount); 128 | } 129 | }; 130 | 131 | class X : public Base { 132 | friend Base; 133 | void impl_addWater(int amount) { 134 | std::cout << "X 设备加了 " << amount << " 毫升水\n"; 135 | } 136 | }; 137 | class Y : public Base { 138 | friend Base; 139 | void impl_addWater(int amount) { 140 | std::cout << "Y 设备加了 " << amount << " 毫升水\n"; 141 | } 142 | }; 143 | 144 | template 145 | void processWaterAddition(Base& r, int amount) { 146 | if (amount > 0) { 147 | r.addWater(amount); 148 | } else { 149 | std::cerr << "无效数量: " << amount << '\n'; 150 | } 151 | } 152 | 153 | int main() { 154 | X x; 155 | Y y; 156 | 157 | processWaterAddition(x, 50); 158 | processWaterAddition(y, 100); 159 | processWaterAddition(x, -10); 160 | } 161 | ``` 162 | 163 | > [运行](https://godbolt.org/z/YoabKjMhh)测试。 164 | 165 | ## C++23 的改动-显式对象形参 166 | 167 | C++23 引入了**显式对象形参**,让我们的 `CRTP` 的形式也出现了变化: 168 | 169 | > [显式对象形参](https://zh.cppreference.com/w/cpp/language/member_functions#.E6.98.BE.E5.BC.8F.E5.AF.B9.E8.B1.A1.E6.88.90.E5.91.98.E5.87.BD.E6.95.B0),顾名思义,就是将 C++23 之前,隐式的,由编译器自动将 `this` 指针传递给成员函数使用的,改成**允许用户显式写明**了,也就是: 170 | > 171 | > ```cpp 172 | > struct X{ 173 | > void f(this const X& self){} 174 | > }; 175 | > ``` 176 | > 177 | > 它也支持模板(可以直接 `auto` 而无需再 `template`),也支持各种修饰,如:`this X self`、`this X& self`、`this const X& self`、`this X&& self`、`this auto&& self`、`const auto& self` ... 等等。 178 | 179 | ```cpp 180 | struct Base { void name(this auto&& self) { self.impl(); } }; 181 | struct D1 : Base { void impl() { std::puts("D1::impl()"); } }; 182 | struct D2 : Base { void impl() { std::puts("D2::impl()"); } }; 183 | ``` 184 | 185 | 不再需要使用 `static_cast` 进行转换,直接调用即可。且如你所见,我们的显式对象形参也可以写成模板的形式:`this auto&& self`。 186 | 187 | 使用上也与之前并无区别,创建子类对象,调用接口即可。 188 | 189 | ```cpp 190 | D1 d; 191 | d.name(); 192 | D2 d2; 193 | d2.name(); 194 | ``` 195 | 196 | `d.name` 也就是把 `d` 传入给父类模板成员函数 `name`,`auto&&` 被推导为 `D1&`,顾名思义”***显式***“对象形参,非常的简单直观。 197 | 198 | > [运行](https://godbolt.org/z/WW59PqEd3)测试。 199 | 200 | ## CRTP 的好处 201 | 202 | 上一节我们详细的介绍和解释了 CRTP 的编写范式和原理。现在我们来稍微介绍一下 CRTP 的众多好处。 203 | 204 | 1. **静态多态** 205 | 206 | CRTP 实现静态多态,无需使用虚函数,静态绑定,无运行时开销。 207 | 2. **类型安全** 208 | 209 | CRTP 提供了类型安全的多态性。通过模板参数传递具体的子类类型,编译器能够确保类型匹配,避免了传统向下转换可能引发的类型错误。 210 | 3. **灵活的接口设计** 211 | 212 | CRTP 允许父类定义公共接口,并要求子类实现具体的操作。这使得基类能够提供通用的接口,而具体的实现细节留给派生类。其实也就是说多态了。 213 | 214 | ## 总结 215 | 216 | 事实上笔者所见过的 `CRTP` 的用法也还不止如此,还有许多更加复杂,有趣的做法,不过就不想再探讨了,以上的内容已然足够。其它的做法也无非是基于以上了。 217 | 218 | 各位可以尝试尽可能的将使用虚函数的代码改成 `CRTP` ,这其实在大多数时候并不构成难度,绝大多数的多态类型都能被很轻松的改成 `CRTP` 的形式。 219 | -------------------------------------------------------------------------------- /code/05显式实例化解决模板导出动态静态库问题/05显式实例化解决模板导出动态静态库问题.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | Win32Proj 24 | {0f5c65d9-5ea4-42be-9214-28fffb856e78} 25 | My05显式实例化解决模板导出动态静态库问题 26 | 10.0 27 | 测试使用动态库 28 | 29 | 30 | 31 | Application 32 | true 33 | v143 34 | Unicode 35 | 36 | 37 | Application 38 | false 39 | v143 40 | true 41 | Unicode 42 | 43 | 44 | Application 45 | true 46 | v143 47 | Unicode 48 | 49 | 50 | Application 51 | false 52 | v143 53 | true 54 | Unicode 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | Level3 77 | true 78 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 79 | true 80 | 81 | 82 | Console 83 | true 84 | 85 | 86 | 87 | 88 | Level3 89 | true 90 | true 91 | true 92 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 93 | true 94 | 95 | 96 | Console 97 | true 98 | true 99 | true 100 | 101 | 102 | 103 | 104 | Level3 105 | true 106 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 107 | true 108 | 109 | 110 | Console 111 | true 112 | $(SolutionDir)lib\dll;%(AdditionalLibraryDirectories) 113 | 生成动态库.lib;%(AdditionalDependencies) 114 | 115 | 116 | 117 | 118 | Level3 119 | true 120 | true 121 | true 122 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 123 | true 124 | 125 | 126 | Console 127 | true 128 | true 129 | true 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /code/测试使用静态库/测试使用静态库.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | Win32Proj 24 | {3a7ecd3d-612f-4acf-981c-f068d2d7673c} 25 | 测试使用静态库 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v143 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v143 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v143 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Level3 76 | true 77 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 78 | true 79 | 80 | 81 | Console 82 | true 83 | 84 | 85 | 86 | 87 | Level3 88 | true 89 | true 90 | true 91 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 92 | true 93 | 94 | 95 | Console 96 | true 97 | true 98 | true 99 | 100 | 101 | 102 | 103 | Level3 104 | true 105 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 106 | true 107 | %(AdditionalIncludeDirectories) 108 | 109 | 110 | Console 111 | true 112 | $(SolutionDir)lib\lib;%(AdditionalLibraryDirectories) 113 | 生成静态库.lib;%(AdditionalDependencies) 114 | 115 | 116 | 117 | 118 | Level3 119 | true 120 | true 121 | true 122 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 123 | true 124 | 125 | 126 | Console 127 | true 128 | true 129 | true 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/06模板显式实例化解决模板分文件问题.md: -------------------------------------------------------------------------------- 1 | # 模板显式实例化解决模板分文件问题 2 | 3 | ## 前言 4 | 5 | 在前面的内容,我们一直讲的都是“**通常写法,函数模板、类模板、变量模板不能分文件**”。 6 | 7 | 并且阐述了原因,简单的说:**在于模板必须使用了才会生成实际的代码,才会有符号让链接器去链接**。 8 | 9 | - **只有实例化模板,编译器才能生成实际的代码**。 10 | 11 | > 需要注意,以前说的“**使用模板**”其实就是会 **[隐式实例化](https://zh.cppreference.com/w/cpp/language/function_template#.E9.9A.90.E5.BC.8F.E5.AE.9E.E4.BE.8B.E5.8C.96)模板**,编译器根据我们的使用,知道我们需要什么类型的模板,生成实际的代码,比如实际的函数,实际的类,实际的变量等,然后再去调用。 12 | 13 | 分文件这个问题显然是可以解决的,那就是:**显式实例化模板**。 14 | 15 | 我们自己指明,到底需要哪些具体的函数。 16 | 17 | ## 函数模板显式实例化 18 | 19 | ```txt 20 | template 返回类型 名字 < 实参列表 > ( 形参列表 ) ; (1) 21 | template 返回类型 名字 ( 形参列表 ) ; (2) 22 | extern template 返回类型 名字 < 实参列表 > ( 形参列表 ) ; (3) (C++11 起) 23 | extern template 返回类型 名字 ( 形参列表 ) ; (4) (C++11 起) 24 | ``` 25 | 26 | 1. 显式实例化定义(显式指定所有无默认值模板形参时不会推导模板实参) 27 | 2. 显式实例化定义,对所有形参进行模板实参推导 28 | 3. 显式实例化声明(显式指定所有无默认值模板形参时不会推导模板实参) 29 | 4. 显式实例化声明,对所有形参进行模板实参推导 30 | 31 | - ***在模板分文件问题中,几乎不会使用到显式实例化声明。*** 32 | 33 | 因为我们引用 `.h` 文件本身就有声明,除非你准备直接两个 `.cpp`。 34 | 35 | >**显式实例化定义强制实例化它所指代的函数或成员函数**。它可以出现在程序中模板定义后的任何位置,而对于给定的实参列表,它在整个程序中只能出现一次,不要求诊断。 36 | 37 | >显式实例化声明(extern 模板)阻止隐式实例化:本来会导致隐式实例化的代码必须改为使用已在程序的别处所提供的显式实例化。(C++11 起) 38 | 39 | >在函数模板特化或成员函数模板特化的显式实例化中,尾部的各模板实参在能从函数参数推导时不需要指定。 40 | 41 | ## 类模板显式实例化 42 | 43 | ```txt 44 | template 类关键词 模板名 < 实参列表 > ; (1) 45 | extern template 类关键词 模板名 < 实参列表 > ; (2) (C++11 起) 46 | ``` 47 | 48 | 类关键词 class,struct 或 union 49 | 50 | 1. 显式实例化定义 51 | 2. 显式实例化声明 52 | 53 | 语法不过多赘述,看使用示例。 54 | 55 | ## 使用示例 56 | 57 | 我们用 cmake 构建了一个[简单的项目](/code/04显式实例化解决模板分文件问题/)展示使用显式实例化模板解决类模板函数模板的分文件的问题。 58 | 59 | [**`main.cpp`**](/code/04显式实例化解决模板分文件问题/main.cpp) 60 | 61 | ```cpp 62 | #include "test_function_template.h" 63 | #include "test_class_template.h" 64 | 65 | int main() { 66 | f_t(1); 67 | f_t(1.2); 68 | f_t('c'); 69 | //f_t("1"); // 没有显式实例化 f_t 版本,会有链接错误 70 | 71 | N::Xx; 72 | x.f(); 73 | //x.f2(); // 链接错误,没有显式实例化 X::f2() 成员函数 74 | N::Xx2{}; 75 | //x2.f(); // 链接错误,没有显式实例化 X::f() 成员函数 76 | 77 | N::X2x3; // 我们显式实例化了类模板 X2 也就自然而然实例化它所有的成员,f,f2 函数 78 | x3.f(); 79 | x3.f2(); 80 | 81 | // 类模板分文件 我们写了两个类模板 X X2,它们一个使用了成员函数显式实例化,一个类模板显式实例化,进行对比 82 | // 这主要在于我们所谓的类模板分文件,其实类模板定义还是在头文件中,只不过成员函数定义在 cpp 罢了。 83 | } 84 | ``` 85 | 86 | [**`test_function_template.h`**](/code/04显式实例化解决模板分文件问题/test_function_template.cpp) 87 | 88 | ```cpp 89 | #pragma once 90 | 91 | #include 92 | #include 93 | 94 | template 95 | void f_t(T); 96 | ``` 97 | 98 | [**`test_function_template.cpp`**](/code/04显式实例化解决模板分文件问题/test_function_template.cpp) 99 | 100 | ```cpp 101 | #include"test_function_template.h" 102 | 103 | template 104 | void f_t(T) { std::cout << typeid(T).name() << '\n'; } 105 | 106 | template void f_t(int); // 显式实例化定义 实例化 f_t(int) 107 | template void f_t<>(char); // 显式实例化定义 实例化 f_t(char),推导出模板实参 108 | template void f_t(double); // 显式实例化定义 实例化 f_t(double),推导出模板实参 109 | ``` 110 | 111 | [**`test_class_template.h`**](/code/04显式实例化解决模板分文件问题/test_class_template.h) 112 | 113 | ```cpp 114 | #pragma once 115 | 116 | #include 117 | #include 118 | 119 | namespace N { 120 | 121 | template 122 | struct X { 123 | int a{}; 124 | void f(); 125 | void f2(); 126 | }; 127 | 128 | template 129 | struct X2 { 130 | int a{}; 131 | void f(); 132 | void f2(); 133 | }; 134 | }; 135 | ``` 136 | 137 | [**`test_class_template.cpp`**](/code/04显式实例化解决模板分文件问题/test_class_template.cpp) 138 | 139 | ```cpp 140 | #include "test_class_template.h" 141 | 142 | template 143 | void N::X::f(){ 144 | std::cout << "f: " << typeid(T).name() << "a: " << this->a << '\n'; 145 | } 146 | 147 | template 148 | void N::X::f2() { 149 | std::cout << "f2: " << typeid(T).name() << "a: " << this->a << '\n'; 150 | } 151 | 152 | template void N::X::f(); // 显式实例化定义 成员函数,这不是显式实例化类模板 153 | 154 | template 155 | void N::X2::f() { 156 | std::cout << "X2 f: " << typeid(T).name() << "a: " << this->a << '\n'; 157 | } 158 | 159 | template 160 | void N::X2::f2() { 161 | std::cout << "X2 f2: " << typeid(T).name() << "a: " << this->a << '\n'; 162 | } 163 | 164 | template struct N::X2; // 类模板显式实例化定义 165 | ``` 166 | 167 |   值得一提的是,我们前面讲类模板的时候说了类模板的成员函数不是函数模板,但是这个语法形式很像前面的“*函数模板显式实例化*”对不对?的确看起来差不多,不过**这是显式实例化类模板成员函数**,而不是函数模板。 168 | 169 | 上面的 `f` `f2` 是定义,但是别把它当成函数模板了,那个 `template` 是属于类模板的。 170 | 171 | 类型链接的时候都不存,只需要保证当前文件有类的完整定义,就能使用模板类。 172 | 173 | 类的完整定义不包括成员函数定义,理论上只要数据成员定义都有就行了。 174 | 175 | 所以我们只需要显式实例化这个成员函数也能完成类模板分文件,如果有其他成员函数,那么我们就得都显式实例化它们才能使用,或者使用显式实例化类模板,它会实例化自己的所有成员。 176 | 177 | ## 模板全特化与显式实例化的类似作用 178 | 179 | 本节是额外补充,虽然一般实际不会有人使用模板全特化来解决我们前面提到的那些问题:“*让编译器生成我们需要的代码*”。 180 | 181 | 我们写了这样一个示例: 182 | 183 | ```cpp 184 | // test.h 185 | #include 186 | template 187 | void f(const T&); 188 | 189 | // test.cpp 190 | #include 191 | template 192 | void f(const T& t){ 193 | std::cout<< t <<'\n'; 194 | } 195 | 196 | //main.cpp 197 | #include "test.h" 198 | int main(){ 199 | f(66); 200 | } 201 | ``` 202 | 203 | 很显然,这段代码是无法通过编译的。[见](https://godbolt.org/z/WY1jd5aKM)。 204 | 205 | 原因在我们讲函数模板的时候就讲的很清楚了,模板只有实例化才会生成实际的函数定义,函数模板不是函数。`test.cpp` 中只是写了一个函数模板定义,编译器不会为我们生成任何函数定义,链接错误。 206 | 207 | 解决方法我相信大家也都懂,我们可以修改 `test.cpp` 为: 208 | 209 | ```cpp 210 | #include 211 | 212 | template 213 | void f(const T& t){ 214 | std::cout<< t <<'\n'; 215 | } 216 | 217 | template void f(const int& t); // 显式实例化 218 | ``` 219 | 220 | 这很合理,不过我们本节的主题是,“**模板全特化**”,它实际有和显式实例化有类似效果,也是要求编译器在当前翻译单元生成我们需要的函数定义,将 `test.cpp` 修改为: 221 | 222 | ```cpp 223 | #include 224 | 225 | template 226 | void f(const T& t){ 227 | std::cout<< t <<'\n'; 228 | } 229 | 230 | template<> 231 | void f(const int& t){ 232 | std::puts("f"); 233 | } 234 | ``` 235 | 236 | 同样可以通过编译。[见](https://godbolt.org/z/Knb6P3E6z)。 237 | 238 | --- 239 | 240 | 我们前面的内容也讲了类模板,它的和函数模板是不同的: 241 | 242 | > 类的完整定义不包括成员函数定义,理论上只要数据成员定义都有就行了。 243 | 244 | 模板全特化对类模板也有效,不过不是对类模板进行全特化,我们还是写一个示例: 245 | 246 | ```cpp 247 | // test.h 248 | #include 249 | template 250 | struct X { 251 | int a{}; 252 | void f(); 253 | }; 254 | 255 | // test.cpp 256 | #include "test.h" 257 | template 258 | void X::f(){ 259 | std::cout << "f: " << typeid(T).name() << "a: " << this->a << '\n'; 260 | } 261 | 262 | //main.cpp 263 | #include "test.h" 264 | int main(){ 265 | Xx; 266 | x.f(); // 此处链接错误 267 | } 268 | ``` 269 | 270 | 很显然,无法通过编译,[链接错误](https://godbolt.org/z/9dPfPcT8x)。 271 | 272 | 显式实例化类模板自然可以解决这个问题,我们修改 `test.cpp` 为其增加一行: 273 | 274 | ```cpp 275 | template struct X; 276 | ``` 277 | 278 | 即可[通过编译](https://godbolt.org/z/a7qGoK34e)。 279 | 280 | 同样的,我们还可以选择为类模板 X 的成员函数 f 进行**全特化**,为 `test.cpp` 增加: 281 | 282 | ```cpp 283 | template<> 284 | void X::f(){ 285 | std::puts("😅:X::f"); 286 | } 287 | ``` 288 | 289 | 同样可以[通过编译](https://godbolt.org/z/T19sh5GWM)。 290 | 291 | ## 总结 292 | 293 | 如你所见,解决分文件问题很简单,显式实例化就完事了。 294 | 295 | 再次我们再重复强调一些概念: 296 | 297 | 模板必须实例化才能使用,实例化就会生成实际代码;有隐式实例化和显式实例化,我们平时粗略的说的“*模板只有使用了才会生成实际代码*”,其实是指使用模板的时候,就会**隐式实例化**,生成实际代码。 298 | 299 | 分文件自然没有隐式实例化了,那我们就得显式实例化,让模板生成我们想要的代码。 300 | 301 | 模板全特化有类似模板显式实例化的作用,不过我们一般知道这件事情即可。 302 | -------------------------------------------------------------------------------- /code/生成静态库/生成静态库.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 16.0 29 | Win32Proj 30 | {6403154d-05bb-4f71-bc0f-a06df3c7eafb} 31 | 生成静态库 32 | 10.0 33 | 34 | 35 | 36 | StaticLibrary 37 | true 38 | v143 39 | Unicode 40 | 41 | 42 | StaticLibrary 43 | false 44 | v143 45 | true 46 | Unicode 47 | 48 | 49 | StaticLibrary 50 | true 51 | v143 52 | Unicode 53 | 54 | 55 | StaticLibrary 56 | false 57 | v143 58 | true 59 | Unicode 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | Level3 82 | true 83 | WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) 84 | true 85 | Use 86 | pch.h 87 | 88 | 89 | 90 | 91 | true 92 | 93 | 94 | 95 | 96 | Level3 97 | true 98 | true 99 | true 100 | WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) 101 | true 102 | Use 103 | pch.h 104 | 105 | 106 | 107 | 108 | true 109 | true 110 | true 111 | 112 | 113 | 114 | 115 | Level3 116 | true 117 | _DEBUG;_LIB;%(PreprocessorDefinitions) 118 | true 119 | NotUsing 120 | pch.h 121 | 122 | 123 | 124 | 125 | true 126 | 127 | 128 | 129 | 130 | Level3 131 | true 132 | true 133 | true 134 | NDEBUG;_LIB;%(PreprocessorDefinitions) 135 | true 136 | Use 137 | pch.h 138 | 139 | 140 | 141 | 142 | true 143 | true 144 | true 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /code/生成动态库/生成动态库.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 16.0 29 | Win32Proj 30 | {dd5efdac-c055-48a7-a432-b2b85ef92cb5} 31 | 生成动态库 32 | 10.0 33 | 34 | 35 | 36 | DynamicLibrary 37 | true 38 | v143 39 | Unicode 40 | 41 | 42 | DynamicLibrary 43 | false 44 | v143 45 | true 46 | Unicode 47 | 48 | 49 | DynamicLibrary 50 | true 51 | v143 52 | Unicode 53 | 54 | 55 | DynamicLibrary 56 | false 57 | v143 58 | true 59 | Unicode 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | Level3 82 | true 83 | WIN32;_DEBUG;MY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 84 | true 85 | Use 86 | pch.h 87 | 88 | 89 | Windows 90 | true 91 | false 92 | 93 | 94 | 95 | 96 | Level3 97 | true 98 | true 99 | true 100 | WIN32;NDEBUG;MY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 101 | true 102 | Use 103 | pch.h 104 | 105 | 106 | Windows 107 | true 108 | true 109 | true 110 | false 111 | 112 | 113 | 114 | 115 | Level3 116 | true 117 | _DEBUG;MY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 118 | true 119 | NotUsing 120 | pch.h 121 | 122 | 123 | Windows 124 | true 125 | false 126 | 127 | 128 | 129 | 130 | Level3 131 | true 132 | true 133 | true 134 | NDEBUG;MY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 135 | true 136 | Use 137 | pch.h 138 | 139 | 140 | Windows 141 | true 142 | true 143 | true 144 | false 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/09待决名.md: -------------------------------------------------------------------------------- 1 | # 待决名 2 | 3 | 本节,我们开始讲待决名 4 | 5 | 待决名在模板当中无处不在,只要你写模板,一定遇到并且处理过它,即使你可能是第一次听到“待决名”这个名字。 6 | 7 | 待决名是你学习模板中重要的一个阶段,以此就可以划分,“新手写模板”和”正常写模板“,**我们不但要知道,怎么写,不能怎么写,还要知道,为什么?** 8 | 9 | >在模板(类模板和函数模板)定义中,某些构造的含义可以在不同的实例化间有所不同。特别是,类型和表达式可能会取决于类型模板形参的类型和常量模板形参的值。 10 | 11 | 循序渐进,待决名的规则非常繁杂,我们用一个一个示例为你慢慢展示(只讲常见与重要的),从简单到困难。 12 | 13 | ## 待决名的 `typename` 消除歧义符 14 | 15 | > **在模板(包括别名模版)的声明或定义中,不是当前实例化的成员且取决于某个模板形参的名字不会被认为是类型,除非使用关键词 typename 或它已经被设立为类型名(例如用 typedef 声明或通过用作基类名)**。 16 | 17 | 先用一个示例引入问题。 18 | 19 | ```cpp 20 | template 21 | const T::type& f(const T&) { 22 | return 0; 23 | } 24 | 25 | struct X{ 26 | using type = int; 27 | }; 28 | 29 | int main(){ 30 | X x; 31 | f(x); 32 | } 33 | ``` 34 | 35 | 以上代码会产生编译错误。 36 | 37 | msvc 报错提示: 38 | 39 | ```txt 40 | error C2061: 语法错误: 标识符“type” 41 | error C2143: 语法错误: 缺少“;”(在“{”的前面) 42 | error C2447: “{”: 缺少函数标题(是否是老式的形式表?) 43 | error C2065: “x”: 未声明的标识符 44 | error C3861: “f”: 找不到标识符 45 | ``` 46 | 47 | [gcc](https://godbolt.org/z/W8oMMavnY) : 48 | 49 | ```txt 50 | 51 | :2:7: error: need 'typename' before 'T::type' because 'T' is a dependent scope 52 | 2 | const T::type& f(const T&) { 53 | | ^ 54 | | typename 55 | : In function 'int main()': 56 | :12:5: error: 'f' was not declared in this scope 57 | 12 | f(x); 58 | | ^ 59 | ``` 60 | 61 | 总的意思也很简单,**编译器不觉得你这个 `type` 是一个类型**。 62 | 63 | 我知道此时,很多人会想到使用一个关键字:**`typename`**。 64 | 65 | 我们只需要在 `T::type&` 前面加上 `typename` 就能够通过编译。 66 | 67 | ```cpp 68 | template 69 | const typename T::type& f(const T&) { 70 | return 0; 71 | } 72 | ``` 73 | 74 | 我们使用这个函数模板,来套一下,一句一句分析我们最开始说的那些概念。 75 | 76 | > 在模板(包括别名模版)的声明或定义中 77 | 78 | 我们函数模板 `f` 自然是在模板中,符合。 79 | 80 | > 不是当前实例化的成员且取决于某个模板形参的名字 81 | 82 | 我们的 `T::type` 的确不是当前实例化的成员,当前实例化的是函数模板 `f`;`T::type` 的确是取决于我们的模板形参的名字,简单的说就是 `T::type` 是什么,取决于当前的函数模板。符合。 83 | 84 | > 不会被认为是类型 85 | 86 | 是的,所以我们前面没有使用 `typename` 产生了编译错误。 87 | 88 | > 除非使用关键词 typename 或它已经被设立为类型名(例如用 typedef 声明或通过用作基类名) 89 | 90 | 是的,我们后面的示例使用了 `typename` 就没有问题了,`T::type` 被认为是类型了。 91 | 92 | --- 93 | 94 | ```cpp 95 | int p = 1; 96 | 97 | template 98 | void foo(const std::vector& v){ 99 | // std::vector::const_iterator 是待决名, 100 | typename std::vector::const_iterator it = v.begin(); 101 | 102 | // 下列内容因为没有 'typename' 而会被解析成 103 | // 类型待决的成员变量 'const_iterator' 和某变量 'p' 的乘法。 104 | // 因为在此处有一个可见的全局 'p',所以此模板定义能编译。 105 | std::vector::const_iterator* p; 106 | 107 | typedef typename std::vector::const_iterator iter_t; 108 | iter_t* p2; // iter_t 是待决名,但已知它是类型名 109 | } 110 | 111 | int main(){ 112 | std::vectorv; 113 | foo(v); // 实例化失败 114 | } 115 | ``` 116 | 117 | 这个就不逐句解释了,就说一下最后一句注释: 118 | 119 | iter_t 是待决名,但已知它是类型名 120 | 121 | > 除非使用关键词 typename 或它**已经被设立为类型名**(例如用 typedef 声明或通过用作基类名) 122 | 123 | 值得一提的是,只有在添加 `foo(v)`,即进行模板实例化后 gcc/clang 才会拒绝该程序; 124 | 如果你测试过 msvc 的话,会注意到,`typedef typename std::vector::const_iterator iter_t;` 这一句,即使不加 `typename` 一样可以通过编译; 125 | 126 | msvc 搞特殊,我们知道就行;不要只测 msvc,不然代码不可跨平台。 127 | 128 | > 关键词 typename 只能以这种方式用于限定名(例如 T::x)之前,但这些名字**不必待决**。 129 | 130 | `v` 不是待决名,但是的确可以以 `typename` 修饰,虽然没啥用处。 131 | 132 | ```cpp 133 | typename std::vector::value_type v; 134 | ``` 135 | 136 | 到此,**`typename`** 待决名消除歧义符,我们已经讲清楚了所有的概念,其他各种使用无非都是从这些概念上的,不会有什么特殊。 137 | 138 | ## 待决名的 `template` 消除歧义符 139 | 140 | > **与此相似,模板定义中不是当前实例化的成员的待决名同样不被认为是模板名,除非使用消歧义关键词 template,或它已被设立为模板名:** 141 | 142 | ```cpp 143 | template 144 | struct S{ 145 | template 146 | void foo() {} 147 | }; 148 | 149 | template 150 | void bar(){ 151 | S s; 152 | s.foo(); // 错误:< 被解析为小于运算符 153 | s.template foo(); // OK 154 | } 155 | ``` 156 | 157 | 使用 `template` 消除歧义更加少见一点,不过我们一句一句分析就行。 158 | 159 | > 模板定义中不是当前实例化的成员的待决名同样不被认为是模板名 160 | 161 | `s.foo()` 的确是在模板定义,并且不是当前实例化的成员,它只是依赖了当前的模板实参 `T` ,所以不被认为是模板名。 162 | 163 | > 除非使用消歧义关键词 template 164 | 165 | `s.template foo()` 的确。 166 | 167 | 注意:`s.foo()` 在 msvc 可以被解析,通过编译,这是非标准的,知道即可。 168 | 169 | --- 170 | 171 | 关键词 `template` 只能以这种方式用于运算符 `::`(作用域解析)、`->`(通过指针的成员访问)和 `.`(成员访问)之后,下列表达式都是合法示例: 172 | 173 | - `T::template foo();` 174 | - `s.template foo();` 175 | - `this->template foo();` 176 | - `typename T::template iterator::value_type v;` 177 | 178 | 与 typename 的情况一样,即使名字并非待决或它的使用并未在模板的作用域中出现,也允许使用 template 前缀。 179 | 180 | ```cpp 181 | struct X{ 182 | template 183 | void f()const {} 184 | }; 185 | struct C{ 186 | using Ctype = int; 187 | }; 188 | 189 | X x; 190 | x.template f(); 191 | C::template Ctype I; 192 | ``` 193 | 194 | *没有作用,但是合法*。 195 | 196 | 重复强调一下:**`template` 的使用比 `typename` 少,并且 `template` 只能用于 `::`、`->`、`.` 三个运算符 *之后*。** 197 | 198 | ## 绑定规则 199 | 200 | > **对待决名和非待决名的名字查找和绑定有所不同**。 201 | > 202 | > **非待决名在模板定义点查找并绑定。即使在模板实例化点有更好的匹配,也保持此绑定** 203 | 204 | [名字查找](https://zh.cppreference.com/w/cpp/language/lookup)有非常复杂的规则,尤其还和待决名掺杂在一起,但是我们却不得不讲。 205 | 206 | ```cpp 207 | #include 208 | 209 | void g(double) { std::cout << "g(double)\n"; } 210 | 211 | template 212 | struct S{ 213 | void f() const{ 214 | g(1); // "g" 是非待决名,现在绑定 215 | } 216 | }; 217 | 218 | void g(int) { std::cout << "g(int)\n"; } 219 | 220 | int main(){ 221 | g(1); // 调用 g(int) 222 | 223 | S s; 224 | s.f(); // 调用 g(double) 225 | } 226 | ``` 227 | 228 | `s.f()` 中调用的是 `g(1);` 按照一般直觉会选择到 `void g(int)`,但是实际却不是如此,它调用了 `g(double)`。 229 | 230 | > 非待决名在模板定义点查找并绑定。即使在模板实例化点有更好的匹配,也保持此绑定 231 | 232 | ## 查找规则 233 | 234 | 我们用 [**loser homework**](https://github.com/Mq-b/Loser-HomeWork#09-名字查找的问题) 第九题,引入我们的问题。 235 | 236 | ```cpp 237 | #include 238 | 239 | template 240 | struct X { 241 | void f()const { std::cout << "X\n"; } 242 | }; 243 | 244 | void f() { std::cout << "全局\n"; } 245 | 246 | template 247 | struct Y : X { 248 | void t()const { 249 | this->f(); 250 | } 251 | void t2()const { 252 | f(); 253 | } 254 | }; 255 | 256 | int main() { 257 | Yy; 258 | y.t(); 259 | y.t2(); 260 | } 261 | ``` 262 | 263 | 以上代码的运行结果是: 264 | 265 | ```txt 266 | X 267 | 全局 268 | ``` 269 | 270 | 名字查找分为:[**有限定**名字查找](https://zh.cppreference.com/w/cpp/language/qualified_lookup),[**无限定**名字查找](https://zh.cppreference.com/w/cpp/language/unqualified_lookup)。 271 | 272 | > 有限定名字查找指? 273 | > 274 | > 出现在作用域解析操作符 `::` 右边的名字是限定名(参阅有限定的标识符)。 限定名可能代表的是: 275 | > 276 | > - 类的成员(包括静态和非静态函数、类型和模板等) 277 | > - 命名空间的成员(包括其他的命名空间) 278 | > - 枚举项 279 | 280 | 如果 `::` 左边为空,那么查找过程只会考虑全局命名空间作用域中作出(或通过 using 声明引入到全局命名空间中)的声明。 281 | 282 | ```cpp 283 | this->f(); 284 | ``` 285 | 286 | 那么显然,这个表达式**不是有限定名字查找**,那么我们就去[无限定名字查找](https://zh.cppreference.com/w/cpp/language/unqualified_lookup)中寻找答案。 287 | 288 | 我们找到**模板定义**: 289 | 290 | > 对于在模板的定义中所使用的**非待决名**,当**检查该模板的定义时将进行无限定的名字查找**。在这个位置与声明之间的绑定并不会受到在实例化点可见的声明的影响。而对于在模板定义中所使用的**待决名**,**它的查找会推迟到得知它的模板实参之时**。此时,ADL 将同时在模板的定义语境和在模板的实例化语境中检查可见的具有外部连接的 (C++11 前)函数声明,而非 ADL 的查找只会检查在模板的定义语境中可见的具有外部连接的 (C++11 前)函数声明。(换句话说,在模板定义之后添加新的函数声明,除非通过 ADL 否则仍是不可见的。)如果在 ADL 查找所检查的命名空间中,在某个别的翻译单元中声明了一个具有外部连接的更好的匹配声明,或者如果当同样检查这些翻译单元时其查找会导致歧义,那么行为未定义。无论哪种情况,**如果某个基类取决于某个模板形参,那么无限定名字查找不会检查它的作用域(在定义点和实例化点都不会)**。 291 | 292 | 很长,但是看我们加粗的就够: 293 | 294 | - 非待决名:检查该模板的定义时将进行无限定的名字查找 295 | - 待决名:它的查找会推迟到得知它的模板实参之时 296 | 297 | 我们这里简单描述一下: 298 | 299 | `this->f()` 是一个待决名,这个 `this` 依赖于模板 `X`。 300 | 301 | 所以,我们的问题可以解决了吗? 302 | 303 | 1. `this->f()` **是待决名**,所以它的查找会推迟到得知它模板实参之时(届时可以确定父类是否有 f 函数)。 304 | 2. `f()` **是非待决名**,检查该模板的定义时将进行无限定的名字查找(无法查找父类的定义),按照正常的查看顺序,先类内(查找不到),然后全局(找到)。 305 | 306 | >补充:如果是 `msvc` 的某些早期版本,或者 C++ 版本设置在 C++20 之前,会打印 `X` `X`。这是因为 `msvc` 不支持**二阶段名字查找** [`Two-phase name lookup`](https://learn.microsoft.com/zh-cn/archive/blogs/c/msvc%E5%B7%B2%E7%BB%8F%E6%94%AF%E6%8C%81two-phase-name-lookup)。 307 | 308 | ## 总结 309 | 310 | 我们省略了很多的规则,这很正常,着重聊了几个重点,这足够各位的使用,如果还有需求,查阅[文档](https://zh.cppreference.com/w/cpp/language/dependent_name)。 311 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/04模板全特化.md: -------------------------------------------------------------------------------- 1 | # 模板全特化 2 | 3 | 本节将介绍模板全特化。 4 | 5 | 其实很多东西都能进行全特化,不过我们围绕着之前的内容:函数模板、类模板、变量模板来展开。 6 | 7 | ## 函数模板全特化 8 | 9 | 给出这样一个函数模板 `f`,你可以看到,它的逻辑是返回两个对象相加的结果,那么如果我有一个需求:“**如果我传的是一个 double 一个 int 类型,那么就让它们返回相减的结果**”。 10 | 11 | ```cpp 12 | template 13 | auto f(const T& a, const T2& b) { 14 | return a + b; 15 | } 16 | ``` 17 | 18 | > C++14 允许函数返回声明的 auto 占位符自行推导类型。 19 | 20 | 这种定制的需求很常见,此时我们就需要使用到模板全特化: 21 | 22 | ```cpp 23 | template<> 24 | auto f(const double& a, const int& b){ 25 | return a - b; 26 | } 27 | ``` 28 | 29 | > 当特化函数模板时,如果模板实参推导能通过函数实参提供,那么就**可以忽略它的模板实参**。 30 | 31 | 语法很简单,只需要先写一个 `template<>` 后面再实现这个函数即可。 32 | 33 | --- 34 | 35 | 不过我们其实有两种写法的,比如上面那个示例,我们还可以写明模板实参。 36 | 37 | ```cpp 38 | template<> 39 | auto f(const double& a, const int& b) { 40 | return a - b; 41 | } 42 | ``` 43 | 44 | 个人建议写明更加明确,因为很多时候模板实参只是函数形参类型的**一部分**而已,比如上面的 `const double&`、`const int&` 只有 `double` 、`int` 是模板实参。 45 | 46 | [使用](https://godbolt.org/z/G9bzsTP5a): 47 | 48 | ```cpp 49 | std::cout << f(2, 1) << '\n'; // 3 50 | std::cout << f(2.1, 1) << '\n'; // 1.1 51 | ``` 52 | 53 | ## 类模板全特化 54 | 55 | 和函数模板一样,类模板一样可以进行全特化。 56 | 57 | ```cpp 58 | template // 主模板 59 | struct is_void{ 60 | static constexpr bool value = false; 61 | }; 62 | template<> // 对 T = void 的显式特化 63 | struct is_void{ 64 | static constexpr bool value = true; 65 | }; 66 | 67 | int main(){ 68 | std::cout <::value << '\n'; // false 69 | std::cout << std::boolalpha << is_void::value << '\n'; // true 70 | } 71 | ``` 72 | 73 | 我们使用全特化,实现了一个 `is_void` 判断模板类型实参是不是 `void` 类型。 74 | 75 | 虽然很简单,但我们还是稍微强调一下:同一个类模板实例化的不同的类,彼此之间毫无关系,而静态数据成员是属于类的,而不是模板类;模板类实例化的不同的类,它们的静态数据成员不是同一个,请注意。 76 | 77 | --- 78 | 79 | 我们知道标准库在 `C++17` 引入了 `is_xxx` 的 `_v` 的版本,就不需要再写 `::value` 了。所以我们也可以这么做,这会使用到变量模板。 80 | 81 | ```cpp 82 | #include 83 | 84 | template // 主模板 85 | struct is_void{ 86 | static constexpr bool value = false; 87 | }; 88 | template<> // 对 T = void 的显式特化 89 | struct is_void{ 90 | static constexpr bool value = true; 91 | }; 92 | 93 | template 94 | constexpr bool is_void_v = is_void::value; 95 | 96 | int main(){ 97 | std::cout < << '\n'; // false 98 | std::cout << std::boolalpha << is_void_v << '\n'; // true 99 | } 100 | ``` 101 | 102 | --- 103 | 104 | 我们再给出一个简单的示例: 105 | 106 | ```cpp 107 | template 108 | struct X{ 109 | void f()const{ 110 | puts("f"); 111 | } 112 | }; 113 | 114 | template<> 115 | struct X{ 116 | void f()const{ 117 | puts("X"); 118 | } 119 | void f2()const{} 120 | 121 | int n; 122 | }; 123 | 124 | int main(){ 125 | X x; 126 | X x_i; 127 | x.f(); // 打印 f 128 | //x.f2(); // Error! 129 | x_i.f(); // 打印 X 130 | x_i.f2(); 131 | } 132 | ``` 133 | 134 | 我们要明白,写一个类的全特化,就相当于写一个新的类一样,你可以自己定义任何东西,不管是函数、数据成员、静态数据成员,等等;根据自己的需求。 135 | 136 | ## 变量模板全特化 137 | 138 | ```cpp 139 | #include 140 | 141 | template 142 | constexpr const char* s = "??"; 143 | 144 | template<> 145 | constexpr const char* s = "void"; 146 | 147 | template<> 148 | constexpr const char* s = "int"; 149 | 150 | int main(){ 151 | std::cout << s << '\n'; // void 152 | std::cout << s << '\n'; // int 153 | std::cout << s << '\n'; // ?? 154 | } 155 | ``` 156 | 157 | 语法形式和前面函数模板、类模板都类似,很简单,这个变量模板是类型形参。我们特化了变量模板 `s` 的模板实参为 `void` 与 `int` 的情况,修改 `s` 的初始化器,让它的值不同。 158 | 159 | ```cpp 160 | template 161 | constexpr bool is_void_v = false; 162 | 163 | template<> 164 | constexpr bool is_void_v = true; 165 | 166 | int main(){ 167 | std::cout << std::boolalpha << is_void_v << '\n'; // false 168 | std::cout << std::boolalpha << is_void_v << '\n'; // true 169 | } 170 | ``` 171 | 172 | 上面的变量模板,模板是类型形参,我们根据类型进行全特化。我们特化了 `is_void_v` 的模板实参为 `void` 的情况,让 `is_void_v` 值 为 `true`。 173 | 174 | ## 细节 175 | 176 | 前面函数、类、变量模板的全特化都讲的很简单,示例也很简单,或者说语法本身大多数时候就是简单的。我们在这里进行一些更多的**补充一些细节**。 177 | 178 | --- 179 | 180 | **特化必须在导致隐式实例化的首次使用之前**,在每个发生这种使用的翻译单元中声明: 181 | 182 | ```cpp 183 | template // 主模板 184 | void f(const T&){} 185 | 186 | void f2(){ 187 | f(1); // 使用模板 f() 隐式实例化 f 188 | } 189 | 190 | template<> // 错误 f 的显式特化在隐式实例化之后出现 191 | void f(const int&){} 192 | ``` 193 | 194 | 如果 f2 中的调用换成 `f(1.)` 就[没问题](https://godbolt.org/z/WrM73Pr7f),它隐式实例化的就是 `f` 了。 195 | 196 | --- 197 | 198 | 只有声明没有定义的模板特化可以像其他不完整类型一样使用(例如可以使用到它的指针和引用): 199 | 200 | ```cpp 201 | template // 主模板 202 | class X; 203 | template<> // 特化(声明,不定义) 204 | class X; 205 | 206 | X* p; // OK:指向不完整类型的指针 207 | X x; // 错误:不完整类型的对象 208 | ``` 209 | 210 | --- 211 | 212 | 函数模板和变量模板的显式特化是否为 [inline](https://zh.cppreference.com/w/cpp/language/inline)/[constexpr](https://zh.cppreference.com/w/cpp/language/constexpr)/[constinit](https://zh.cppreference.com/w/cpp/language/constinit)/[consteval](https://zh.cppreference.com/w/cpp/language/consteval) **只与显式特化自身有关**,**主模板的声明是否带有对应说明符对它没有影响**。模板声明中出现的[属性](https://zh.cppreference.com/w/cpp/language/attributes)在它的显式特化中也没有效果: 213 | 214 | ```cpp 215 | template 216 | int f(T) { return 6; } 217 | template<> 218 | constexpr int f(int) { return 6; } // OK,f 是以 constexpr 修饰的 219 | 220 | template 221 | constexpr T g(T) { return 6; } // 这里声明的 constexpr 修饰函数模板是无效的 222 | template<> 223 | int g(int) { return 6; } //OK,g 不是以 constexpr 修饰的 224 | 225 | int main(){ 226 | constexpr auto n = f(0); // OK,f 是以 constexpr 修饰的,可以编译期求值 227 | //constexpr auto n2 = f(0); // Error! f 不可编译期求值 228 | 229 | //constexpr auto n3 = g(0); // Error! 函数模板 g 不可编译期求值 230 | 231 | constexpr auto n4 = g(0); // OK! 函数模板 g 可编译期求值 232 | } 233 | ``` 234 | 235 | [可通过编译](https://godbolt.org/z/67Wevdoch)。 236 | 237 | 如果主模板有 constexpr 属性,那么模板实例化的,如 `g` 自然也是附带了 `constexpr`,但是***如果其特化没有,那么以特化为准***(如 `g`)。 238 | 239 | --- 240 | 241 | ## 特化的成员 242 | 243 | 特化成员的写法略显繁杂,但是只要明白其逻辑,一切就会很简单。 244 | 245 | > **主模板** 246 | 247 | ```cpp 248 | template 249 | struct A{ 250 | struct B {}; // 成员类 251 | 252 | template // 成员类模板 253 | struct C {}; 254 | }; 255 | ``` 256 | 257 | > **特化模板类**。`A`。 258 | 259 | ```cpp 260 | template<> 261 | struct A{ 262 | void f(); // 类内声明 263 | }; 264 | 265 | void A::f(){ // 类外定义 266 | // todo.. 267 | } 268 | ``` 269 | 270 | > **特化成员类**。设置 `A` 的情况下 `B` 类的定义。 271 | 272 | ```cpp 273 | template<> 274 | struct A::B{ // 特化 A::B 275 | void f(); // 类内声明 276 | }; 277 | 278 | void A::B::f(){ // 类外定义 279 | // todo.. 280 | } 281 | ``` 282 | 283 | > **特化成员类模板**。设置 `A` 情况下模板类 `C` 的定义。 284 | 285 | ```cpp 286 | template<> 287 | template 288 | struct A::C{ 289 | void f(); // 类内声明 290 | }; 291 | // template<> 会用于定义被特化为类模板的显式特化的成员类模板的成员 292 | template<> 293 | template 294 | void A::C::f(){ // 类外定义 295 | // todo.. 296 | } 297 | ``` 298 | 299 | --- 300 | 301 | > **特化类的成员函数模板** 302 | 303 | 其实语法和普通特化函数模板没什么区别,类外的话那就指明函数模板是在 X 类中。 304 | 305 | ```cpp 306 | struct X{ 307 | template 308 | void f(T){} 309 | 310 | template<> // 类内特化 311 | void f(int){ 312 | std::puts("int"); 313 | } 314 | }; 315 | 316 | template<> // 类外特化 317 | void X::f(double){ 318 | std::puts("void"); 319 | } 320 | 321 | X x; 322 | x.f(1); // int 323 | x.f(1.2); // double 324 | x.f(""); 325 | ``` 326 | 327 | --- 328 | 329 | > **特化类模板的成员函数模板** 330 | 331 | 成员或成员模板可以在多个外围类模板内嵌套。在这种成员的显式特化中,对每个显式特化的外围类模板都有一个 **`template<>`**。 332 | 333 | 其实就是注意有几层那就多套几个 `template<>`,并且指明模板类的模板实参。下面这样:就是自定义了 `X` 且 `f` 的情况下的函数。 334 | 335 | ```cpp 336 | template 337 | struct X { 338 | template 339 | void f(T2) {} 340 | 341 | template<> 342 | void f(int) { // 类内特化,对于 函数模板 f 的情况 343 | std::puts("f(int)"); 344 | } 345 | }; 346 | 347 | template<> 348 | template<> 349 | void X::f(double) { // 类外特化,对于 X::f 的情况 350 | std::puts("X::f"); 351 | } 352 | 353 | X x; 354 | x.f(1); // f(int) 355 | x.f(1.2); // X::f 356 | x.f(""); 357 | ``` 358 | 359 | > 视频中的代码,模板类和成员函数模板都用的 ***`T`***,只能在 `msvc` 下运行,gcc 与 clang 有歧义,**需要注意**。 360 | 361 | > **类内对成员函数 `f` 的特化**,在 `gcc` [**无法通过编译**](https://godbolt.org/z/qYGjGhhPE),根据考察,这是一个很多年前就有的 `BUG`,使用 `gcc` 的开发者自行注意。 362 | 363 | ## 总结 364 | 365 | 我们省略了一些内容,但是以上在我看来也完全足够各位学习使用了。如有需求,查看 [cppreference](https://zh.cppreference.com/w/cpp/language/template_specialization)。 366 | 367 | 模板全特化的语法主要核心在于 `template<>`,以及你需要注意,你到底要写几个 `template<>`。其他的都很简单。 368 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/02类模板.md: -------------------------------------------------------------------------------- 1 | # 类模板 2 | 3 | 本节将介绍类模板 4 | 5 | ## 初识类模板 6 | 7 | 类模板不是类,只有实例化类模板,编译器才能生成实际的类。 8 | 9 | ### 定义类模板 10 | 11 | 下面是一个类模板,它和普通类的区别只是多了一个 `template` 12 | 13 | ```cpp 14 | template 15 | struct Test{}; 16 | ``` 17 | 18 | 和函数模板一样,其实类模板的语法也就是: 19 | 20 | ```cpp 21 | template< 形参列表 > 类声明 22 | ``` 23 | 24 | 几乎所有我们前面讲的,**函数模板中形参列表能写的东西,类模板都可以**。 25 | 26 | 同样的,我们的类模板一样可以用 `class` 引入类型形参名,一样不能用 `struct` 27 | 28 | ```cpp 29 | template 30 | struct Test{}; 31 | ``` 32 | 33 | ### 使用类模板 34 | 35 | 下面展示了如何使用类模板 `Test` 36 | 37 | ```cpp 38 | template 39 | struct Test {}; 40 | 41 | int main(){ 42 | Test t; 43 | Test t2; 44 | //Test t; // Error! 45 | } 46 | ``` 47 | 48 | 我们必须显式的指明类模板的类型实参,并且没有办法推导,事实上这个空类在这里本身没什么意义。 49 | 50 | 或许我们可以这样: 51 | 52 | ```cpp 53 | template 54 | struct Test{ 55 | T t; 56 | }; 57 | ``` 58 | 59 | 这理所应当,类模板能使用类模板形参,声明自己的成员,那么如何使用呢? 60 | 61 | ```cpp 62 | // Test t; // Error! 63 | Test t2; 64 | // Test t3; // Error! 65 | Test t4{ 1 }; // C++17 OK! 66 | ``` 67 | 68 | - `Test` 我们稍微带入一下,模板的 `T` 是 `void` 那 `T t` 是?所以很合理 69 | - `Test t4{ 1 };` C++17 增加了类模板实参推导,也就是说类模板也可以像函数模板一样被推导,而不需要显式的写明模板类型参数了,这里的 `Test` 被推导为 `Test`。 70 | 71 | 不单单是聚合体,当然,写构造函数也可以: 72 | 73 | ```cpp 74 | template 75 | struct Test{ 76 | Test(T v) :t{ v } {} 77 | private: 78 | T t; 79 | }; 80 | ``` 81 | 82 | ## 类模板参数推导 83 | 84 | 这涉及到一些非常复杂的规则,不过我们不用在意。 85 | 86 | 对于简单的类模板,通常可以普通的类似函数模板一样的自动推导,比如前面提到的 `Test` 类型,又或者下面: 87 | 88 | ```cpp 89 | template 90 | struct A{ 91 | A(T, T); 92 | }; 93 | auto y = new A{1, 2}; // 分配的类型是 A 94 | ``` 95 | 96 | new 表达式中一样可以。 97 | 98 | 同样的可以像函数模板那样加上许多的修饰: 99 | 100 | ```cpp 101 | template 102 | struct A { 103 | A(const T&, const T&); 104 | }; 105 | ``` 106 | 107 | 多的就不用再提。 108 | 109 | ### 用户定义的推导指引 110 | 111 | 举个例子,我要让一个类模板,如果推导为 int,就让它实际成为 size_t: 112 | 113 | ```cpp 114 | template 115 | struct Test{ 116 | Test(T v) :t{ v } {} 117 | private: 118 | T t; 119 | }; 120 | 121 | Test(int) -> Test; 122 | 123 | Test t(1); // t 是 Test 124 | ``` 125 | 126 | 如果要类模板 `Test` 推导为指针类型,就变成数组呢? 127 | 128 | ```cpp 129 | template 130 | Test(T*) -> Test; 131 | 132 | char* p = nullptr; 133 | 134 | Test t(p); // t 是 Test 135 | ``` 136 | 137 | 推导指引的语法还是简单的,如果只是涉及具体类型,那么只需要: 138 | 139 | **`模板名称(类型a)->模板名称<想要让类型a被推导为的类型>`** 140 | 141 | 如果涉及的是一类类型,那么就需要加上 `template`,然后使用它的模板形参。 142 | 143 | --- 144 | 145 | 我们提一个稍微有点难度的需求: 146 | 147 | ```cpp 148 | template 149 | struct array { 150 | Ty arr[size]; 151 | }; 152 | 153 | ::array arr{1, 2, 3, 4, 5}; // Error! 154 | ``` 155 | 156 | 类模板 array 同时使用了类型模板形参与常量模板形参,保有了一个成员是数组。 157 | 158 | 它无法被我们直接推导出类型,此时就需要我们自己**定义推导指引**。 159 | 160 | 这会用到我们之前在函数模板里学习到的形参包。 161 | 162 | ```cpp 163 | template 164 | array(T t,Args...) -> array; 165 | ``` 166 | 167 | 原理很简单,我们要给出 array 的模板类型,那么就让模板形参单独写一个 T 占位,放到形参列表中,并且写一个模板类型形参包用来处理任意个参数;获取 array 的 size 也很简单,直接使用 sizeof... 获取形参包的元素个数,然后再 +1 ,因为先前我们用了一个模板形参占位。 168 | 169 | 标准库的 [`std::array`](https://zh.cppreference.com/w/cpp/container/array/deduction_guides) 的推导指引,原理和这个一样。 170 | 171 | ## 有默认实参的模板形参 172 | 173 | 和函数模板一样,类模板一样可以有默认实参。 174 | 175 | ```cpp 176 | template 177 | struct X{}; 178 | 179 | X x; // x 是 X C++17 起 OK 180 | X<> x2; // x2 是 X 181 | ``` 182 | 183 | 必须达到 **C++17** 有 [`CTAD`](https://zh.cppreference.com/w/cpp/language/class_template_argument_deduction),才可以在全局、函数作用域声明为 `X` 这种形式,才能省略 **`<>`**。 184 | 185 | 但是在类中声明一个,有默认实参的类模板类型的数据成员(静态或非静态,是否类内定义都无所谓),不管是否达到 C++17,都不能省略 `<>`。 186 | 187 | ```cpp 188 | template 189 | struct X {}; 190 | 191 | struct Test{ 192 | X x; // Error 193 | X<> x2; // OK 194 | static inline X x3; // Error 195 | }; 196 | ``` 197 | 198 | >但是 gcc13.2 有[不同行为](https://godbolt.org/z/n1EfWf9GM),开启 `std=c++17`,类内定义的静态数据成员省略 `<>` 可以通过编译。但是,总而言之,不要类内声明中省略 `<>`。 199 | 200 | ```cpp 201 | template 202 | struct X {}; 203 | 204 | struct Test{ 205 | static inline X x3; // OK 206 | }; 207 | 208 | int main(){ 209 | 210 | } 211 | ``` 212 | 213 | `MinGw clang 16.02` 与 `msvc` 均不可通过编译。 214 | 215 | 标准库中也经常使用默认实参: 216 | 217 | [`std::vector`](https://zh.cppreference.com/w/cpp/container/vector) 218 | 219 | ```cpp 220 | template< 221 | class T, 222 | class Allocator = std::allocator 223 | > class vector; 224 | ``` 225 | 226 | [`std::string`](https://zh.cppreference.com/w/cpp/string/basic_string) 227 | 228 | ```cpp 229 | template< 230 | class CharT, 231 | class Traits = std::char_traits, 232 | class Allocator = std::allocator 233 | > class basic_string; 234 | ``` 235 | 236 | 当然了,也可以给常量模板形参以默认值,虽然不是很常见: 237 | 238 | ```cpp 239 | template 240 | struct Arr 241 | { 242 | T arr[N]; 243 | }; 244 | 245 | Arr x; // x 是 Arr 它保有一个成员 int arr[10] 246 | ``` 247 | 248 | 知道这些即可,这很合理,毕竟函数模板可以,你类模板也可以。 249 | 250 | ## 常量模板形参 251 | 252 | 前面其实已经提了,像 `std::array` 都是有常量模板形参的,这没有什么问题,类似于函数模板。 253 | 254 | ## 模板模板形参 255 | 256 | 类模板的模板类型形参可以接受一个类模板作为参数,我们将它称为:模板模板形参。 257 | 258 | 先随便给出一个简单的示例: 259 | 260 | ```cpp 261 | template 262 | struct X {}; 263 | 264 | template typename C> 265 | struct Test {}; 266 | 267 | Testarr; 268 | ``` 269 | 270 | 模板模板形参的语法略微有些复杂,我们需要理解一下,先把外层的 `template<>` 去掉。 271 | 272 | `template typename C` 我们分两部分看就好 273 | 274 | - 前面的 `template` 就是我们要接受的类模板它的模板列表,是需要一模一样的,比如类模板 X 就是。 275 | 276 | - 后面的 `typename` 是语法要求,需要声明这个模板模板形参的名字,可以自定义,这样就引入了一个模板模板形参。 277 | 278 | --- 279 | 280 | 下面是详细的语法形式: 281 | 282 | ```txt 283 | template < 形参列表 > typename(C++17)|class 名字(可选) (1) 284 | template < 形参列表 > typename(C++17)|class 名字(可选) = default (2) 285 | template < 形参列表 > typename(C++17)|class ... 名字(可选) (3) (C++11 起) 286 | ``` 287 | 288 | 1. **可以有名字的模板模板形参**。 289 | 290 | ```cpp 291 | template 292 | struct my_array{ 293 | T arr[10]; 294 | }; 295 | 296 | template typename C > 297 | struct Array { 298 | Carray; 299 | }; 300 | 301 | Arrayarr; // arr 保有的成员是 my_array 而它保有了 int arr[10] 302 | ``` 303 | 304 | 2. **有默认模板且可以有名字的模板模板形参**。 305 | 306 | ```cpp 307 | template 308 | struct my_array{ 309 | T arr[10]; 310 | }; 311 | 312 | template typename C = my_array > 313 | struct Array { 314 | Carray; 315 | }; 316 | 317 | Arrayarr; // arr 的类型同(1),模板模板形参一样可以有 默认值 318 | ``` 319 | 320 | 3. **可以有名字的模板模板形参包**。 321 | 322 | > 其实就是形参包的一种,能接受任意个数的类模板 323 | > 324 | ```cpp 325 | template 326 | struct X{}; 327 | 328 | template 329 | struct X2 {}; 330 | 331 | templatetypename...Ts> 332 | struct Test{}; 333 | 334 | Testt; // 我们可以传递任意个数的模板实参 335 | ``` 336 | 337 | --- 338 | 339 | 当然了,模板模板形参也可以和常量模板形参一起使用,都是一样的,比如: 340 | 341 | ```cpp 342 | template 343 | struct X {}; 344 | 345 | template typename C> 346 | struct Test {}; 347 | 348 | Testarr; 349 | ``` 350 | 351 | 注意到了吗?我们省略了其中 `template` 常量模板形参的名字,可能通常会写成 `template` ,我们只是为了表达这是可以省略了,看自己的需求。 352 | 353 | --- 354 | 355 | 对于普通的有形参包的类模板也都是同理: 356 | 357 | ```cpp 358 | template 359 | struct my_array{ 360 | int arr[sizeof...(T)]; // 保有的数组大小根据模板类型形参的元素个数 361 | }; 362 | 363 | template typename C = my_array > 364 | struct Array { 365 | Carray; 366 | }; 367 | 368 | Arrayarr; 369 | ``` 370 | 371 | ## 成员函数模板 372 | 373 | 成员函数模板基本上和普通函数模板没多大区别,唯一需要注意的是,它大致有两类: 374 | 375 | - 类模板中的成员函数模板 376 | - 普通类中的成员函数模板 377 | 378 | 需要注意的是: 379 | 380 | ```cpp 381 | template 382 | struct Class_template{ 383 | void f(T) {} 384 | }; 385 | ``` 386 | 387 | `Class_template` 的成员函数 f,它不是函数模板,它就是普通的成员函数,在类模板实例化为具体类型的时候,成员函数也被实例化为具体。 388 | 389 | 1. **类模板中的成员函数模板** 390 | 391 | ```cpp 392 | template 393 | struct Class_template{ 394 | template 395 | void f(Args&&...args) {} 396 | }; 397 | ``` 398 | 399 | `f` 就是成员函数模板,通常写起来和普通函数模板没多大区别,大部分也都 支持,比如形参包。 400 | 401 | 2. **普通类中的成员函数模板** 402 | 403 | ```cpp 404 | struct Test{ 405 | template 406 | void f(Args&&...args){} 407 | }; 408 | ``` 409 | 410 | `f` 就是成员函数模板,没什么问题。 411 | 412 | --- 413 | 414 | 其实都是字面意思,很好理解,上面的示例都没什么实际的使用,都是语法展示,我相信明白函数模板就自然能明白这些。 415 | 416 | ## 可变参数类模板 417 | 418 | 形参包与包展开等知识,在类模板中是通用的。 419 | 420 | ```cpp 421 | template 422 | struct X { 423 | X(Args...args) :value{ args... } {} // 参数展开 424 | std::tuplevalue; // 类型形参包展开 425 | }; 426 | 427 | X x{ 1,"2",'3',4. }; // x 的类型是 X 428 | std::cout << std::get<1>(x.value) << '\n'; // 2 429 | ``` 430 | 431 | [`std::tuple`](https://zh.cppreference.com/w/cpp/utility/tuple) 是一个模板类,我们用来存储任意类型任意个数的参数,我们指明它的模板实参是使用的模板的类型形参包展开,`std::tuple` 展开后成为 `std::tuple` 。 432 | 433 | 构造函数中使用成员初始化列表来初始化成员 value,没什么问题,正常展开。 434 | 435 | 需要注意的是字符串字面量的类型是 `const char[N]` ,之所以被推导为 `const char*` 在于数组之间不能“拷贝”。它隐式转换为了指向数组首地址的指针,类型自然也被推导为 `const char*`。 436 | 437 | ```cpp 438 | int arr[1]{1}; 439 | int arr2[2]{1,2}; 440 | arr = arr2; // Error! 441 | ``` 442 | 443 | ```cpp 444 | int a = 0; 445 | int b = a; // OK! 446 | 447 | int arr[1]{1}; 448 | int arr2[1] = arr; // Error! 449 | int arr3[1] = {arr}; // Error! 450 | ``` 451 | 452 | ## 类模板分文件 453 | 454 | 和前面提到的函数模板分文件的原因一样,类模板也没有办法分文件。 455 | 456 | 我们给出了一个[项目示例](/code/03类模板分文件),展示类模板通常分文件的情况。 457 | 458 | 通常就是统一写到 `.h` 文件中,或者大家约定俗成了一个 `.hpp` 后缀,这个通常用来放模板。 459 | 460 | > 我们后面会单独做一个内容处理这些情况。 461 | 462 | ## 总结 463 | 464 | 类模板的知识远不止如此,不过目前也足够使用了,后续还会有补充。 465 | 466 | 我们写的类模板的内容没有函数模板那么多,主要在于很多内容是和函数模板重复的,很多特性彼此之间是相通的,我们就没必要讲那么多,所以需要注意,不要跳着看。 467 | -------------------------------------------------------------------------------- /md/第二部分-造轮子/Linux中封装POSIX接口编写thread类.md: -------------------------------------------------------------------------------- 1 | # Linux 中封装 POSIX 接口编写 thread 类 2 | 3 | 在我们上一节内容其实就提到了 `std::thread` 的 msvc stl 的实现,本质上就是封装了 win32 的那些接口,包括其中最重要的就是利用了模板技术接受任意可调用类型,将其转发给 C 的只是接受单一函数指针的 `_beginthreadex` 去创建线程。 4 | 5 | 上一节中我们只是讲了单纯的讲了 “*模板包装C风格API进行调用*”,这一节我们就来实际一点,直接封装编写一个自己的 `std::thread`,使用 POSIX 接口。 6 | 7 | 我们将在 Ubuntu22.04 中使用 gcc11.4 开启 C++17 标准进行编写和测试。 8 | 9 | ## 实现 10 | 11 | ### 搭建框架 12 | 13 | ```cpp 14 | namespace mq_b{ 15 | class thread{ 16 | public: 17 | class id; 18 | 19 | id get_id() const noexcept; 20 | }; 21 | 22 | namespace this_thread { 23 | [[nodiscard]] thread::id get_id() noexcept; 24 | } 25 | 26 | class thread::id { 27 | public: 28 | id() noexcept = default; 29 | 30 | private: 31 | explicit id(pthread_t other_id) noexcept : Id(other_id) {} 32 | 33 | pthread_t Id; 34 | 35 | friend thread::id thread::get_id() const noexcept; 36 | friend thread::id this_thread::get_id() noexcept; 37 | friend bool operator==(thread::id left, thread::id right) noexcept; 38 | 39 | template 40 | friend std::basic_ostream& operator<<(std::basic_ostream& str, thread::id Id); 41 | }; 42 | 43 | [[nodiscard]] inline thread::id thread::get_id() const noexcept { 44 | return thread::id{ Id }; 45 | } 46 | 47 | [[nodiscard]] inline thread::id this_thread::get_id() noexcept { 48 | return thread::id{ pthread_self() }; 49 | } 50 | 51 | template 52 | std::basic_ostream& operator<<(std::basic_ostream& str, thread::id Id){ 53 | str << Id.Id; 54 | return str; 55 | } 56 | } 57 | ``` 58 | 59 | 我们先搭建一个基础的架子给各位, 定义我们自己的命名空间,定义内部类 `thread::id`,以及运算符重载。这些并不涉及什么模板技术,我们先进行编写,其实重点只是构造函数而已,剩下的其它成员函数也很简单,我们一步一步来。 60 | 61 | 另外,**POSIX 的线程函数都只需要一个句柄,其实就是无符号 int 类型就能操作**,别名是 `pthread_t` 所以我们只需要保有这样一个 id 就好了。 62 | 63 | ### 实现构造函数 64 | 65 | ```cpp 66 | template 67 | thread(Fn&& func, Args&&... args) { 68 | using Tuple = std::tuple, std::decay_t...>; 69 | auto Decay_copied = std::make_unique(std::forward(func), std::forward(args)...); 70 | auto Invoker_proc = start(std::make_index_sequence<1 + sizeof...(Args)>{}); 71 | if (int result = pthread_create(&Id, nullptr, Invoker_proc, Decay_copied.get()); result == 0) { 72 | (void)Decay_copied.release(); 73 | }else{ 74 | std::cerr << "Error creating thread: " << strerror(result) << std::endl; 75 | throw std::runtime_error("Error creating thread"); 76 | } 77 | } 78 | template 79 | static constexpr auto start(std::index_sequence) noexcept { 80 | return &Invoke; 81 | } 82 | 83 | template 84 | static void* Invoke(void* RawVals) noexcept { 85 | const std::unique_ptr FnVals(static_cast(RawVals)); 86 | Tuple& Tup = *FnVals.get(); 87 | std::invoke(std::move(std::get(Tup))...); 88 | nullptr 0; 89 | } 90 | ``` 91 | 92 | 这其实很简单,几乎是直接复制了我们上一节的内容,只是把函数改成了 `pthread_create`,然后多传了两个参数,以及修改了 Invoke 的返回类型和 return,确保类型符合 `pthread_create` 。 93 | 94 | ### 完善其它成员函数 95 | 96 | 然后再稍加完善那些简单的成员函数,也就是: 97 | 98 | ```cpp 99 | ~thread(){ 100 | if (joinable()) 101 | std::terminate(); 102 | } 103 | 104 | thread(const thread&) = delete; 105 | 106 | thread& operator=(const thread&) = delete; 107 | 108 | thread(thread&& other) noexcept : Id(std::exchange(other.Id, {})) {} 109 | 110 | thread& operator=(thread&& t) noexcept{ 111 | if (joinable()) 112 | std::terminate(); 113 | swap(t); 114 | return *this; 115 | } 116 | 117 | void swap(thread& t) noexcept{ 118 | std::swap(Id, t.Id); 119 | } 120 | 121 | bool joinable() const noexcept{ 122 | return !(Id == 0); 123 | } 124 | 125 | void join() { 126 | if (!joinable()) { 127 | throw std::runtime_error("Thread is not joinable"); 128 | } 129 | int result = pthread_join(Id, nullptr); 130 | if (result != 0) { 131 | throw std::runtime_error("Error joining thread: " + std::string(strerror(result))); 132 | } 133 | Id = {}; // Reset thread id 134 | } 135 | 136 | void detach() { 137 | if (!joinable()) { 138 | throw std::runtime_error("Thread is not joinable or already detached"); 139 | } 140 | int result = pthread_detach(Id); 141 | if (result != 0) { 142 | throw std::runtime_error("Error detaching thread: " + std::string(strerror(result))); 143 | } 144 | Id = {}; // Reset thread id 145 | } 146 | 147 | id get_id() const noexcept; 148 | 149 | native_handle_type native_handle() const{ 150 | return Id; 151 | } 152 | ``` 153 | 154 | 我觉得无需多言,这些都十分的简单。然后就完成了,对,就是这么简单。 155 | 156 | ### 完整代码与测试 157 | 158 | **完整实现代码**: 159 | 160 | ```cpp 161 | namespace mq_b{ 162 | class thread{ 163 | public: 164 | class id; 165 | using native_handle_type = pthread_t; 166 | 167 | thread() noexcept :Id{} {} 168 | 169 | template 170 | thread(Fn&& func, Args&&... args) { 171 | using Tuple = std::tuple, std::decay_t...>; 172 | auto Decay_copied = std::make_unique(std::forward(func), std::forward(args)...); 173 | auto Invoker_proc = start(std::make_index_sequence<1 + sizeof...(Args)>{}); 174 | if (int result = pthread_create(&Id, nullptr, Invoker_proc, Decay_copied.get()); result == 0) { 175 | (void)Decay_copied.release(); 176 | }else{ 177 | std::cerr << "Error creating thread: " << strerror(result) << std::endl; 178 | throw std::runtime_error("Error creating thread"); 179 | } 180 | } 181 | template 182 | static constexpr auto start(std::index_sequence) noexcept { 183 | return &Invoke; 184 | } 185 | 186 | template 187 | static void* Invoke(void* RawVals) noexcept { 188 | const std::unique_ptr FnVals(static_cast(RawVals)); 189 | Tuple& Tup = *FnVals.get(); 190 | std::invoke(std::move(std::get(Tup))...); 191 | return nullptr; 192 | } 193 | 194 | ~thread(){ 195 | if (joinable()) 196 | std::terminate(); 197 | } 198 | 199 | thread(const thread&) = delete; 200 | 201 | thread& operator=(const thread&) = delete; 202 | 203 | thread(thread&& other) noexcept : Id(std::exchange(other.Id, {})) {} 204 | 205 | thread& operator=(thread&& t) noexcept{ 206 | if (joinable()) 207 | std::terminate(); 208 | swap(t); 209 | return *this; 210 | } 211 | 212 | void swap(thread& t) noexcept{ 213 | std::swap(Id, t.Id); 214 | } 215 | 216 | bool joinable() const noexcept{ 217 | return !(Id == 0); 218 | } 219 | 220 | void join() { 221 | if (!joinable()) { 222 | throw std::runtime_error("Thread is not joinable"); 223 | } 224 | int result = pthread_join(Id, nullptr); 225 | if (result != 0) { 226 | throw std::runtime_error("Error joining thread: " + std::string(strerror(result))); 227 | } 228 | Id = {}; // Reset thread id 229 | } 230 | 231 | void detach() { 232 | if (!joinable()) { 233 | throw std::runtime_error("Thread is not joinable or already detached"); 234 | } 235 | int result = pthread_detach(Id); 236 | if (result != 0) { 237 | throw std::runtime_error("Error detaching thread: " + std::string(strerror(result))); 238 | } 239 | Id = {}; // Reset thread id 240 | } 241 | 242 | id get_id() const noexcept; 243 | 244 | native_handle_type native_handle() const{ 245 | return Id; 246 | } 247 | 248 | pthread_t Id; 249 | }; 250 | 251 | namespace this_thread { 252 | [[nodiscard]] thread::id get_id() noexcept; 253 | } 254 | 255 | class thread::id { 256 | public: 257 | id() noexcept = default; 258 | 259 | private: 260 | explicit id(pthread_t other_id) noexcept : Id(other_id) {} 261 | 262 | pthread_t Id; 263 | 264 | friend thread::id thread::get_id() const noexcept; 265 | friend thread::id this_thread::get_id() noexcept; 266 | friend bool operator==(thread::id left, thread::id right) noexcept; 267 | 268 | template 269 | friend std::basic_ostream& operator<<(std::basic_ostream& str, thread::id Id); 270 | }; 271 | 272 | [[nodiscard]] inline thread::id thread::get_id() const noexcept { 273 | return thread::id{ Id }; 274 | } 275 | 276 | [[nodiscard]] inline thread::id this_thread::get_id() noexcept { 277 | return thread::id{ pthread_self() }; 278 | } 279 | 280 | template 281 | std::basic_ostream& operator<<(std::basic_ostream& str, thread::id Id){ 282 | str << Id.Id; 283 | return str; 284 | } 285 | } 286 | ``` 287 | 288 | **标头**: 289 | 290 | ```cpp 291 | #include 292 | #include 293 | #include 294 | #include 295 | #include 296 | #include 297 | #include 298 | #include 299 | ``` 300 | 301 | **测试**: 302 | 303 | ```cpp 304 | void func(int& a) { 305 | std::cout << &a << '\n'; 306 | } 307 | void func2(const int& a){ 308 | std::cout << &a << '\n'; 309 | } 310 | struct X{ 311 | void f() { std::cout << "X::f\n"; } 312 | }; 313 | 314 | int main(){ 315 | std::cout << "main thread id: " << mq_b::this_thread::get_id() << '\n'; 316 | 317 | int a = 10; 318 | std::cout << &a << '\n'; 319 | mq_b::thread t{ func,std::ref(a) }; 320 | t.join(); 321 | 322 | mq_b::thread t2{ func2,a }; 323 | t2.join(); 324 | 325 | mq_b::thread t3{ [] {std::cout << "thread id: " << mq_b::this_thread::get_id() << '\n'; } }; 326 | t3.join(); 327 | 328 | X x; 329 | mq_b::thread t4{ &X::f,&x }; 330 | t4.join(); 331 | 332 | mq_b::thread{ [] {std::cout << "👉🤣\n"; } }.detach(); 333 | sleep(1); 334 | } 335 | ``` 336 | 337 | > [运行](https://godbolt.org/z/1j48Mh89x)测试。 338 | 339 | 项目: 340 | 341 | ## 总结 342 | 343 | 其实这玩意没多少难度,唯一的难度就只有那个构造函数而已,剩下的代码和成员函数,甚至可以照着标准库抄一些,或者就是调用 POSIX 接口罢了。 344 | 345 | 不过如果各位能完全理解明白,那也足以自傲,毕竟的确没多少人懂。简单是相对而言的,如果你跟着视频一直学习了前面的模板,并且有基本的并发的知识,对 `POSIX` 接口有基本的认识,以及看了前面提到的 [**《`std::thread` 的构造-源码解析》**](https://mq-b.github.io/ModernCpp-ConcurrentProgramming-Tutorial/md/%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90/01thread%E7%9A%84%E6%9E%84%E9%80%A0%E4%B8%8E%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.html),那么本节的内容对你,肯定不构成难度。 346 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/10了解与利用SFINAE.md: -------------------------------------------------------------------------------- 1 | # SFINAE 2 | 3 | “代换失败不是错误” (Substitution Failure Is Not An Error) 4 | 5 | 在**函数模板的重载决议**[^1]中会应用此规则:当模板形参在替换成显式指定的类型或推导出的类型失败时,从重载集中丢弃这个特化,*而非导致编译失败*。 6 | 7 | 此特性被用于模板元编程。 8 | 9 | > 注意:**本节非常非常的重要,是模板基础中的基础,最为基本的特性和概念**。 10 | 11 | ## 解释 12 | 13 | 对函数模板形参进行两次代换(由模板实参所替代): 14 | 15 | - 在模板实参推导前,对显式指定的模板实参进行代换 16 | 17 | - 在模板实参推导后,对推导出的实参和从默认项获得的实参进行替换 18 | 19 | 代换的实参写出时非良构[^2](并带有必要的诊断)的任何场合,都是*代换失败*。 20 | 21 | > ”对显式指定的模板实参进行代换“这里的显式指定,就比如 `f()` 就是显式指明了。我知道你肯定有疑问:我都显式指明了,那下面还推导啥?对,如果模板函数 `f` 只有一个模板形参,而你显式指明了,的确第二次代换没用,因为根本没啥好推导的。 22 | 23 | > 两次代换都有作用,是在于有多个模板形参,显式指定一些,又根据传入参数推导一些。 24 | 25 | ## 代换失败与硬错误 26 | 27 | > **只有在函数类型或其模板形参类型或其 explicit 说明符 (C++20 起)的*立即语境*中的类型与表达式中的失败,才是 *SFINAE 错误*。如果对代换后的类型/表达式的*求值导致副作用*,例如实例化某模板特化、生成某隐式定义的成员函数等,那么这些副作用中的错误都被当做*硬错误***。 28 | 29 | > 代换失败就是指 SFINAE 错误。 30 | 31 | 以上概念中注意关键词“SFINAE 错误”、“硬错误”,这些解释不用在意,先看完以下示例再去看概念理解。 32 | 33 | ```cpp 34 | #include 35 | 36 | template 37 | struct B { using type = typename A::type; }; // 待决名,C++20 之前必须使用 typename 消除歧义 38 | 39 | template< 40 | class T, 41 | class U = typename T::type, // 如果 T 没有成员 type 那么就是 SFINAE 失败(代换失败) 42 | class V = typename B::type> // 如果 T 没有成员 type 那么就是硬错误 不过标准保证这里不会发生硬错误,因为到 U 的默认模板实参中的代换会首先失败 43 | void foo(int) { std::puts("SFINAE T::type B::type"); } 44 | 45 | template 46 | void foo(double) { std::puts("SFINAE T"); } 47 | 48 | int main(){ 49 | struct C { using type = int; }; 50 | 51 | foo>(1); // void foo(int) 输出: SFINAE T::type B::type 52 | foo(1); // void foo(double) 输出: SFINAE T 53 | } 54 | ``` 55 | 56 | 全平台[测试通过](https://godbolt.org/z/88bPesedP)。 57 | 58 | 以上的示例很好的向我们展示了 SFINAE 的作用,可以影响重载决议。 59 | 60 | `foo>(1)`、`foo(1)` 如果根据一般直觉,它们都会选择到 `void foo(int)`,然而实际却不是如此; 61 | 62 | 这是因为 `foo(1);` 去尝试匹配 `void foo(int)` 的时候,模板实参类型 `void` 进行替换,就会变成: 63 | 64 | ```cpp 65 | template< 66 | class void, 67 | class U = typename void::type, // SFINAE 失败 68 | class V = typename B::type> // 不会发生硬错误,因为 U 的代换已经失败 69 | ``` 70 | 71 | **`void::type`** 这一看就是非良构[^2],根据前面提到的: 72 | 73 | > 代换的实参写出时非良构(并带有必要的诊断)的任何场合,都是代换失败。 74 | 75 | 所以这是一个代换失败,但是因为“*代换失败不是错误*”,只是从“*重载集中丢弃这个特化,而不会导致编译失败*”,然后就就去尝试匹配 `void foo(double)` 了,`1` 是 int 类型,隐式转换到 double,没什么问题。 76 | 77 | 至于其中提到的*硬错误*?为啥它是硬错误?其实最开始的概念已经说了: 78 | 79 | > 如果对代换后的类型/表达式的求值导致副作用,例如实例化某模板特化、生成某隐式定义的成员函数等,那么这些副作用中的错误都被当做硬错误。 80 | 81 | `B` 显然是对代换后的类型求值导致了副作用,实例化了模板,实例化失败自然被当做硬错误。 82 | 83 | > 注意,你应当关注 `B` 而非 `B::type`,因为是直接在实例化模板 B 的时候就失败了,被当成硬错误;如果 `B` 实例化成功,而没有 `::type`,则被当成**代换失败**(不过这里是不可能)。 84 | 85 | --- 86 | 87 | 这节内容非常重要,提到的概念和代码需要全部掌握,后面的内容其实无非都是以本节为基础的变种、各种使用示例、利用标准库的设施让写法简单一点,但是根本的原理,就是本节讲的。 88 | 89 | ## 基础使用示例 90 | 91 | *请在完全读懂上一节内容再阅读本节内容*。 92 | 93 | C++ 的模板,很多时候就像拼图一样,我们带入进去想,很多问题即使没有阅读规则,也可以无师自通,猜出来。 94 | 95 | --- 96 | 97 | > ***我需要写一个函数模板 `add`,想要要求传入的对象必须是支持 `operator+` 的,应该怎么写?*** 98 | 99 | 利用 SFINAE 我们能轻松完成这个需求。 100 | 101 | ```cpp 102 | template 103 | auto add(const T& t1, const T& t2) -> decltype(t1 + t2){ // C++11 后置返回类型,在返回类型中运用 SFINAE 104 | std::puts("SFINAE +"); 105 | return t1 + t2; 106 | } 107 | ``` 108 | 109 | 我知道你一定会有疑问 110 | 111 | > 1. 这样有啥好处吗?使用了 SFINAE 看起来还变复杂了。我就算不用这写法,如果对象没有 `operator+` 不是一样会编译错误吗? 112 | > 2. 虽然前面说了 SFINAE 可以影响重载决议,我知道这个很有用,但是我这个函数根本没有别的重载,这样写还是有必要的吗? 113 | 114 | 这两个问题其实是一个问题,本质上就是还是不够懂 SFINAE 或者说模板: 115 | 116 | - 如果就是简单写一个 `add` 函数模板不使用 SFINAE,那么编译器在编译的时候,会尝试模板实例化,生成函数定义,发现你这类型根本没有 `operator+`,于是实例化模板错误。 117 | 118 | - 如果按照我们上面的写法使用 SFINAE,根据“*代换失败不是错误*”的规则,从重载集中丢弃这个特化 `add`,然而又没有其他的 `add` 重载,所以这里的错误是“**未找到匹配的重载函数**”。 119 | 120 | 这里的重点是什么?**是模板实例化,能不要实例化就不要实例化**,我们当前的示例只是因为 `add` 函数模板非常的简单,即使实例化错误,编译器依然可以很轻松的报错告诉你,是因为没有 `operator+`。但是很多模板是非常复杂的,编译器实例化模板经常会产生一些完全不可读的报错;如果我们使用 SFINAE,编译器就是直接告诉我:“未找到匹配的重载函数”,我们自然知道就是传入的参数没有满足要求。而且实例化模板也是有开销的,很多时候甚至很大。 121 | 122 | 总而言之: 123 | **即使不为了处理重载,使用 SFINAE 约束函数模板的传入类型,也是有很大好处的:报错、编译速度**。 124 | 125 | 但是令人诟病的是 SFINAE 的写法在很多时候非常麻烦,目前各位可能还是没有感觉,后面的需求,写出的示例,慢慢的你就会感觉到了。这些问题会在下一章的[约束与概念](/md/第一部分-基础知识/11约束与概念.md)解决。 126 | 127 | ## 标准库支持 128 | 129 | 标准库提供了一些设施帮助我们更好的使用 SFINAE。 130 | 131 | ### `std::enable_if` 132 | 133 | ```cpp 134 | template 135 | struct enable_if {}; 136 | 137 | template // 类模板偏特化 138 | struct enable_if { typedef T type; }; // 只有 B 为 true,才有 type,即 ::type 才合法 139 | 140 | template< bool B, class T = void > 141 | using enable_if_t = typename enable_if::type; // C++14 引入 142 | ``` 143 | 144 | 这是一个模板类,在 C++11 引入,它的用法很简单,就是第一个模板参数为 true,此模板类就有 `type`,不然就没有,以此进行 SFINAE。 145 | 146 | ```cpp 147 | template>> 149 | void f(T){} 150 | ``` 151 | 152 | 函数 `f` 要求 `T` 类型必须是 `int` 类型;我们一步一步分析 153 | 154 | `std::enable_if_t>>` 如果 T 不是 int,那么 [std::is_same_v](https://zh.cppreference.com/w/cpp/types/is_same) 就会返回一个 false,也就是说 `std::enable_if_t` ,带入: 155 | 156 | ```cpp 157 | using enable_if_t = typename enable_if::type; // void 是默认模板实参 158 | ``` 159 | 160 | 但是问题在于: 161 | 162 | - **enable_if 如果第一个模板参数为 `false`,它根本没有 `type` 成员**。 163 | 164 | 所以这里是个**代换失败**,但是因为“*代换失败不是错误*”,所以只是不选择函数模板 `f`,而不会导致编译错误。 165 | 166 | --- 167 | 168 | 再谈,`std::enable_if` 的默认模板实参是 **`void`**,如果我们不在乎 `std::enable_if` 得到的类型,就让它默认就行,比如我们先前的示例 `f` 根本不在乎第二个模板形参 `SFINAE` 是啥类型。 169 | 170 | 此外,`std::enable_if` 还有一种常见的用法,即利用其第二个模板参数而不声明额外的模板类型参数。例如: 171 | 172 | ```cpp 173 | template,int> =0> 175 | void f(T){} 176 | ``` 177 | 178 | 它的作用和之前的写法是一样的,但这个写法的原理是什么呢?我们可以逐步解析: 179 | 180 | ```cpp 181 | std::enable_if_t,int> =0 182 | ``` 183 | 184 | 这里的 `=0` 实际上是对前面 `enable_if_t` 表达式的默认实参,它起到的是无名默认实参的作用。也就是说,如果 `std::is_same_v` 为 true,那么 `std::enable_if_t` 变为: 185 | 186 | ```cpp 187 | using enable_if_t = typename enable_if::type; 188 | ``` 189 | 190 | `true` 会选择 enable_if 的偏特化,从而**有 `type` 别名**,它的类型就是是我们传入的第二个参数 `int`。因此,**`std::enable_if_t` 实际上就是 int**。 191 | 192 | 当然,如果 `std::is_same_v` 为 `false`,则 `std::enable_if_t` 会导致**代换失败**。 193 | 不过因为“代换失败不是错误”,所以只是不选择函数模板 `f`,而不会导致编译错误。(当然了,如果没有一个符合条件的重载,那还是会报编译错误的:“*未找到匹配的重载函数*”)。 194 | 195 | --- 196 | 197 | ```cpp 198 | template 199 | array(Type, Args...) -> array && ...), Type>, sizeof...(Args) + 1>; 200 | ``` 201 | 202 | 以上示例,是显式指明了 `std::enable_if` 的第二个模板实参,为 `Type`。 203 | 204 | 它是我们[类模板](02类模板.md)推导指引那一节的示例的**改进版本**,我们使用 std::enable_if_t 与 C++17 折叠表达式,为它增加了约束,这几乎和 [libstdc++](https://github.com/gcc-mirror/gcc/blob/7a01cc711f33530436712a5bfd18f8457a68ea1f/libstdc%2B%2B-v3/include/std/array#L292-L295) 中的代码一样。 205 | 206 | `(std::is_same_v && ...)` 做 std::enable_if 的第一个模板实参,这里是一个一元右折叠,使用了 **`&&`** 运算符,也就是必须 std::is_same_v 全部为 true,才会是 true。简单的说就是要求类型形参包 Args 中的每一个类型全部都是一样的,不然就是替换失败。 207 | 208 | 这样做有很多好处,老式写法存在很多问题: 209 | 210 | ```cpp 211 | template 212 | struct array { 213 | Ty arr[size]; 214 | }; 215 | 216 | template 217 | array(T t, Args...) -> array; 218 | 219 | ::array arr{1.4, 2, 3, 4, 5}; // 被推导为 array 220 | ::array arr2{1, 2.3, 3.4, 4.5, 5.6}; // 被推导为 array 有数据截断 221 | ``` 222 | 223 | 如果不使用 SFINAE 约束,那么 array 的类型完全取决于第一个参数的类型,很容易导致其他问题。 224 | 225 | ### `std::void_t` 226 | 227 | ```cpp 228 | template< class... > 229 | using void_t = void; 230 | ``` 231 | 232 | 如你所见,它的实现非常非常的简单,就是一个别名,接受任意个数的类型参数,但自身始终是 `void` 类型。 233 | 234 | - 将任意类型的序列映射到类型 void 的工具元函数。 235 | 236 | - 模板元编程中,用此元函数检测 SFINAE 语境中的非良构类型[^2]。 237 | 238 | --- 239 | 240 | > *我要写一个函数模板 `add`,我要求传入的对象需要支持 `+` 以及它需要有别名 `type` ,成员 `value`、`f`*。 241 | 242 | ```cpp 243 | #include 244 | #include 245 | 246 | template> 249 | auto add(const T& t1, const T& t2) { 250 | std::puts("SFINAE + | typename T::type | T::value"); 251 | return t1 + t2; 252 | } 253 | 254 | struct Test { 255 | int operator+(const Test& t)const { 256 | return this->value + t.value; 257 | } 258 | void f()const{} 259 | using type = void; 260 | int value; 261 | }; 262 | 263 | int main() { 264 | Test t{ 1 }, t2{ 2 }; 265 | add(t, t2); // OK 266 | //add(1, 2); // 未找到匹配的重载函数 267 | } 268 | ``` 269 | 270 | - `decltype(T{} + T{})` 用 decltype 套起来只是为了获得类型符合语法罢了,std::void_t 只接受类型参数。如果类型没有 `operator+`,自然是*代换失败*。 271 | 272 | - `typename T::type` 使用 `typename` 是因为[待决名](09待决名.md);type 本身是类型,不需要 decltype。如果 `add` 推导的类型没有 `type` 别名,自然是*代换失败*。 273 | 274 | - `decltype(&T::value)` 用 decltype 套就不用说了,`&T::value` 是[成员指针](https://zh.cppreference.com/w/cpp/language/pointer#.E6.88.90.E5.91.98.E6.8C.87.E9.92.88)的语法,不区分是数据成员还是成员函数,如果有这个成员 `value`,`&类名::成员名字` 自然合法,要是没有,就是*代换失败*。 275 | 276 | - `decltype(&T::f)` ,其实前面已经说了,成员函数是没区别的,没有成员 `f` 就是 *代换失败*。 277 | 278 | 总而言之,这是为了使用 SFINAE。 279 | 280 | > 那么这里 `std::void_t` 的作用是? 281 | 282 | 其实倒也没啥,无非就是给了个好的语境,让我们能这样写,最终 `typename SFINAE = std::void_t` 这里的 `SFINAE` 的类型就是 `void`;当然了,这不重要,重要的是创造这样写的语境,能够方便我们进行 **`SFINAE`**。 283 | 284 | 仅此一个示例,我相信就足够展示 `std::void_t` 的使用了。 285 | 286 | > *那么如果在 C++17 标准之前,没有 std::void_t ,我该如何要求类型有某些成员呢?* 287 | 288 | 其实形式和原理都是一样的。 289 | 290 | ```cpp 291 | template 292 | void f(T){} 293 | 294 | struct Test { 295 | void f()const{} 296 | }; 297 | 298 | Test t; 299 | f(t); // OK 300 | f(1); // 未找到匹配的重载函数 301 | ``` 302 | 303 | C++11 可用。 304 | 305 | ### `std::declval` 306 | 307 | ```cpp 308 | template 309 | typename std::add_rvalue_reference::type declval() noexcept; 310 | ``` 311 | 312 | 将任意类型 T 转换成引用类型,*使得在 decltype 说明符的操作数中不必经过构造函数就能使用成员函数*。 313 | 314 | - [std::declval](https://zh.cppreference.com/w/cpp/utility/declval) 只能用于 **[不求值语境](https://zh.cppreference.com/w/cpp/language/expressions#.E6.BD.9C.E5.9C.A8.E6.B1.82.E5.80.BC.E8.A1.A8.E8.BE.BE.E5.BC.8F)**,且不要求有定义。 315 | 316 | - **它不能被实际调用,因此不会返回值,返回类型是 `T&&`**。 317 | 318 | 它常用于模板元编程 SFINAE,我们用一个示例展现它的必要性: 319 | 320 | ```cpp 321 | template > 322 | auto add(const T& t1, const T& t2) { 323 | std::puts("SFINAE +"); 324 | return t1 + t2; 325 | } 326 | 327 | struct X{ 328 | int operator+(const X&)const{ 329 | return 0; 330 | } 331 | }; 332 | 333 | struct X2 { 334 | X2(int){} // 有参构造,没有默认构造函数 335 | int operator+(const X2&)const { 336 | return 0; 337 | } 338 | }; 339 | 340 | int main(){ 341 | X x1, x2; 342 | add(x1, x2); // OK 343 | 344 | X2 x3{ 0 }, x4{ 0 }; 345 | add(x3,x4); // 未找到匹配的重载函数 346 | } 347 | ``` 348 | 349 | 错误的原因很简单,`decltype(T{} + T{})` 这个表达式中,同时**要求了 `T` 类型支持默认构造**(虽然这不是我们的本意),然而我们的 `X2` 类型没有默认构造,自然而然 `T{}` 不是合法表达式,*代换失败*。其实我们之前也有类似的写法,我们在本节进行纠正,使用 `std::declval`: 350 | 351 | ```cpp 352 | template() + std::declval())> > 353 | auto add(const T& t1, const T& t2) { 354 | std::puts("SFINAE +"); 355 | return t1 + t2; 356 | } 357 | ``` 358 | 359 | [测试](https://godbolt.org/z/7GGWvd5PM)。 360 | 361 | 把 `T{}` 改成 `std::declval()` 即可,decltype 是不求值语境,没有问题。 362 | 363 | --- 364 | 365 | 还不止如此,使用它得以让我们先前的 `SFINAE` 检查类型是否有某些成员的形式得以改进,而不是像之前一样的 `decltype(&T::value), decltype(&T::f)` 的利用成员指针的形式。 366 | 367 | ```cpp 368 | template().f(1))> 369 | void f(int) { std::puts("f int"); } 370 | 371 | template().f())> 372 | void f(double) { std::puts("f"); } 373 | 374 | struct X{ 375 | void f()const{} 376 | }; 377 | struct Y{ 378 | void f(int)const{} 379 | }; 380 | 381 | int main(){ 382 | f(1); 383 | f(1.1); 384 | } 385 | ``` 386 | 387 | [**运行结果**](https://godbolt.org/z/vvdWKKM5n): 388 | 389 | ```txt 390 | f 391 | f int 392 | ``` 393 | 394 | 显而易见,虽然我们的 `f(1)` 传递的参数是 int 类型,但是却打印了 `f`,也就是代表实际匹配到了参数为 `f(double)` 的版本,这是因为我们的 `f(int)` 版本的 `SFINAE` 约束要求了类型必须是支持 `f(1)` 这种形式,`X` 的成员函数 `f` 是空参的,自然不满足。 395 | 396 | **使用此种方式得以更加明确的约束,因为不管成员函数 f 的形参是什么情况,其成员指针表示形式都是:`&类名::f`。** 397 | 398 | 数据成员同样可以使用 `declval` 进行约束: 399 | 400 | ```cpp 401 | template().value)> 402 | void f(int) { std::puts("f value"); } 403 | 404 | template 405 | void f(double) { std::puts("f"); } 406 | 407 | struct X { 408 | int value{}; 409 | }; 410 | struct Y {}; 411 | 412 | int main() { 413 | f(1); // f value 414 | f(1); // f 415 | } 416 | ``` 417 | 418 | [**运行结果**](https://godbolt.org/z/c3ahK1WxP): 419 | 420 | ```txt 421 | f value 422 | f 423 | ``` 424 | 425 | `f(1)` 虽然传递的参数是 int 类型,但是因为 `Y` 不满足 `SFINAE` 的约束,即没有成员 `value`,所以只能选择到 `f(double)` 的版本。 426 | 427 | ## 部分(偏)特化中的 SFINAE 428 | 429 | 在确定一个类或变量 (C++14 起)模板的特化是由部分特化还是主模板生成的时候也会出现推导与替换。在这种确定期间,**部分特化的替换失败不会被当作硬错误,而是像函数模板一样*代换失败不是错误*,只是忽略这个部分特化**。 430 | 431 | ```cpp 432 | #include 433 | 434 | template 435 | struct X{ 436 | static void f() { std::puts("主模板"); } 437 | }; 438 | 439 | template 440 | struct X>{ 441 | using type = typename T::type; 442 | static void f() { std::puts("偏特化 T::type"); } 443 | }; 444 | 445 | struct Test { using type = int; }; 446 | struct Test2 { }; 447 | 448 | int main(){ 449 | X::f(); // 偏特化 T::type 450 | X::f(); // 主模板 451 | } 452 | ``` 453 | 454 | ## 总结 455 | 456 | 到此,其实就足够了,SFINAE 的原理、使用、标准库支持(std::enable_if、std::void_t、std::declval)。 457 | 458 | 虽然称不上全部,但如果你能完全理解明白本节的所有内容,那你一定超越了至少 95% C++ 开发者。其他的各种形式无非都是这样类似的,因为我们已经为你讲清楚了 ***原理***。 459 | 460 | - ***代换失败不是错误***。 461 | 462 | [^1]: 注:“[重载决议](https://zh.cppreference.com/w/cpp/language/overload_resolution)”,简单来说,一个函数被重载,编译器必须决定要调用哪个重载,我们决定调用的是各形参与各实参之间的匹配最紧密的重载。 463 | 464 | [^2]: 注:[非良构(ill-formed)](https://zh.cppreference.com/w/cpp/language/ub)——程序拥有语法错误或可诊断的语义错误。遵从标准的 C++ 编译器必须为此给出诊断。 465 | -------------------------------------------------------------------------------- /md/第一部分-基础知识/11约束与概念.md: -------------------------------------------------------------------------------- 1 | # 约束与概念 2 | 3 | 类模板,函数模板,以及非模板函数(通常是类模板的成员),可以与一项约束(constraint)相关联,它指定了对模板实参的一些要求,这些要求可以被用于选择最恰当的函数重载和模板特化。 4 | 5 | 这种要求的具名集合被称为概念(concept)。每个概念都是一个谓词,它在编译时求值,并在将之用作约束时成为模板接口的一部分。 6 | 7 | ## 前言 8 | 9 | 在 C++20 引入了约束与概念,这一核心语言特性是所有使用模板的 C++ 开发者都期待的。 10 | 11 | 有了它,我们的模板可以有更多的静态检查,语法更加美观,写法更加容易,而不再需要利用古老的 **SFINAE**。 12 | 13 | 请务必学习完了上一章节内容;本节会一边为你教学约束与概念的语法,一边用 SFINAE 对比,让你意识到:***这是多么先进、简单的核心语言特性***。 14 | 15 | ## 定义*概念*(concept)与使用 16 | 17 | ```cpp 18 | 19 | template < 模板形参列表 > 20 | concept 概念名 属性 (可选) = 约束表达式; 21 | ``` 22 | 23 | 定义概念所需要的 *约束表达式*,只需要是可以在编译期产生 `bool` 值的表达式即可。 24 | 25 | > - 你可以先不看基本概念,关注我们的示例和下面的讲解。 26 | 27 | --- 28 | 29 | > ***我需要写一个函数模板 `add`,想要要求传入的对象必须是支持 `operator+` 的,应该怎么写?*** 30 | 31 | 此需求就是 `SFINAE` 中提到的,我们使用*概念*(concept)来完成。 32 | 33 | ```cpp 34 | template 35 | concept Add = requires(T a) { 36 | a + a; // "需要表达式 a+a 是可以通过编译的有效表达式" 37 | }; 38 | 39 | template 40 | auto add(const T& t1, const T& t2){ 41 | std::puts("concept +"); 42 | return t1 + t2; 43 | } 44 | ``` 45 | 46 | 我们使用关键字 `concept` 定义了一个*概念*(concept),命名为 `Add`,它的*约束*是 `requires(T a) { a + a; }` 即要求 `f(T a)`、`a + a` 是合法表达式。 47 | 48 | ```cpp 49 | template // T 被 Add 约束 50 | ``` 51 | 52 | 语法上就是把原本的 `typename` 、`class` 换成了我们定义的 `Add` *概念*(concept),语义和作用也非常的明确: 53 | 54 | - **就是让这个概念约束模板类型形参 `T`,即要求 `T` 必须满足*约束表达式*的*要求序列* `T a` `a + a`**。如果不满足,则不会选择这个模板。 55 | 56 | > "满足":要求带入后必须是合法表达式; 57 | 58 | 最开始的概念已经说了: 59 | 60 | > *概念*(concept)可以与一项约束(constraint)相关联,它指定了对模板实参的一些要求,这些要求可以被用于选择最恰当的函数重载和模板特化。 61 | 62 | 另外最开始的概念中还说过: 63 | 64 | > 每个概念都是一个**谓词**,它在**编译时求值**,并在将之用作约束时成为模板接口的一部分。 65 | 66 | 也就是说我们其实可以这样: 67 | 68 | ```cpp 69 | std::cout << std::boolalpha << Add << '\n'; // true 70 | std::cout << std::boolalpha << Add << '\n'; // false 71 | constexpr bool r = Add; // true 72 | ``` 73 | 74 | 我相信这非常的好理解,这些语法形式,合理且简单。 75 | 76 | *记得我们在第一章节[函数模板](01函数模板.md)中提到的:“C++20 简写函数模板”吗?* 77 | 78 | ```cpp 79 | decltype(auto) max(const auto& a, const auto& b) { // const T& 80 | return a > b ? a : b; 81 | } 82 | ``` 83 | 84 | 这段代码来自函数模板那一章节。 85 | 86 | > **我想要约束:传入的对象 a b 必须都是整数类型,应该怎么写?**。 87 | 88 | ```cpp 89 | #include // C++20 概念库标头 90 | 91 | decltype(auto) max(const std::integral auto& a, const std::integral auto& b) { 92 | return a > b ? a : b; 93 | } 94 | 95 | max(1, 2); // OK 96 | max('1', '2'); // OK 97 | max(1u, 2u); // OK 98 | max(1l, 2l); // OK 99 | max(1.0, 2); // Error! 未满足关联约束 100 | ``` 101 | 102 | 如你所见,我们没有自己定义 *概念*(concept),而是使用了标准库的 [`std::integral`](https://zh.cppreference.com/w/cpp/concepts/integral),它的实现非常简单: 103 | 104 | ```cpp 105 | template< class T > 106 | concept integral = std::is_integral_v; 107 | ``` 108 | 109 | 这也告诉各位我们一件事情:**定义*概念*(concept)** 时声明的约束表达式,只需要是编译期可得到 `bool` 类型的表达式即可。 110 | 111 | > 我相信你这里一定有疑问:“那么我们之前写的 requires 表达式呢?它会返回 `bool` 值吗?” 对,简单的说,把模板参数带入到 `requires` 表达式中,是否符合语法,符合就返回 `true`,不符合就返回 `false`。在 [`requires` 表达式](#requires-表达式) 一节中会详细讲解。 112 | 113 | 它的实现是直接使用了标准库的 `std::is_integral_v`,非常简单。 114 | 115 | 再谈*概念*(concept)在简写函数模板中的写法 `const std::integral auto& a`,*概念*(concept)只需要写在 `auto` 之前即可,表示此概念约束 `auto` 推导的类型必须为整数类型,语义十分明确,像是 cv 限定、引用等,不需要在乎,或许我们可以先写的简单一点先去掉那些: 116 | 117 | ```cpp 118 | decltype(auto) max(std::integral auto a, std::integral auto b) { 119 | return a > b ? a : b; 120 | } 121 | ``` 122 | 123 | 这是否直观了很多?并且概念不单单是可以用作简写函数模板中的 `auto`,还有几乎一切语境,比如: 124 | 125 | ```cpp 126 | int f() { return 0; } 127 | 128 | std::integral auto result = f(); 129 | ``` 130 | 131 | 还是那句话,语义很明确: 132 | 133 | - ***概念*(concept)约束了 `auto` ,它必须被推导为整数类型;如果函数 `f()` 返回类型是 `double` `auto` 无法推导为整数类型,那么编译器会报错:“*未满足关联约束*”**。 134 | 135 | --- 136 | 137 | 类模板同理,如: 138 | 139 | ```cpp 140 | template 141 | concept add = requires(T t){ // 定义概念,通常推荐首字母小写 142 | t + t; 143 | }; 144 | 145 | template 146 | struct X{ 147 | T t; 148 | }; 149 | ``` 150 | 151 | 变量模板也同理 152 | 153 | ```cpp 154 | template 155 | concept add = requires(T t){ 156 | t + t; 157 | }; 158 | 159 | template 160 | T t; 161 | 162 | t; // OK 163 | t; // “t”未满足关联约束 164 | ``` 165 | 166 | 将模板中的 `typename` 、`class` 换成 *概念*(concept)即可,表示约束此模板类型形参 `T`。 167 | 168 | ## `requires` 子句 169 | 170 | 关键词 requires 用来引入 requires 子句,它指定对各模板实参,或对函数声明的约束。 171 | 172 | 也就是说我们多了一种使用*概念*(concept)或者说约束的写法。 173 | 174 | ```cpp 175 | template 176 | concept add = requires(T t) { 177 | t + t; 178 | }; 179 | 180 | template 181 | requires std::is_same_v 182 | void f(T){} 183 | 184 | template requires add 185 | void f2(T) {} 186 | 187 | template 188 | void f3(T)requires requires(T t) { t + t; } 189 | {} 190 | ``` 191 | 192 | > `requires` 子句期待一个能够编译期产生 `bool` 值的表达式。 193 | 194 | 以上示例展示了 `requires` 子句的用法,我们一个个解释 195 | 196 | 1. `f` 的 `requires` 子句写在 `template` 之后,并空四格,这是我个人推荐的写法;它的约束是:`std::is_same_v`,意思很明确,约束 `T` 必须是 int 类型,就这么简单。 197 | 2. `f2` 的 `requires` 子句写法和 `f` 其实是一样的,只是没换行和空格;它使用了我们自定义的*概念*(concept)`add`,约束 `T` 必须满足 `add`。 198 | 3. `f3` 的 `requires` 子句在函数声明符的末尾元素出现;这里我们连用了两个 `requires`,为什么?其实很简单,我们要区分,第一个 `requires` 是 *`requires` 子句*,第二个 `requires` 是*约束表达式*,它会产生一个编译期的 `bool` 值,没有问题。(如果 `T` 类型带入*约束表达式*是良构,那么就返回 `true`、否则就返回 `false`)。 199 | 200 | > 类模板、变量模板等也都同理 201 | 202 | requires 子句中,**关键词 requires 必须后随某个常量表达式**。 203 | 204 | ```cpp 205 | template 206 | requires true 207 | void f(T){} 208 | ``` 209 | 210 | 完全可行,各位其实可以直接带入,说白了 `requires` 子句引入的约束表,必须是可以编译期返回 `bool` 类型的值的表达式,我们前面的三个例子:`std::is_same_v`、`add`、`requires 表达式` 都如此。 211 | 212 | ## 约束 213 | 214 | 前面我们讲的都是非常基础的*概念*(concept)使用,它们的约束也都十分简单,本节我们详细讲一下。 215 | 216 | 约束是逻辑操作和操作数的序列,它指定了对模板实参的要求。它们可以在 requires 表达式(见下文)中出现,也可以直接作为概念的主体。 217 | 218 | 有三种类型的约束: 219 | 220 | 1. 合取(conjunction) 221 | 2. 析取(disjunction) 222 | 223 | ### 合取 224 | 225 | 两个约束的合取是通过在约束表达式中使用 && 运算符来构成的: 226 | 227 | ```cpp 228 | template 229 | concept Integral = std::is_integral_v; 230 | template 231 | concept SignedIntegral = Integral && std::is_signed_v; 232 | template 233 | concept UnsignedIntegral = Integral && !SignedIntegral; 234 | ``` 235 | 236 | 很合理,**约束表达式**可以使用 `&&` 运算符连接两个约束,只有在两个约束都被满足时才会得到满足 237 | 238 | 我们先定义了一个 *概念*(concept)Integral,此概念要求整形;又定义了*概念*(concept)SignedIntegral,它的约束表达式用到了先前定义的*概念*(concept)Integral,然后又加上了一个 **`&&`** 还需要满足 std::is_signed_v。 239 | 240 | *概念*(concept)`SignedIntegral` 是要求有符号整数类型,它的*约束表达式*是:**`Integral && std::is_signed_v`**,就是这个表达式要返回 `true` 才成立,就这么简单。 241 | 242 | ```cpp 243 | void s_f(const SignedIntegral auto&){} 244 | void u_f(const UnsignedIntegral auto&){} 245 | 246 | s_f(1); // OK 247 | s_f(1u); // 未找到匹配的重载函数 248 | u_f(1); // 未找到匹配的重载函数 249 | u_f(1u); // OK 250 | ``` 251 | 252 | > ***两个约束的合取只有在两个约束都被满足时才会得到满足**。合取从左到右短路求值(如果不满足左侧的约束,那么就不会尝试对右侧的约束进行模板实参替换:这样就会防止出现立即语境外的替换所导致的失败)*。 253 | 254 | ```cpp 255 | struct X{ 256 | int c{}; // 无意义,为了扩大 X 257 | static constexpr bool value = true; 258 | }; 259 | 260 | template 261 | constexpr bool get_value() { return T::value; } 262 | 263 | template 264 | requires (sizeof(T) > 1 && get_value()) 265 | void f(T){} 266 | 267 | X x; 268 | f(x); // OK 269 | ``` 270 | 271 | ### 析取 272 | 273 | 两个约束的析取,是通过在约束表达式中使用 || 运算符来构成的: 274 | 275 | ```cpp 276 | template 277 | concept number = std::integral || std::floating_point; 278 | ``` 279 | 280 | 和 `||` 运算符本来的意思一样, `std::integral`、`std::floating_point` 满足任意一个,那么整个约束表达式就都得到满足。 281 | 282 | ```cpp 283 | void f(const number auto&){} 284 | 285 | f(1); // OK 286 | f(1u); // OK 287 | f(1.2); // OK 288 | f(1.2f); // OK 289 | f("1"); // 未找到匹配的重载函数 290 | ``` 291 | 292 | > *如果其中一个约束得到满足,那么两个约束的析取的到满足。析取从左到右短路求值(如果满足左侧约束,**那么就不会尝试对右侧约束进行模板实参替换**)*。 293 | 294 | ```cpp 295 | struct X{ 296 | int c{}; // 无意义,为了扩大 X 297 | //static constexpr bool value = true; 298 | }; 299 | 300 | template 301 | constexpr bool get_value() { return T::value; } 302 | 303 | template 304 | requires (sizeof(T) > 1 || get_value()) 305 | void f(T){} 306 | 307 | X x; 308 | f(x); // OK 即使 X 根本没有静态 value 成员。 309 | ``` 310 | 311 | 如你所见,即使我们的 X 根本不满足右侧约束 `get_value()` 的要求,没有静态 `value` 成员,不过一样可以通过编译。 312 | 313 | ## `requires` 表达式 314 | 315 | > **产生描述约束的 bool 类型的纯右值表达式**。 316 | 317 | 虽然前面聊*概念*(concept)的时候,用到了 `requires` 表达式(定义 concept 的时候),但是没有详细说明,本节我们详细展开说明。 318 | 319 | > *注意,`requires` 表达式 和 `requires` 子句,没关系*。 320 | 321 | ```txt 322 | requires { 要求序列 } 323 | requires ( 形参列表 (可选) ) { 要求序列 } 324 | ``` 325 | 326 | 要求序列,是以下形式之一: 327 | 328 | - 简单要求 329 | - 类型要求 330 | - 复合要求 331 | - 嵌套要求 332 | 333 | ### 解释 334 | 335 | 要求可以援引处于作用域内的模板形参,形参列表中引入的局部形参,以及在上下文中可见的任何其他声明。 336 | 337 | - 将模板参数**代换**到模板化实体的声明中所使用的 requires 表达式中,*可能会导致在其要求中形成无效的类型或表达式,或者违反这些要求的语义*。在这种情况下,requires 表达式的值为 **`false`** 而不会导致程序非良构。 338 | - 按照词法顺序进行**代换**和语义约束检查,当遇到决定 requires 表达式结果的条件时就停止。*如果代换(若存在)和语义约束检查成功*,则 requires 表达式的结果为 **`true`**。 339 | 340 | > **简单的说,把模板参数带入到 requires 表达式中,是否符合语法,符合就返回 `true`,不符合就返回 `false`**。 341 | 342 | ```cpp 343 | #include 344 | 345 | template 346 | void f(T) { 347 | constexpr bool v = requires{ T::type; }; // 此处可不使用 typename 348 | std::cout << std::boolalpha << v << '\n'; 349 | } 350 | 351 | struct X { using type = void; }; 352 | struct Y { static constexpr int type = 0; }; 353 | 354 | int main() { 355 | f(1); // false 因为 int::type 不是合法表达式 356 | f(X{}); // false 因为 X::type 在待决名中不被认为是类型,需要添加 typename 357 | f(Y{}); // true 因为 Y::type 是合法表达式 358 | } 359 | ``` 360 | 361 | > 三端[测试](https://godbolt.org/z/sxf4PnsfG)。 362 | 363 | ### 简单要求 364 | 365 | 简单要求是任何不以关键词 requires 开始的表达式语句。它断言该表达式是有效的。表达式是不求值的操作数;只检查语言的正确性。 366 | 367 | ```cpp 368 | template 369 | concept Addable = requires (T a, T b) { 370 | a + b; // "需要表达式 a+b 是可以通过编译的有效表达式" 371 | }; 372 | 373 | template 374 | concept Swappable = requires(T && t, U && u) { 375 | swap(std::forward(t), std::forward(u)); 376 | swap(std::forward(u), std::forward(t)); 377 | }; 378 | 379 | template 380 | requires (Addable && Swappable) 381 | struct Test{}; 382 | 383 | namespace loser{ 384 | struct X{ 385 | X operator+(const X&)const{ 386 | return *this; 387 | } 388 | }; 389 | void swap(const X&,const X&){} 390 | } 391 | 392 | int main() { 393 | using loser::X; 394 | 395 | Test t2; // OK 396 | std::cout << std::boolalpha << Addable << '\n'; // true 397 | std::cout << std::boolalpha << Swappable << '\n'; // true 398 | } 399 | ``` 400 | 401 | > 以上代码利用了实参依赖查找(`ADL`),即 `swap(X{})` 是合法表达式,而不需要增加命名空间限定。 402 | 403 | 以关键词 requires 开始的要求总是被解释为[嵌套要求](#嵌套要求)。因此简单要求**不能以没有括号的 requires 表达式开始**。 404 | 405 | ### 类型要求 406 | 407 | 类型要求是关键词 **`typename`** 后面接一个可以被限定的**类型名称**。该要求是,所指名的类型是有效的。 408 | 409 | 可以用来验证: 410 | 411 | 1. 某个指名的嵌套类型是否存在 412 | 2. 某个类模板特化是否指名了某个类型 413 | 3. 某个别名模板特化是否指名了某个类型。 414 | 415 | ```cpp 416 | struct Test{ 417 | struct X{}; 418 | using type = int; 419 | }; 420 | 421 | template 422 | struct S{}; 423 | 424 | template 425 | using Ref = T&; 426 | 427 | template 428 | concept C = requires{ 429 | typename T::X; // 需要嵌套类型 430 | typename T::type; // 需要嵌套类型 431 | typename S; // 需要类模板特化 432 | typename Ref; // 需要别名模板代换 433 | }; 434 | 435 | std::cout << std::boolalpha << C << '\n'; // true 436 | ``` 437 | 438 | 稍微解释一下,类 `Test` 有一个嵌套类 `X`,一个别名 `type`,所以 `typename T::X`、`typename T::type` 类型是有效的。 439 | 440 | `typename S` 因为有类模板 `S`,且它接受类型模板参数,所以 `typename S` 类型是有效的。假设模板类 `S` 的模板是接受常量模板参数的,比如 `template S` ,那么 `typename S` 类型自然不是有效的。 441 | 442 | `typename Ref` 因为有别名模板 `Ref`,自然没问题,类型自然是有效的。 443 | 444 | > 其实说来说去也很简单,你就直接**带入**,把*概念*(concept)的模板实参(比如 Test)直接带入进去 `requires` 表达式,想想它是不是合法的表达式就可以了。 445 | 446 | ### 复合要求 447 | 448 | 复合要求具有如下形式 449 | 450 | ```txt 451 | { 表达式 } noexcept(可选) 返回类型要求 (可选) ; 452 | ``` 453 | 454 | > 返回类型要求:-> 类型约束(*概念* concept) 455 | 456 | 并断言所指名表达式的属性。替换和语义约束检查按以下顺序进行: 457 | 458 | 1. 模板实参 (若存在) 被替换到 表达式 中; 459 | 2. 如果使用了`noexcept`,表达式 一定不能潜在抛出; 460 | 3. 如果*返回类型要求* 存在,则: 461 | - 模板实参被替换到*返回类型要求* 中; 462 | - `decltype((表达式))` 必须满足*类型约束* 蕴含的约束。否则,被包含的 requires 表达式是 **`false`**。 463 | 464 | ```cpp 465 | template 466 | concept C2 = requires(T x){ 467 | // 表达式 *x 必须合法 468 | // 并且 类型 T::inner 必须存在 469 | // 并且 *x 的结果必须可以转换为 T::inner 470 | {*x} -> std::convertible_to; 471 | 472 | // 表达式 x + 1 必须合法 473 | // 并且 std::same_as 必须满足 474 | // 即, (x + 1) 必须为 int 类型的纯右值 475 | {x + 1} -> std::same_as; 476 | 477 | // 表达式 x * 1 必须合法 478 | // 并且 它的结果必须可以转换为 T 479 | {x * 1} -> std::convertible_to; 480 | 481 | // 复合:"x.~T()" 是不会抛出异常的合法表达式 482 | { x.~T() } noexcept; 483 | }; 484 | ``` 485 | 486 | 我们可以写一个满足*概念*(concept)`C2` 的类型: 487 | 488 | ```cpp 489 | struct X{ 490 | int operator*()const { return 0; } 491 | int operator+(int)const { return 0; } 492 | X operator*(int)const { return *this; } 493 | using inner = int; 494 | }; 495 | ``` 496 | 497 | ```cpp 498 | std::cout << std::boolalpha << C2 << '\n'; // true 499 | ``` 500 | 501 | [测试](https://godbolt.org/z/vWhcPjbe7)。 502 | 503 | **析构函数比较特殊**,不需要我们显式声明它为 `noexcept` 的,它默认就是 `noexcept` 的。 504 | 505 | 不管编译器为我们生成的 `X` 析构函数,还是我们用户显式定义的 `X` 析构函数,默认都是有 `noexcept` 的[^1]。只有我们用户定义析构函数的时候把它声明为了 `noexcept(false)` 这个析构函数才不是 `noexcept` 的,才会不满足 *概念*(concept)`C2` 的要求。 506 | 507 | [^1]: 参见[文档](https://zh.cppreference.com/w/cpp/language/destructor#:~:text=%E5%A6%82%E6%9E%9C%E6%B2%A1%E6%9C%89%E6%98%BE,C%2B%2B11%20%E8%B5%B7)。 508 | 509 | ### 嵌套要求 510 | 511 | 嵌套要求具有如下形式 512 | 513 | ```txt 514 | requires 约束表达式 ; 515 | ``` 516 | 517 | 它可用于根据本地形参指定其他约束。*约束表达式* 必须由被替换的模板实参(若存在)满足。将模板实参替换到嵌套要求中会导致替换到 *约束表达式* 中,但仅限于确定是否满足 *约束表达式* 所需的程度。 518 | 519 | ```cpp 520 | template 521 | concept C3 = requires(T a, std::size_t n) { 522 | requires std::is_same_v; // 要求 is_same_v 求值为 true 523 | requires std::same_as; // 要求 same_as 求值为 true 524 | requires requires{ a + a; }; // 要求 requires{ a + a; } 求值为 true 525 | requires sizeof(a) > 4; // 要求 sizeof(a) > 4 求值为 true 526 | }; 527 | std::cout << std::boolalpha << C3 << '\n'; // false 528 | std::cout << std::boolalpha << C3 << '\n'; // true 529 | ``` 530 | 531 | > 嵌套要求的 *约束表达式*,只要能编译期产生 `bool` 值的表达式即可,*概念*(concept)、[类型特征](https://zh.cppreference.com/w/cpp/meta#.E7.B1.BB.E5.9E.8B.E7.89.B9.E5.BE.81)的库、`requires` 表达式,等都一样。 532 | 533 | 这里用 `std::is_same_v` 和 `std::same_as` 其实毫无区别,因为它们都是编译时求值,返回 `bool` 值的表达式。 534 | 535 | 在上面示例中 `requires requires{ a + a; }` 其实是更加麻烦的写法,目的只是为了展示 `requires` 表达式是编译期产生 `bool` 值的表达式,所以有可能会有**两个 `requires`连用的情况**;我们完全可以直接改成 `a + a`,效果完全一样。 536 | 537 | ## 部分(偏)特化中使用*概念* 538 | 539 | 我们在讲 SFINAE 的时候[提到](https://github.com/Mq-b/Modern-Cpp-templates-tutorial/blob/main/md/%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86-%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/10%E4%BA%86%E8%A7%A3%E4%B8%8E%E5%88%A9%E7%94%A8SFINAE.md#%E9%83%A8%E5%88%86%E5%81%8F%E7%89%B9%E5%8C%96%E4%B8%AD%E7%9A%84-sfinae)了,它可以用作模板偏特化,帮助我们选择特化版本;本节的约束与概念当然也可以做到,并且写法**更加简单直观优美**: 540 | 541 | ```cpp 542 | #include 543 | 544 | template 545 | concept have_type = requires{ 546 | typename T::type; 547 | }; 548 | 549 | template 550 | struct X { 551 | static void f() { std::puts("主模板"); } 552 | }; 553 | 554 | template 555 | struct X { 556 | using type = typename T::type; 557 | static void f() { std::puts("偏特化 T::type"); } 558 | }; 559 | 560 | struct Test { using type = int; }; 561 | struct Test2 { }; 562 | 563 | int main() { 564 | X::f(); // 偏特化 T::type 565 | X::f(); // 主模板 566 | } 567 | ``` 568 | 569 | 这个示例完全是从 SFINAE 的写法改进而来,我们不需要再写第二个模板类型参数,我们直接写作 `template` 就完成了,概念约束了模板类型参数 `T`。 570 | 571 | - **只有概念被满足的时候,才会选择到这个偏特化**。 572 | 573 | 一些实际的用途,比如我以前的 [`C++20 STL Cookbook`](https://github.com/Mq-b/Cpp20-STL-Cookbook-src#76%E4%BD%BF%E7%94%A8%E6%A0%BC%E5%BC%8F%E5%BA%93%E6%A0%BC%E5%BC%8F%E5%8C%96%E6%96%87%E6%9C%AC) 中对 [`std::formatter`](https://zh.cppreference.com/w/cpp/utility/format/formatter) 进行偏特化,也是使用的概念,[`std::ranges::range`](https://zh.cppreference.com/w/cpp/ranges/range)。 574 | 575 | ## 总结 576 | 577 | 我们先讲述了 *概念*(concept)的定义和使用,其中使用到了 `requires` 表达式,但是我们留到了后面详细讲述。 578 | 579 | 其实本章内容可以划分为两个部分 580 | 581 | - 约束与概念 582 | 583 | - `requires` 表达式 584 | 585 | 如果你耐心看完,我相信也能意识到它们是互相掺杂,一起使用的。语法上虽然感觉有些多,但是也都很合理,我们只需要 ***带入***,按照基本的常识判断这是不是符合语法,基本上就可以了。 586 | 587 | `requires` 关键字的用法很多,但是划分的话其实就两类 588 | 589 | - `requires` 子句 590 | 591 | - `requires` 表达式 592 | 593 | `requires` 子句和 `requires` 表达式可以连用,组成 `requires requires` 的形式。我们在 [`requires` 子句](#requires-子句)讲过。 594 | 595 | 还有在 `requires` 表达式中的嵌套要求,也会有 `requires requires` 的形式。 596 | 597 | 如果看懂了,这些看似奇怪的 `requires` 关键字复用,其实也都很合理,只需要记住最重要的一句话: 598 | 599 | > 可以连用 `requires requires` 的情况,都是因为第一个 `requires` 期待一个可以编译期产生 `bool` 值的表达式;而 **`requires` 表达式就是产生描述约束的 bool 类型的纯右值表达式**。 600 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-NoDerivatives 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 58 | International Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-NoDerivatives 4.0 International Public 63 | License ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Copyright and Similar Rights means copyright and/or similar rights 84 | closely related to copyright including, without limitation, 85 | performance, broadcast, sound recording, and Sui Generis Database 86 | Rights, without regard to how the rights are labeled or 87 | categorized. For purposes of this Public License, the rights 88 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 89 | Rights. 90 | 91 | c. Effective Technological Measures means those measures that, in the 92 | absence of proper authority, may not be circumvented under laws 93 | fulfilling obligations under Article 11 of the WIPO Copyright 94 | Treaty adopted on December 20, 1996, and/or similar international 95 | agreements. 96 | 97 | d. Exceptions and Limitations means fair use, fair dealing, and/or 98 | any other exception or limitation to Copyright and Similar Rights 99 | that applies to Your use of the Licensed Material. 100 | 101 | e. Licensed Material means the artistic or literary work, database, 102 | or other material to which the Licensor applied this Public 103 | License. 104 | 105 | f. Licensed Rights means the rights granted to You subject to the 106 | terms and conditions of this Public License, which are limited to 107 | all Copyright and Similar Rights that apply to Your use of the 108 | Licensed Material and that the Licensor has authority to license. 109 | 110 | g. Licensor means the individual(s) or entity(ies) granting rights 111 | under this Public License. 112 | 113 | h. NonCommercial means not primarily intended for or directed towards 114 | commercial advantage or monetary compensation. For purposes of 115 | this Public License, the exchange of the Licensed Material for 116 | other material subject to Copyright and Similar Rights by digital 117 | file-sharing or similar means is NonCommercial provided there is 118 | no payment of monetary compensation in connection with the 119 | exchange. 120 | 121 | i. Share means to provide material to the public by any means or 122 | process that requires permission under the Licensed Rights, such 123 | as reproduction, public display, public performance, distribution, 124 | dissemination, communication, or importation, and to make material 125 | available to the public including in ways that members of the 126 | public may access the material from a place and at a time 127 | individually chosen by them. 128 | 129 | j. Sui Generis Database Rights means rights other than copyright 130 | resulting from Directive 96/9/EC of the European Parliament and of 131 | the Council of 11 March 1996 on the legal protection of databases, 132 | as amended and/or succeeded, as well as other essentially 133 | equivalent rights anywhere in the world. 134 | 135 | k. You means the individual or entity exercising the Licensed Rights 136 | under this Public License. Your has a corresponding meaning. 137 | 138 | 139 | Section 2 -- Scope. 140 | 141 | a. License grant. 142 | 143 | 1. Subject to the terms and conditions of this Public License, 144 | the Licensor hereby grants You a worldwide, royalty-free, 145 | non-sublicensable, non-exclusive, irrevocable license to 146 | exercise the Licensed Rights in the Licensed Material to: 147 | 148 | a. reproduce and Share the Licensed Material, in whole or 149 | in part, for NonCommercial purposes only; and 150 | 151 | b. produce and reproduce, but not Share, Adapted Material 152 | for NonCommercial purposes only. 153 | 154 | 2. Exceptions and Limitations. For the avoidance of doubt, where 155 | Exceptions and Limitations apply to Your use, this Public 156 | License does not apply, and You do not need to comply with 157 | its terms and conditions. 158 | 159 | 3. Term. The term of this Public License is specified in Section 160 | 6(a). 161 | 162 | 4. Media and formats; technical modifications allowed. The 163 | Licensor authorizes You to exercise the Licensed Rights in 164 | all media and formats whether now known or hereafter created, 165 | and to make technical modifications necessary to do so. The 166 | Licensor waives and/or agrees not to assert any right or 167 | authority to forbid You from making technical modifications 168 | necessary to exercise the Licensed Rights, including 169 | technical modifications necessary to circumvent Effective 170 | Technological Measures. For purposes of this Public License, 171 | simply making modifications authorized by this Section 2(a) 172 | (4) never produces Adapted Material. 173 | 174 | 5. Downstream recipients. 175 | 176 | a. Offer from the Licensor -- Licensed Material. Every 177 | recipient of the Licensed Material automatically 178 | receives an offer from the Licensor to exercise the 179 | Licensed Rights under the terms and conditions of this 180 | Public License. 181 | 182 | b. No downstream restrictions. You may not offer or impose 183 | any additional or different terms or conditions on, or 184 | apply any Effective Technological Measures to, the 185 | Licensed Material if doing so restricts exercise of the 186 | Licensed Rights by any recipient of the Licensed 187 | Material. 188 | 189 | 6. No endorsement. Nothing in this Public License constitutes or 190 | may be construed as permission to assert or imply that You 191 | are, or that Your use of the Licensed Material is, connected 192 | with, or sponsored, endorsed, or granted official status by, 193 | the Licensor or others designated to receive attribution as 194 | provided in Section 3(a)(1)(A)(i). 195 | 196 | b. Other rights. 197 | 198 | 1. Moral rights, such as the right of integrity, are not 199 | licensed under this Public License, nor are publicity, 200 | privacy, and/or other similar personality rights; however, to 201 | the extent possible, the Licensor waives and/or agrees not to 202 | assert any such rights held by the Licensor to the limited 203 | extent necessary to allow You to exercise the Licensed 204 | Rights, but not otherwise. 205 | 206 | 2. Patent and trademark rights are not licensed under this 207 | Public License. 208 | 209 | 3. To the extent possible, the Licensor waives any right to 210 | collect royalties from You for the exercise of the Licensed 211 | Rights, whether directly or through a collecting society 212 | under any voluntary or waivable statutory or compulsory 213 | licensing scheme. In all other cases the Licensor expressly 214 | reserves any right to collect such royalties, including when 215 | the Licensed Material is used other than for NonCommercial 216 | purposes. 217 | 218 | 219 | Section 3 -- License Conditions. 220 | 221 | Your exercise of the Licensed Rights is expressly made subject to the 222 | following conditions. 223 | 224 | a. Attribution. 225 | 226 | 1. If You Share the Licensed Material, You must: 227 | 228 | a. retain the following if it is supplied by the Licensor 229 | with the Licensed Material: 230 | 231 | i. identification of the creator(s) of the Licensed 232 | Material and any others designated to receive 233 | attribution, in any reasonable manner requested by 234 | the Licensor (including by pseudonym if 235 | designated); 236 | 237 | ii. a copyright notice; 238 | 239 | iii. a notice that refers to this Public License; 240 | 241 | iv. a notice that refers to the disclaimer of 242 | warranties; 243 | 244 | v. a URI or hyperlink to the Licensed Material to the 245 | extent reasonably practicable; 246 | 247 | b. indicate if You modified the Licensed Material and 248 | retain an indication of any previous modifications; and 249 | 250 | c. indicate the Licensed Material is licensed under this 251 | Public License, and include the text of, or the URI or 252 | hyperlink to, this Public License. 253 | 254 | For the avoidance of doubt, You do not have permission under 255 | this Public License to Share Adapted Material. 256 | 257 | 2. You may satisfy the conditions in Section 3(a)(1) in any 258 | reasonable manner based on the medium, means, and context in 259 | which You Share the Licensed Material. For example, it may be 260 | reasonable to satisfy the conditions by providing a URI or 261 | hyperlink to a resource that includes the required 262 | information. 263 | 264 | 3. If requested by the Licensor, You must remove any of the 265 | information required by Section 3(a)(1)(A) to the extent 266 | reasonably practicable. 267 | 268 | 269 | Section 4 -- Sui Generis Database Rights. 270 | 271 | Where the Licensed Rights include Sui Generis Database Rights that 272 | apply to Your use of the Licensed Material: 273 | 274 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 275 | to extract, reuse, reproduce, and Share all or a substantial 276 | portion of the contents of the database for NonCommercial purposes 277 | only and provided You do not Share Adapted Material; 278 | 279 | b. if You include all or a substantial portion of the database 280 | contents in a database in which You have Sui Generis Database 281 | Rights, then the database in which You have Sui Generis Database 282 | Rights (but not its individual contents) is Adapted Material; and 283 | 284 | c. You must comply with the conditions in Section 3(a) if You Share 285 | all or a substantial portion of the contents of the database. 286 | 287 | For the avoidance of doubt, this Section 4 supplements and does not 288 | replace Your obligations under this Public License where the Licensed 289 | Rights include other Copyright and Similar Rights. 290 | 291 | 292 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 293 | 294 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 295 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 296 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 297 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 298 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 299 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 300 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 301 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 302 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 303 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 304 | 305 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 306 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 307 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 308 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 309 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 310 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 311 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 312 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 313 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 314 | 315 | c. The disclaimer of warranties and limitation of liability provided 316 | above shall be interpreted in a manner that, to the extent 317 | possible, most closely approximates an absolute disclaimer and 318 | waiver of all liability. 319 | 320 | 321 | Section 6 -- Term and Termination. 322 | 323 | a. This Public License applies for the term of the Copyright and 324 | Similar Rights licensed here. However, if You fail to comply with 325 | this Public License, then Your rights under this Public License 326 | terminate automatically. 327 | 328 | b. Where Your right to use the Licensed Material has terminated under 329 | Section 6(a), it reinstates: 330 | 331 | 1. automatically as of the date the violation is cured, provided 332 | it is cured within 30 days of Your discovery of the 333 | violation; or 334 | 335 | 2. upon express reinstatement by the Licensor. 336 | 337 | For the avoidance of doubt, this Section 6(b) does not affect any 338 | right the Licensor may have to seek remedies for Your violations 339 | of this Public License. 340 | 341 | c. For the avoidance of doubt, the Licensor may also offer the 342 | Licensed Material under separate terms or conditions or stop 343 | distributing the Licensed Material at any time; however, doing so 344 | will not terminate this Public License. 345 | 346 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 347 | License. 348 | 349 | 350 | Section 7 -- Other Terms and Conditions. 351 | 352 | a. The Licensor shall not be bound by any additional or different 353 | terms or conditions communicated by You unless expressly agreed. 354 | 355 | b. Any arrangements, understandings, or agreements regarding the 356 | Licensed Material not stated herein are separate from and 357 | independent of the terms and conditions of this Public License. 358 | 359 | 360 | Section 8 -- Interpretation. 361 | 362 | a. For the avoidance of doubt, this Public License does not, and 363 | shall not be interpreted to, reduce, limit, restrict, or impose 364 | conditions on any use of the Licensed Material that could lawfully 365 | be made without permission under this Public License. 366 | 367 | b. To the extent possible, if any provision of this Public License is 368 | deemed unenforceable, it shall be automatically reformed to the 369 | minimum extent necessary to make it enforceable. If the provision 370 | cannot be reformed, it shall be severed from this Public License 371 | without affecting the enforceability of the remaining terms and 372 | conditions. 373 | 374 | c. No term or condition of this Public License will be waived and no 375 | failure to comply consented to unless expressly agreed to by the 376 | Licensor. 377 | 378 | d. Nothing in this Public License constitutes or may be interpreted 379 | as a limitation upon, or waiver of, any privileges and immunities 380 | that apply to the Licensor or You, including from the legal 381 | processes of any jurisdiction or authority. 382 | 383 | ======================================================================= 384 | 385 | Creative Commons is not a party to its public 386 | licenses. Notwithstanding, Creative Commons may elect to apply one of 387 | its public licenses to material it publishes and in those instances 388 | will be considered the “Licensor.” The text of the Creative Commons 389 | public licenses is dedicated to the public domain under the CC0 Public 390 | Domain Dedication. Except for the limited purpose of indicating that 391 | material is shared under a Creative Commons public license or as 392 | otherwise permitted by the Creative Commons policies published at 393 | creativecommons.org/policies, Creative Commons does not authorize the 394 | use of the trademark "Creative Commons" or any other trademark or logo 395 | of Creative Commons without its prior written consent including, 396 | without limitation, in connection with any unauthorized modifications 397 | to any of its public licenses or any other arrangements, 398 | understandings, or agreements concerning use of licensed material. For 399 | the avoidance of doubt, this paragraph does not form part of the 400 | public licenses. 401 | 402 | Creative Commons may be contacted at creativecommons.org. 403 | 404 | Modern-Cpp-templates-tutorial © 2023 by mq白 is licensed under CC BY-NC-ND 4.0. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-nd/4.0/ 405 | Modern-Cpp-templates-tutorial © 2023 by mq白 下许可, CC BY-NC-ND 4.0.要查看此许可证的副本,请访问 http://creativecommons.org/licenses/by-nc-nd/4.0/ -------------------------------------------------------------------------------- /md/第一部分-基础知识/01函数模板.md: -------------------------------------------------------------------------------- 1 | # 函数模板 2 | 3 | 本节将介绍函数模板 4 | 5 | ## 初识函数模板 6 | 7 | 函数模板[^1]不是函数,只有实例化[^2]函数模板,编译器才能生成实际的函数定义。不过在很多时候,它看起来就像普通函数一样。 8 | 9 | ### 定义模板 10 | 11 | 下面是一个函数模板,返回两个对象中较大的那个: 12 | 13 | ```cpp 14 | template 15 | T max(T a,T b){ 16 | return a > b ? a : b; 17 | } 18 | ``` 19 | 20 | 这应该很简单,即使我们还没有开始讲述函数模板的语法。 21 | 22 | 如果要声明一个函数模板,我们通常要使用: 23 | 24 | ```cpp 25 | template< 形参列表 > 函数声明 26 | ``` 27 | 28 | 而 `max` 函数模板中的形参列表是 `typename T`,关键字 typename 顾名思义,引入了一个*类型形参*。 29 | 30 | 类型形参名是 `T`,也可以使用其它标识符作为类型形参名(`T`或 `Ty` 等,是约定的惯例),你也可以在需要的时候自定义一些有明确意义的名字。在调用函数模板 `max` 时,根据传入参数,编译器可以推导出类型形参的类型,实例化函数模板。我们需要传入支持函数模板操作的类型,如 int 或 重载了 `>` 运算符的类。注意 `max` 的 `return` 这意味着我们的模板形参 T 还需要是可 *[复制](https://zh.cppreference.com/w/cpp/named_req/CopyConstructible)/[移动](https://zh.cppreference.com/w/cpp/named_req/MoveConstructible)* 的,以便返回。 31 | 32 | > C++17 之前,类型 T 必须是可复制或移动才能传递参数。C++17 以后,即使复制构造函数和移动构造函数都无效,因为 C++17 强制的[复制消除](https://zh.cppreference.com/w/cpp/language/copy_elision),也可以传递临时纯右值。 33 | 34 | 因为一些历史原因,我们也可以使用 **`class`** 关键字来声明模板类型形参。所以先前的模板 `max` 可以**等价于**: 35 | 36 | ```cpp 37 | template 38 | T max(T a,T b){ 39 | return a > b ? a : b; 40 | } 41 | ``` 42 | 43 | 但是与类声明不同,在声明模板类型形参时,不能使用 `struct`。 44 | 45 | [^1]: 注:函数模板自身并不是类型、函数或任何其他实体。不会从只包含模板定义的源文件生成任何代码。模板只有实例化才会有代码出现。 46 | 47 | [^2]: 注:术语“实例化”,指代的是编译器确定各模板实参(可以是根据传入的参数推导,又或者是自己显式指明模板的实参)后从模板生成实际的代码(如从函数模板生成函数,类模板生成类等),这是在编译期就完成的,没有运行时开销。实例化还分为隐式实例化和显式实例化,后面会详细聊。 48 | 49 | ### 使用模板 50 | 51 | 下面展示了如何使用函数模板 `max` : 52 | 53 | ```cpp 54 | #include 55 | 56 | template 57 | T max(T a, T b) { 58 | return a > b ? a : b; 59 | } 60 | 61 | struct Test{ 62 | int v_{}; 63 | Test() = default; 64 | Test(int v) :v_(v) {} 65 | bool operator>(const Test& t) const{ 66 | return this->v_ > t.v_; 67 | } 68 | }; 69 | 70 | int main(){ 71 | int a{ 1 }; 72 | int b{ 2 }; 73 | std::cout << "max(a, b) : " << ::max(a, b) << '\n'; 74 | 75 | Test t1{ 10 }; 76 | Test t2{ 20 }; 77 | std::cout << "max(t1, t2) : " << ::max(t1, t2).v_ << '\n'; 78 | 79 | } 80 | ``` 81 | 82 | > 看起来使用上的确和调用普通函数没区别,那么这样调用函数模板和调用普通函数相比,编译器会做什么呢? 83 | 84 | 编译器会**实例化两个函数**,也就是生成了一个参数为 int 的 max 函数,一个参数为 Test 的函数。 85 | 86 | ```cpp 87 | int max(int a, int b) 88 | { 89 | return a > b ? a : b; 90 | } 91 | 92 | Test max(Test a, Test b) 93 | { 94 | return a > b ? a : b; 95 | } 96 | ``` 97 | 98 | 我们可以使用 [cppinsights](https://cppinsights.io/) 验证我们的想法。 99 | 100 | 用一句非常不严谨的话来说: 101 | 102 | - **模板,只有你“用”了它,才会生成实际的代码**。 103 | 104 | > 这里的“**用**”,其实就是指代会*隐式实例化*,生成代码。 105 | 106 | 并且需要注意,同一个函数模板生成的不同类型的函数,彼此之间没有任何关系。 107 | 108 | --- 109 | 110 | 除了让编译器自己去推导函数模板的形参类型以外,我们还可以自己显式的指明: 111 | 112 | ```cpp 113 | template 114 | T max(T a, T b) { 115 | return a > b ? a : b; 116 | } 117 | 118 | int main(){ 119 | int a{ 1 }; 120 | int b{ 2 }; 121 | max(a, b); // 函数模板 max 被推导为 max 122 | 123 | max(a, b); // 传递模板类型实参,函数模板 max 为 max 124 | } 125 | ``` 126 | 127 | ## 模板参数推导 128 | 129 | 当使用函数模板(如 max())时,模板参数可以由传入的参数推导。如果类型 T 传递两个 int 型参数,那编译器就会认为 T 是 int 型。 130 | 131 | 然而,T 可能只是类型的“一部分”。若声明 max() 使用 `const&` : 132 | 133 | ```cpp 134 | template 135 | T max(const T& a, const T& b) { 136 | return a > b ? a : b; 137 | } 138 | ``` 139 | 140 | 如果我们 `max(1, 2)` 或者说 `max(x,x)`,T 当然会是 int,但是函数形参类型会是 `const int&`。 141 | 142 | 不过我们需要注意,有不少情况是没有办法进行推导的: 143 | 144 | ```cpp 145 | // 省略 max 146 | using namespace std::string_literals; 147 | 148 | int main(){ 149 | max(1, 1.2); // Error 无法确定你的 T 到底是要 int 还是 double 150 | max("luse"s, "乐"); // Error 无法确定你的 T 到底是要 std::string 还是 const char[N] 151 | } 152 | ``` 153 | 154 | 那么我们如何处理这种错误呢?可以使用前面提到的**显式指定函数模板的(T)类型**。 155 | 156 | ```cpp 157 | max(1, 1.2); 158 | max("luse"s, "乐"); 159 | ``` 160 | 161 | 又或者说**显式类型转换**: 162 | 163 | ```cpp 164 | max(static_cast(1), 1.2); 165 | ``` 166 | 167 | 但是 std::string 没有办法如此操作,我们可以显式的构造一个无名临时对象: 168 | 169 | ```cpp 170 | max("luse"s, std::string("乐")); // Error 为什么? 171 | ``` 172 | 173 | 此时就不是我们的 `T` 不明确了,而是函数模板 `max` 不明确,它会和标准库的 `std::max` 产生冲突,虽然我们没有使用 `std::`,但是根据 C++ 的查找规则,(实参依赖查找)[ADL](https://zh.cppreference.com/w/cpp/language/adl),依然可以查找到。 174 | 175 | 那么我们如何解决呢?很简单,进行有限定名字查找,即使用 `::` 或 `std::` 说明,你到底要调用 “全局作用域”的 max,还是 std 命名空间中的 max。 176 | 177 | ```cpp 178 | ::max("luse"s, std::string("乐")); 179 | ``` 180 | 181 | ### 万能引用与引用折叠 182 | 183 | 所谓的万能引用(又称转发引用),即**接受左值表达式那形参类型就推导为左值引用,接受右值表达式,那就推导为右值引用**。 184 | 185 | 比如: 186 | 187 | ```cpp 188 | template 189 | void f(T&&t){} 190 | 191 | int a = 10; 192 | f(a); // a 是左值表达式,f 是 f 但是它的形参类型是 int& 193 | f(10); // 10 是右值表达式,f 是 f 但它的形参类型是 int&& 194 | ``` 195 | 196 | > 被推导为 `f` 涉及到了特殊的[推导规则](https://zh.cppreference.com/w/cpp/language/template_argument_deduction#:~:text=%E5%A6%82%E6%9E%9C%20P%20%E6%98%AF%E5%88%B0%E6%97%A0%20cv%20%E9%99%90%E5%AE%9A%E6%A8%A1%E6%9D%BF%E5%BD%A2%E5%8F%82%E7%9A%84%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8%EF%BC%88%E4%B9%9F%E5%B0%B1%E6%98%AF%E8%BD%AC%E5%8F%91%E5%BC%95%E7%94%A8%EF%BC%89%E4%B8%94%E5%AF%B9%E5%BA%94%E5%87%BD%E6%95%B0%E7%9A%84%E8%B0%83%E7%94%A8%E5%AE%9E%E5%8F%82%E6%98%AF%E5%B7%A6%E5%80%BC%EF%BC%8C%E9%82%A3%E4%B9%88%E5%B0%86%E5%88%B0%20A%20%E7%9A%84%E5%B7%A6%E5%80%BC%E5%BC%95%E7%94%A8%E7%B1%BB%E5%9E%8B%E7%94%A8%E4%BA%8E%20A%20%E7%9A%84%E4%BD%8D%E7%BD%AE%E8%BF%9B%E8%A1%8C%E6%8E%A8%E5%AF%BC):如果 P 是到无 cv 限定模板形参的右值引用(也就是转发引用)且对应函数的调用实参是左值,那么将到 A 的左值引用类型用于 A 的位置进行推导。 197 | --- 198 | 199 | 通过模板或 typedef 中的类型操作可以构成引用的引用,此时适用引用折叠(reference collapsing)规则: 200 | 201 | - **右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用**。 202 | 203 | ```cpp 204 | typedef int& lref; 205 | typedef int&& rref; 206 | int n; 207 | 208 | lref& r1 = n; // r1 的类型是 int& 209 | lref&& r2 = n; // r2 的类型是 int& 210 | rref& r3 = n; // r3 的类型是 int& 211 | rref&& r4 = 1; // r4 的类型是 int&& 212 | ``` 213 | 214 | ```cpp 215 | template 216 | constexpr Ty&& forward(Ty& Arg) noexcept { 217 | return static_cast(Arg); 218 | } 219 | 220 | int a = 10; // 不重要 221 | ::forward(a); // 返回 int&& 因为 Ty 是 int,Ty&& 就是 int&& 222 | ::forward(a); // 返回 int& 因为 Ty 是 int&,Ty&& 就是 int& 223 | ::forward(a); // 返回 int&& 因为 Ty 是 int&&,Ty&& 就是 int&& 224 | ``` 225 | 226 | ## 有默认实参的模板类型形参 227 | 228 | 就如同函数形参可以有默认值一样,模板形参也可以有默认值。当然了,这里是“类型形参”(后面会讲非类型/常量的)。 229 | 230 | ```cpp 231 | template 232 | void f(); 233 | 234 | f(); // 默认为 f 235 | f(); // 显式指明为 f 236 | ``` 237 | 238 | ```cpp 239 | using namespace std::string_literals; 240 | 241 | template 243 | 244 | RT max(const T1& a, const T2& b) { // RT 是 std::string 245 | return a > b ? a : b; 246 | } 247 | 248 | int main(){ 249 | auto ret = ::max("1", "2"s); 250 | std::cout << ret << '\n'; 251 | } 252 | ``` 253 | 254 | > [#25](https://github.com/Mq-b/Modern-Cpp-templates-tutorial/issues/25) GCC 编译器有 **BUG**,自行注意。 255 | 256 | 以上这个示例你可能有很多疑问,我们第一次使用了多个模板类型形参,并且第三个模板类型形参给了默认值,但是这个值似乎有点难以理解,我们后面慢慢讲解。 257 | 258 | `max(const T1& a, const T2& b)` 259 | 260 | 让 max 函数模板接受两个参数的时候不需要再是相同类型,那么这自然而然就会引入另一个问题了,如何确定返回类型? 261 | 262 | ```cpp 263 | typename RT = decltype(true ? T1{} : T2{}) 264 | ``` 265 | 266 | 我们从最里面开始看: 267 | 268 | ```cpp 269 | decltype(true ? T1{} : T2{}) 270 | ``` 271 | 272 | 这是一个三目运算符表达式。然后外面使用了 decltype 获取这个表达式的类型,那么问题是,为什么是 true 呢?以及为什么需要 T1{},T2{} 这种形式? 273 | 274 | 1. 我们为什么要设置为 **true**? 275 | 276 | 其实无所谓,设置 false 也行,true 还是 false 不会影响三目表达式的类型。这涉及到了一些复杂的规则,简单的说就是三目表达式要求第二项和第三项之间能够隐式转换,然后整个表达式的类型会是 **“公共”类型**。 277 | 278 | 比如第二项是 int 第三项是 double,三目表达式当然会是 double。 279 | 280 | ```cpp 281 | using T = decltype(true ? 1 : 1.2); 282 | using T2 = decltype(false ? 1 : 1.2); 283 | ``` 284 | 285 | **T 和 T2 都是 double 类型**。 286 | 287 | 2. 为什么需要 `T1{}`,`T2{}` 这种形式? 288 | 289 | 没有办法,必须构造临时对象来写成这种形式,这里其实是[不求值语境](https://zh.cppreference.com/w/cpp/language/expressions#.E6.BD.9C.E5.9C.A8.E6.B1.82.E5.80.BC.E8.A1.A8.E8.BE.BE.E5.BC.8F),我们只是为了写出这样一种形式,让 decltype 获取表达式的类型罢了。 290 | 291 | 模板的默认实参的和函数的默认实参大部分规则相同。 292 | 293 | `decltype(true ? T1{} : T2{})` 解决了。 294 | 295 | 事实上上面的写法都十分的丑陋与麻烦,我们可以使用 auto 简化这一切。 296 | 297 | --- 298 | 299 | ```cpp 300 | template 301 | auto max(const T& a, const T2& b) -> decltype(true ? a : b){ 302 | return a > b ? a : b; 303 | } 304 | ``` 305 | 306 | 这是 C++11 [后置返回类型](https://zh.cppreference.com/w/cpp/language/auto#.E5.87.BD.E6.95.B0.E5.A3.B0.E6.98.8E),它和我们之前用默认模板实参 `RT` 的区别只是稍微好看了一点吗? 307 | 308 | 不,**它们的返回类型是不一样的**,如果函数模板的形参是**类型相同** `true ? a : b` 表达式的类型是 **`const T&`**;如果是 `max(1, 2)` 调用,那么也就是 `const int&`;而前面的例子只是 `T` 即 `int`(前面都是用模板类型参数直接构造临时对象,而不是有实际对象,自然如此,比如 `T{}`)。 309 | 310 | > 假设以 `max(1,1.0)` 调用,那么自然返回类型不是 `const T&` 311 | > 312 | > ```cpp 313 | > max(1, 2.0); // 返回类型为 double 314 | > ``` 315 | > 316 | > 下面的 `max` 定义也类似,涉及的规则不再强调。 317 | 318 | 使用 C++20 简写函数模板,我们可以直接再简化为: 319 | 320 | ```cpp 321 | decltype(auto) max(const auto& a, const auto& b) { 322 | return a > b ? a : b; 323 | } 324 | ``` 325 | 326 | 效果和上面使用后置返回类型的写法完全一样;C++14 引入了两个特性: 327 | 328 | 1. [返回类型推导](https://zh.cppreference.com/w/cpp/language/function#.E8.BF.94.E5.9B.9E.E7.B1.BB.E5.9E.8B.E6.8E.A8.E5.AF.BC)(也就是函数可以直接写 auto 或 decltype(auto) 做返回类型,而不是像 C++11 那样,只是后置返回类型。 329 | 330 | 2. [`decltype(auto)`](https://zh.cppreference.com/w/cpp/language/auto) “*如果返回类型没有使用 decltype(auto),那么推导遵循[模板实参推导](https://zh.cppreference.com/w/cpp/language/template_argument_deduction#.E5.85.B6.E4.BB.96.E8.AF.AD.E5.A2.83)的规则进行*”。我们上面的 `max` 示例如果不使用 decltype(auto),按照模板实参的推导规则,是不会有引用和 cv 限定的,就只能推导出返回 `T` 类型。 331 | 332 | > 大家需要注意后置返回类型和返回类型推导的区别,它们不是一种东西,后置返回类型虽然也是写的 `auto` ,但是它根本没推导,只是占位。 333 | 334 | 模板的默认实参无处不在,比如标准库的 [std::vector](https://zh.cppreference.com/w/cpp/container/vector),[std::string](https://zh.cppreference.com/w/cpp/string/basic_string),当然了,这些都是类模板,我们以后会讲到。 335 | 336 | ## 常量模板形参 337 | 338 | 既然有“*类型模板形参*”,自然有**非类型**的,顾名思义,也就是模板不接受类型,而是接受**常量**,包括指代对象的左值常量。过去的标准用词称这类模板形参为“非类型模板形参”, C++26 周期的[编辑改动](https://wg21.link/EDIT7587)将它们更精准地称为“常量模板形参”。 339 | 340 | ```cpp 341 | template 342 | void f() { std::cout << N << '\n'; } 343 | 344 | f<100>(); 345 | ``` 346 | 347 | 常量模板形参有众多的规则和要求,目前,你简单认为需要参数是“常量”即可。 348 | 349 | 常量模板形参当然也可以有默认值: 350 | 351 | ```cpp 352 | template 353 | void f() { std::cout << N << '\n'; } 354 | 355 | f(); // 默认 f<100> 356 | f<66>(); // 显式指明 f<66> 357 | ``` 358 | 359 | 后续我们会有更多详细讲解和应用。 360 | 361 | ## 重载函数模板 362 | 363 | 函数模板与非模板函数可以重载。 364 | 365 | 这里会涉及到非常复杂的函数重载决议[^3],即选择到底调用哪个函数。 366 | 367 | 我们用一个简单的示例展示一部分即可: 368 | 369 | ```cpp 370 | template 371 | void test(T) { std::puts("template"); } 372 | 373 | void test(int) { std::puts("int"); } 374 | 375 | test(1); // 匹配到test(int) 376 | test(1.2); // 匹配到模板 377 | test("1"); // 匹配到模板 378 | ``` 379 | 380 | - 通常优先选择非模板的函数。 381 | 382 | ## 可变参数模板 383 | 384 | 和其他语言一样,C++ 也是支持可变参数的,我们必须使用模板才能做到。 385 | 386 | 老式 C 语言的变长实参有众多弊端,[参见](https://github.com/Mq-b/Loser-HomeWork/blob/main/src/C++CoreGuidelines/第4章-函数.md#f55-不要使用-va_arg-参数)。 387 | 388 | 同样的,它的规则同样众多繁琐,我们不会说太多,以后会用到的,我们当前还是在入门阶段。 389 | 390 | 我们提一个简单的需求: 391 | 392 | > 我需要一个函数 sum,支持 sum(1,2,3.5,x,n...) 即函数 sum 支持任意类型,任意个数的参数进行调用,你应该如何实现? 393 | 394 | 首先就要引入一个东西:[**形参包**](https://zh.cppreference.com/w/cpp/language/parameter_pack) 395 | 396 | > 本节以 **C++14** 标准进行讲述。 397 | 398 | 模板形参包是接受零个或更多个模板实参(常量、类型或模板)的模板形参。函数形参包是接受零个或更多个函数实参的函数形参。 399 | 400 | ```cpp 401 | template 402 | void sum(Args...args){} 403 | ``` 404 | 405 | 这样一个函数,就可以接受任意类型的任意个数的参数调用,我们先观察一下它的语法和普通函数有什么不同。 406 | 407 | 模板中需要 typename 后跟三个点 Args,函数形参中需要用模板类型形参包后跟着三个点 再 args。 408 | 409 | **args 是函数形参包,Args 是类型形参包,它们的名字我们可以自定义。** 410 | 411 | **args 里,就存储了我们传入的全部的参数,Args 中存储了我们传入的全部参数的类型。** 412 | 413 | 那么问题来了,存储很简单,我们要如何把这些东西取出来使用呢?这就涉及到另一个知识:[**形参包展开**](https://zh.cppreference.com/w/cpp/language/parameter_pack#.E5.8C.85.E5.B1.95.E5.BC.80)。 414 | 415 | ```cpp 416 | void f(const char*, int, double) { puts("值"); } 417 | void f(const char**, int*, double*) { puts("&"); } 418 | 419 | template 420 | void sum(Args...args){ // const char * args0, int args1, double args2 421 | f(args...); // 相当于 f(args0, args1, args2) 422 | f(&args...); // 相当于 f(&args0, &args1, &args2) 423 | } 424 | 425 | int main() { 426 | sum("luse", 1, 1.2); 427 | } 428 | ``` 429 | 430 | sum 的 `Args...args` 被展开为 `const char * args0, int args1, double args2`。 431 | 432 | 这里我们需要定义一个术语:**模式**。 433 | 434 | 后随省略号且其中至少有一个形参包的名字的**模式**会被展开 成零个或更多个**逗号分隔**的模式实例。 435 | 436 | `&args...` 中 `&args` 就是模式,在展开的时候,模式,也就是省略号前面的一整个表达式,会被不停的填入对象并添加 `&`,然后逗号分隔。直至形参包的元素被消耗完。 437 | 438 | 那么根据这个,我们就能写出一些有意思的东西,比如一次性把它们打印出来: 439 | 440 | ```cpp 441 | template 442 | void print(const Args&...args){ // const char (&args0)[5], const int & args1, const double & args2 443 | int _[]{ (std::cout << args << ' ' ,0)... }; 444 | } 445 | 446 | int main() { 447 | print("luse", 1, 1.2); 448 | } 449 | ``` 450 | 451 | 一步一步看:`(std::cout << args << ' ' ,0)...` 是一个包展开,那么它的模式是:`(std::cout << args << ' ' ,0)`,实际展开的时候是: 452 | 453 | ```cpp 454 | (std::cout << arg0 << ' ' ,0), (std::cout << arg1 << ' ' ,0),(std::cout << arg2 << ' ' ,0) 455 | ``` 456 | 457 | 很明显是为了打印,对,但是为啥要括号里加个逗号零呢?这是因为逗号表达式是从左往右执行的,返回最右边的值作为整个逗号表达式的值,也就是说:每一个 `(std::cout << arg0 << ' ' ,0)` 都会返回 0,这主要是为了符合语法,用来初始化数组。我们创建了一个数组 `int _[]` ,最终这些 0 会用来初始化这个数组,当然,这个数组本身没有用,**只是为了创造合适的[包展开场所](https://zh.cppreference.com/w/cpp/language/parameter_pack#.E5.B1.95.E5.BC.80.E5.9C.BA.E6.89.80)**。 458 | 459 | > 为了简略,我们不详细说明有哪些展开场所,不过我们上面使用到的是在[*花括号包围的初始化器*](https://zh.cppreference.com/w/cpp/language/parameter_pack#.E8.8A.B1.E6.8B.AC.E5.8F.B7.E5.8C.85.E5.9B.B4.E7.9A.84.E5.88.9D.E5.A7.8B.E5.8C.96.E5.99.A8)中展开。 460 | 461 | - ***只有在合适的形参包展开场所才能进行形参包展开***。 462 | 463 | >```cpp 464 | >template 465 | >void print(const Args &...args) { 466 | > (std::cout << args << " ")...; // 不是合适的形参包展开场所 Error! 467 | >} 468 | >``` 469 | 470 |
471 | 细节 472 | 473 | ```cpp 474 | template 475 | void print(const Args&...args){ 476 | int _[]{ (std::cout << args << ' ' ,0)... }; 477 | } 478 | ``` 479 | 480 | 我们先前的函数模板 `print` 的实现还存在一些问题,如果形参包为空呢? 481 | 482 | 也就是说,假设我如此调用: 483 | 484 | ```cpp 485 | print(); // Error! 486 | ``` 487 | 488 | 目前这个写法在参数列表为空时会导致 `_` 为[*非良构*](https://zh.cppreference.com/w/cpp/language/ub#:~:text=%E8%A7%82%E5%AF%9F%E8%A1%8C%E4%B8%BA%EF%BC%9A-,%E9%9D%9E%E8%89%AF%E6%9E%84,-%EF%BC%88ill%2Dformed%EF%BC%89)的 **0 长度数组**,在严格的模式下造成*编译错误*。 489 | 490 | 解决方案是在数组中添加一个元素使其长度始终为正。 491 | 492 | ```cpp 493 | template 494 | void print(const Args&...args){ 495 | int _[]{ 0, (std::cout << args << ' ' ,0)... }; 496 | } 497 | ``` 498 | 499 | 大家不用感到奇怪,当形参包为空的时候,也就基本相当于 500 | 501 | ```cpp 502 | int _[]{ 0, }; 503 | ``` 504 | 505 | 也就是当做直接去掉 `(std::cout << args << ' ' ,0)...` 即可。 506 | 507 | 不用感到奇怪,花括号初始化器列表一直都允许有一个尾随的逗号。从古代 C++ 起 `int a[] = {0, };` 就是合法的定义 508 | 509 | > ***可是我为什么要允许空参(print())调用呢?*** 510 | 511 | 这里的确完全没必要,不过我们出于教学目的,可以提及一下。 512 | 513 | --- 514 | 515 | 这个示例其实还有修改的余地:**我们的本意并非是创造一个局部的数组,我们只是想执行其中的副作用(打印)。** 516 | 517 | 让这个数组对象直到函数结束生存期才结束,实在是太晚了,我们可以创造一个临时的数组对象,这样它的生存期也就是那一行罢了: 518 | 519 | ```cpp 520 | template 521 | void print(const Args&...args) { 522 | using Arr = int[]; // 创建临时数组,需要使用别名 523 | Arr{ 0, (std::cout << args << ' ' ,0)... }; 524 | } 525 | ``` 526 | 527 | 这没问题,但是还不够,在某些编译器(clang)这会造成编译器的[警告](https://godbolt.org/z/E17eYWhY6),想要解决也很简单,将它变为一个[*弃值表达式*](https://zh.cppreference.com/w/cpp/language/expressions#.E5.BC.83.E5.80.BC.E8.A1.A8.E8.BE.BE.E5.BC.8F),也就是转换到 `void`。 528 | 529 | > *弃值表达式* 是只用来**实施它的副作用的表达式**。从这种表达式计算的值会被舍弃。 530 | 531 | ```cpp 532 | template 533 | void print(const Args&...args){ 534 | using Arr = int[]; 535 | (void)Arr{ 0, (std::cout << args << ' ' ,0)... }; 536 | } 537 | ``` 538 | 539 | 此时编译器就开心了,不再有警告。并且也很合理,我们的确只是需要实施副作用而不需要“值”。 540 | 541 |
542 | 543 | --- 544 | 545 | 我们再给出一个数组的示例: 546 | 547 | ```cpp 548 | template 549 | void print(const Args&...args) { 550 | int _[]{ (std::cout << args << ' ' ,0)... }; 551 | } 552 | 553 | template 554 | void f(const T(&array)[N], Args...index) { 555 | print(array[index]...); 556 | } 557 | 558 | int main() { 559 | int array[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 560 | f(array, 1, 3, 5); 561 | } 562 | ``` 563 | 564 | > 在[*函数形参列表*](https://zh.cppreference.com/w/cpp/language/parameter_pack#.E5.87.BD.E6.95.B0.E5.BD.A2.E5.8F.82.E5.88.97.E8.A1.A8)中展开。 565 | 566 | 我们复用了之前写的 print 函数,我们看新的 **f** 函数即可。 567 | 568 | `const T(&array)[N]` 注意,这是一个数组引用,我们也使用到了常量模板形参 `N`;加括号,`(&array)` 只是为了区分优先级。那么这里的 `T` 是 int,`N` 是 10,组成了一个数组类型。 569 | 570 | 不必感到奇怪,内建的数组类型,其 size 也是类型的一部分,这就如同 `int[1]` 和 `int[2]` 不是一个类型一样,很正常。 571 | 572 | `print(array[index]...);` 其中 `array[index]...` 是包展开,`array[index]` 是模式,实际展开的时候就是: 573 | 574 | `array[arg0], array[arg1], array[arg2]` 575 | 576 | 到此,如果你自己写了,理解了这两个示例,那么你应该就能正确的使用形参包展开,那就可以正确的使用基础的可变参数函数。 577 | 578 | --- 579 | 580 | 那么回到最初的需求,实现一个 `sum`: 581 | 582 | ```cpp 583 | #include 584 | #include 585 | 586 | template> 587 | RT sum(const Args&...args) { 588 | RT _[]{ static_cast(args)... }; 589 | RT n{}; 590 | for (int i = 0; i < sizeof...(args); ++i) { 591 | n += _[i]; 592 | } 593 | return n; 594 | } 595 | 596 | int main() { 597 | double ret = sum(1, 2, 3, 4, 5, 6.7); 598 | std::cout << ret << '\n'; // 21.7 599 | } 600 | ``` 601 | 602 | [`std::common_type_t`](https://zh.cppreference.com/w/cpp/types/common_type) 的作用很简单,就是确定我们传入的共用类型,说白了就是这些东西都能隐式转换到哪个,那就会返回那个类型。 603 | 604 | `RT _[]{ static_cast(args)... };` 创建一个数组,形参包在它的初始化器中展开,初始化这个数组,数组存储了我们传入的全部的参数。 605 | 606 | > 因为[窄化转换](https://zh.cppreference.com/w/cpp/language/list_initialization#.E7.AA.84.E5.8C.96.E8.BD.AC.E6.8D.A2)禁止了列表初始化中 int 到 double 的隐式转换,所以我们需要显式的转换为“公共类型” `RT`。 607 | 608 | 至于 `sizeof...` 很简单,单纯的获取形参包的元素个数。 609 | 610 | 其实也可以不写这么复杂,我们不用手动写循环,直接调用标准库的求和函数。 611 | 612 | 我们简化一下: 613 | 614 | ```cpp 615 | template> 616 | RT sum(const Args&...args) { 617 | RT _[]{ args... }; 618 | return std::accumulate(std::begin(_), std::end(_), RT{}); 619 | } 620 | ``` 621 | 622 | `RT{}` 构造一个临时无名对象,表示初始值,std::begin 和 std::end 可以获取数组的首尾地址。 623 | 624 | --- 625 | 626 | 当然了,常量模板形参也可以使用形参包,我们举个例子: 627 | 628 | ```cpp 629 | template 630 | void f(){ 631 | std::size_t _[]{ N... }; // 展开相当于 1UL, 2UL, 3UL, 4UL, 5UL 632 | std::for_each(std::begin(_), std::end(_), 633 | [](std::size_t n){ 634 | std::cout << n << ' '; 635 | } 636 | ); 637 | } 638 | f<1, 2, 3, 4, 5>(); 639 | ``` 640 | 641 | 这很合理,无非是让模板形参存储的不再是类型形参包,而是参数形参包罢了。 642 | 643 | --- 644 | 645 | *在后面的内容,我们还会向你展示新的形参包展开方式:[C++17 折叠表达式](08折叠表达式.md)*。不用着急。 646 | 647 | ## 模板分文件 648 | 649 | 新手经常会有一个想法就是,对模板进行分文件,写成 `.h` `.cpp` 这种形式。 650 | 651 | **这显然是不可以的**,我们给出了一个项目[示例](/code/01模板分文件/main.cpp)。 652 | 653 | > 后续会讲如何处理 654 | 655 | 在聊为什么不可以之前,我们必须先从头讲解编译链接,以及 `#include` 的知识,不然你将无法理解。 656 | 657 | ### include 指令 658 | 659 | 先从预处理指令 `#include` 开始,你知道它会做什么吗? 660 | 661 | 很多人会告诉你,**它就是简单的替换**,的确,没有问题,但是我觉得不够明确,我给你几个示例: 662 | 663 | [`array.txt`](/code/02include指令/array.txt) 664 | 665 | ```txt 666 | 1,2,3,4,5 667 | ``` 668 | 669 | [`main.cpp`](/code/02include指令/main.cpp) 670 | 671 | ```cpp 672 | #include 673 | 674 | int main(){ 675 | int arr[] = { 676 | #include"array.txt" 677 | }; 678 | for(int i = 0; i < sizeof(arr)/sizeof(int); ++i) 679 | std::cout<< arr[i] <<' '; 680 | std::cout<<'\n'; 681 | } 682 | ``` 683 | 684 | >`g++ main.cpp -o main` 685 | > 686 | >`./main` 687 | 688 | 直接编译运行,会打印出 `1 2 3 4 5`。 689 | 690 | `#include"array.txt"` 直接被替换为了 `1,2,3,4,5`,所以 arr 是: 691 | 692 | ```cpp 693 | int arr[] = {1,2,3,4,5}; 694 | ``` 695 | 696 | 或者我们可以使用 gcc 的 `-E` 选项来查看预处理之后的文件内容: 697 | 698 | [`main2.cpp`](/code/02include指令/main2.cpp) 699 | 700 | ```cpp 701 | int main(){ 702 | int arr[] = { 703 | #include"array.txt" 704 | }; 705 | } 706 | ``` 707 | 708 | 去除头文件打印之类的是因为,iostream 的内容非常庞大,不利于我们关注数组 arr。 709 | 710 | >`g++ -E main2.cpp` 711 | 712 | ```cpp 713 | # 0 "main2.cpp" 714 | # 0 "" 715 | # 0 "" 716 | # 1 "main2.cpp" 717 | int main(){ 718 | int arr[] = { 719 | # 1 "array.txt" 1 720 | 1,2,3,4,5 721 | # 4 "main2.cpp" 2 722 | }; 723 | } 724 | ``` 725 | 726 | `# 0` `# 1` 这些是 gcc 的行号更改指令。不用过多关注,不是当前的重点,明白 #include 会进行替换即可。 727 | 728 | ### 分文件的原理是什么? 729 | 730 | 我们通常将函数声明放在 `.h` 文件中,将函数定义放在 `.cpp` 文件中,我们只需要在需要使用的文件中 `include` 一个 `.h` 文件;我们前面也说了,`include` 就是复制,事实上是把函数声明复制到了我们当前的文件中。 731 | 732 | ```cpp 733 | //main.cpp 734 | #include "test.h" 735 | 736 | int main(){ 737 | f(); // 非模板,OK 738 | } 739 | ``` 740 | 741 | [`test.h`](/code/01模板分文件/test.h) 只是存放了函数声明,函数定义在 [`test.cpp`](/code/01模板分文件/test.cpp) 中,我们编译的时候是选择编译了 `main.cpp` 与 `test.cpp` 这两个文件,那么为什么程序可以成功编译运行呢? 742 | 743 | 是怎么找到函数定义的呢?明明我们的 main.cpp 其实预处理过后只有函数声明而没有函数定义。 744 | 745 | 这就是链接器做的事情,如果编译器在编译一个翻译单元(如 main.cpp)的时候,如果发现找不到函数的定义,那么就会空着一个符号地址,将它编译为目标文件。期待链接器在链接的时候去其他的翻译单元找到定义来填充符号。 746 | 747 | 我们的 `test.cpp` 里面存放了 `f` 的函数定义,并且具备外部链接,在编译成目标文件之后之后,和 `main.cpp` 编译的目标文件进行链接,链接器能找到函数 `f` 的符号。 748 | 749 | **不单单是函数,全局变量等都是这样,这是编译链接的基本原理和步骤**。 750 | 751 | > 类会有所不同,总而言之后续视频会单独讲解的。 752 | 753 | --- 754 | 755 | 那么,模板不能分文件[^4]的原因就显而易见了,我们在讲[使用模板](#使用模板)的时候就说了: 756 | 757 | - **模板,只有你“用”了它,才会生成实际的代码**。 758 | 759 | 你单纯的放在一个 `.cpp` 文件中,它不会生成任何实际的代码,自然也没有函数定义,也谈不上链接器找符号了。 760 | 761 | >所以模板通常是直接放在 `.h` 文件中,而不会分文件。或者说用 `.hpp` 这种后缀,这种约定俗成的,代表这个文件里放的是模板。 762 | 763 | ## 总结 764 | 765 | 事实上函数模板的各种知识远不止如此,但也足够各位目前的学习与使用了。 766 | 767 | 不用着急,后面会有更多的技术和函数模板一起结合使用的,本节所有的代码示例请务必全部理解和自己亲手写一遍,通过编译,有任何不懂一定要问,提出来。 768 | 769 | [^3]: 注:“[重载决议](https://zh.cppreference.com/w/cpp/language/overload_resolution)”,简单来说,一个函数被重载,编译器必须决定要调用哪个重载,我们决定调用的是各形参与各实参之间的匹配最紧密的重载。 770 | 771 | [^4]: 注:这个问题可以通过显式实例化解决,在[后面](06模板显式实例化解决模板分文件问题.md)会讲。 772 | --------------------------------------------------------------------------------