# 纲要 > 主干纲要、Hint/线索/路标 - 模版声明 - 模版实例化 - 模特特例化 - 模版参数 - 类型模版参数(type template parameter) - 非类型模版参数(non-type template parameter) - 模版模版参数 %% # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% <br> # 泛型编程 泛型编程是一种**编程范式**,即 "**独立于任何特定类型**" 来编写代码。 C++中的泛型编程主要通过 "**模版**" (templates)实现,支持**编写类型无关**的代码,使用**相同的代码逻辑**来处理**不同的数据类型**,而具体的数据类型**在编译时确定**。 模版代码不针对特定类型,但**开发者在编写模版代码**时通常需要**对所使用的类型有一些假设与预期**。 > [!NOTE] > > - "函数" 的封装为 **"同类型" 的不同调用参数**提供了代码复用。 > > > - "模版" 的实现为 **"不同类型"** 或是提供了代码复用 (或是**同类型的不同"编译时参数"**) > <br><br> # 模版 模版可看作是 "**编译器为==生成类或函数==而编写的一份说明**",编译器根据模版而生成具体代码。 模版定义以 `template` 关键字开始,后跟一个 "**模版参数列表**"。 C++中的可定义以下类型的模版: - **==类模板==**(class templates) - **==函数模版==**(function templates) - **成员模版**(member templates) - **==别名模版==**(alias template) - 变量模版(variable template) since C++14 - 概念(constraints and concepts) since C++20 ![[02-开发笔记/01-cpp/预处理相关/cpp-头文件说明#^cpj1sg]] <br><br> # 模版声明 **模版声明**(即"**前置声明**")用以**告诉编译器某个模版的存在**以及基本结构, 模版声明必须包括**模版参数列表**,但**不包括模版的完整定义**。 > [!NOTE] 模版声明在文件中的位置 > 一个特定文件所需要的**所有模版的声明**通常一起**放置在文件开始位置**,出现于任何使用这些模版的代码之前。 > <br> ### 区别"模版声明"与"模版实例声明" 注意区别 "**==模版声明==**" 、"**==模版实例声明==**(**显式实例化**或带有 `extern` **显式实例化声明**)"、 - **==模版==声明**:声明的是一个"模版",必须带有 "**显式模版参数列表**"; - **==模版实例==声明**:声明的是一个 "**模版实例**",一个具体的模版类或模版函数 - "**模版==显式实例化==**":以指示编译器**在当前翻译单元中**唯一地生成一个**模版实例**。 - "**模版==显式实例化声明==**":带有 `extern` 关键字,用以告诉编译器已在其他文件中进行了该模版实例的显式实例化。 > [!NOTE] **"模版显式实例化" 语句** 和 **带有 `extern` 的"模版显式实例化声明语句"** 都可以用作为一个翻译单元中的 "**==模版实例的前置声明==**" (放在需要使用模版实例的代码之前) 说明示例: ```cpp title:template_declaration.cpp // 模版"声明" // 类模版声明 template <typename T, typename U> class MyClass; // 模版参数名"T"可省略, 但要在参数列表中保留`typename`关键字 template <typename, typename> class MyClass2; // 函数模版声明 template <typename T> void MyFunc(const T&); // `T`不能省略, 因为形参列表里有用到. // 模版"定义" template <typename T, typename U> // 类模版定义 class MyClass {}; template <typename T, typename U> class MyClass2 {}; template <typename T> void MyFunc(const T&) {} // 函数模版定义 // 模版实例声明, 也即显式实例化语句 template class MyClass<int, char>; // 模版类声明语句, 也即显式实例化语句 template class MyClass2<double, int>; template void MyFunc<string>(const string&); // 模版函数声明语句, 也即显式实例化语句 // 在其他文件中再次引用这些模版实例时, 需要使用带有`extern`的"显式实例化声明"语句. // "显式实例化声明"语句示例如下: // extern template class MyClass<int, char>; // extern template class MyClass2<double, int>; // extern template void MyFunc<string>(const string&); int main() { MyClass<int, char> obj; MyClass2<double, char> obj2; } ``` <br><br> # 模版实例化 Instantiation 模版实例化:**编译器根据==模版定义==以及==模版实参==生成具体代码(模版实例——模版类或模版函数)的过程**。 > [!important] **"模版定义" 与 "模版实例化语句"(无论是隐式还是显式)必须位于==同一翻译单元上下文==中**。 > > 模版实例化**在编译时进行**,而**编译器必须要能够看到==模版的完整定义==才能生成模版的实例代码**。 > [!NOTE] 模版只有在 "被使用" 时,才会被实例化。 > > - **函数模版只有在被使用时才会被实例化**; > - **类模版只有在定义 "==该类的实例变量==" 时才会实例化**,而**类模版中的成员函数只有在 "==该函数被使用时==" 才被实例化**; > <br> ### 模版实例化的类型 模版实例化可以分为两种情况: - "**隐式实例化**":不进行显式实例化,而直接使用模版,**编译器在模版首次被使用时为其生成模版实例**。 - "**显式实例化**":**明确指示编译器"==在当前翻译单元=="根据==提供的模版实参==为==特定类型==生成模版实例**。 > [!summary] 模版实例的生成数量 > > - **隐式实例化**中,**对每个唯一的模版参数组合,编译器会在其==首次使用时==生成一个模版实例**。当具有相同模版参数的模版被多次使用时,编译器通常**复用已有的模版实例**,而不会再新生成。 > > > - **显式实例化**中,**对每个==被显式指定生成==的模版实例**,其在 **==整个程序中只会被生成一次==**,无论相同的显式实例化语句在代码中出现多少次,又或者存在多少次对相同实例的使用。 > > > - **特化(全特化和偏特化)** 会为每个特化的模版参数组合生成一个实例。 <br><br> ## 隐式实例化 隐式实例化发生在 **==模板被使用==** 时,编译器根据**函数调用**或**类对象创建时提供的模板参数**自动生成模板实例。 - 对于**函数模版**,可以不提供模版实参,编译器可根据**调用时的参数类型**自动推导其模版参数; - 对于**类模版**,**==必须==提供模版实参**。编译器不能为**类模版**推断模版参数类型。 - 如果类模版的所有模版实参都具有默认实参,且想要使用全部的默认实参,也必须在模版名之后**提供==空的模版实参列表==** `<>` 来进行**模版实例化**。 > [!info] 在没有显式实例化时,**编译器只在处理到 =="使用模版" 的代码语句==时才会进行实例化**,这即是隐式实例化。 ```cpp template <typename T> void print(T value) { std::cout << value << std::endl; } int main() { // 使用不同的模版参数, 实例化得到不同的"模版函数" // 下列对模版的使用都属于"隐式实例化" print(1); // 隐式实例化: print<int>(int) print(1.5); // 隐式实例化: print<double>(double) print<double>(1.5); // 也是隐式实例化, 但提供了 "显式模版实参" print<char>('h'); // 也是隐式实例化, 但提供了 "显式模版实参" } ``` <br><br> ## 显式实例化 #### 显式实例化定义 > 显式实例化定义 (Explicit Instantiation Definition, EID) "**==显式实例化==**" 语句用于**明确指示编译器"==在当前翻译单元=="根据==提供的模版实参==为==特定类型==生成模版实例**。 > [!caution] 在 "显式实例化语句" 之前,"模版定义"必须要对编译器可见! ```cpp title:explicit_template_instantiation.cpp // 声明一个类模版 template <typename T> class Box { ... }; // 声明一个函数模版 template <typename T> void print(T& value) {} // 显式实例化一个类模版, 生成一个模版类实例 template class Box<int>; // 显式实例化一个函数模版, 生成一个模版函数实例 template void print<int>(int&); ``` <br> #### 显式实例化声明 > 显式实例化声明 (Explicit Instantiation Declaration, EIDec) "**==显式实例化声明==**":在显式实例化语句前添加 `extern` 关键字,明确告诉编译器**在其他编译单元中已经或将会生成模板实例代码,因而==不需要在当前编译单元中进行实例化**==。 > [!caution] 在 "**显式实例化声明**" 语句之前,"模版定义" 也必须要对编译器可见! > > 尽管显式实例化声明(`extern template`)告诉编译器**不在当前翻译单元生成模板实例**,但**编译器仍然需要知道 "模板的定义" 以进行类型检查和语法验证**。 > [!important] **对每个==被显式实例化==的模版实例**,最终 **==整个程序中只会存在一份实例==**,无论相同的显式实例化语句在代码中出现多少次,又或者存在多少次对相同实例的使用。 > > 如果多个翻译单元中包含有相同的 "**显式实例化定义**" 语句(没有 `extern` 关键字),则: > > - 在编译阶段:**编译器会为每个翻译单元各自生成一份模版实例的定义**; > - 在链接阶段:**链接器会将多个重复模版实例定义==合并==**,最终只保留一份实例。 > > 因此,最终**整个程序中只会有一个模板实例的定义**。 > > 通常,为了**减少编译时间**,避免在多个编译单元中重复生成相同模板实例的代码,正确做法是: > > - 在一个源文件中进行**模板的==显式实例化定义==**(不使用`extern`关键字)。 > - 在**其他需要引用该模板实例**的源文件或头文件中,使用==**显式实例化声明**==(使用`extern`关键字); - **显式实例化定义**(在一个 `.cpp` 文件中): ```cpp title:explicit_instantiation_define.cpp template class MyClass<int>; // 在一个编译单元中显式实例化定义 ``` - **显式实例化声明**(在其他需要引用模版实例的源文件或头文件中): ```cpp title:explicit_instantiation_declare.h extern template class MyClass<int>; // "显式实例化声明" ``` <br><br> # 模版特例化 Specialization 模版特例化(Specialization)是**为模版定义一个 "==特定类型==" 或 "==参数组合==" 的特殊实现**。 模版特化包括:**全特例化**(Full specialization)、**部分特例化**(Partial specialization) - **==全特例化==**:为模板的 **==所有==模板参数** 提供具体类型或值。 - 例如,针对特定类型进行特殊实现; - ==**部分特例化**==:仅为模版的 **==部分==模版参数** 提供具体类型或值,或对参数施加一些约束。 - 例如,针对指针、迭代器的特殊处理; > [!Info] 一个 "全特例化" 版本本质上是一个 "**==实例==**",而一个 "部分特例化版本" 仍然是一个 "**模版**"。 > [!caution] 定义模版特例化版本时, "==原始模版==" 的定义必须在其之前可见!且必须 "==在原始模版所在的命名空间==" 中进行特例化 ### 定义说明 定义 "**全特例化**" 或 "**部分特例化版本**" 时: 1. **模版参数列表**: - 对于 "**全特例化**",模版参数列表为 **==空==**,即 `template <>`,表示不再需要模版参数; - 对于 "**部分特例化**",模版参数列表为 "**原始模版参数列表的一个子集**"; 2. **==类名或函数名==** 后,要通过 `<...>` 给出 **==用于特例化的具体模版参数==**。 > [!NOTE] 当需要为某个"特定类型"或"参数"**实现不同于"通用模版"的特定行为时**,可通过模版特化来实现。 > > - 对于 "**函数模版**" 和 "**成员模版**",只能进行 "**==全特化==**"; > - 对于 "**类模版**",可以进行 "**==全特化==**" 或 "**==偏特化==**"。 > - 还可以只 "**全特例化**" 其某个 "**成员函数**" ### 使用示例 示例一: "**全特例化**" 与 "**偏特例化**" ```cpp #include <iostream> using namespace std; // 声明了一个类模板 template <typename T> struct MyTemplate { void doSomething() { // ...一般实现 cout << "Generic version" << '\n'; } }; // 全特化为int类型: 当模版参数是int类型时, 将使用这个全特化的实现 template <> struct MyTemplate<int> { // 特化的标识 void doSomething() { // 针对int类型的特殊实现 cout << "Full specialized version for int" << '\n'; } }; // 偏特化为指针类型: 当模板参数是任何类型的指针时,将使用这个偏特化的实现。 template <typename T> struct MyTemplate<T*> { // 特化的标识 void doSomething() { // 针对指针类型的特殊实现 cout << "Partial specialized version for pointes" << '\n'; } }; int main() { MyTemplate<double> generic_obj; generic_obj.doSomething(); MyTemplate<int> full_specialized_obj; full_specialized_obj.doSomething(); MyTemplate<char*> partial_specialized_obj; partial_specialized_obj.doSomething(); } ``` 示例:`std::remove_reference<>` 等类模版的实现:通过 "**==struct"类型成员==** & "**==模版特例化==**" 进行实现 ```cpp // std::remove_reference<>的实现方式:通过"struct"类型成员 & "模版特例化" 进行实现. namespace MySpace { template <typename T> struct remove_reference { typedef T type; }; template <typename T> struct remove_reference<T&> { // 针对左值引用的 "模版特例化" 版本 typedef T type; }; template <typename T> struct remove_reference<T&&> { // 针对右值引用的 "模版特例化" 版本 typedef T type; }; } // end namespace MySpace int main() { MySpace::remove_reference<int&>::type var = 25; // var是int类型. return 0; } ``` 示例三:只为类模版中的某一个成员函数进行 "**全特例化**" ```cpp template <typename T> class Foo { public: Foo(const T&t = T()) : mem(t) {} void Bar() { cout << "Foo<T>::Bar()" << endl; } private: T mem; }; // 只特例化Foo<int>::Bar() template <> void Foo<int>::Bar() { cout << "Foo<int>::Bar()" << endl; } int main() { Foo<string> fs; fs.Bar(); // 原模版的`Foo<T>::Bar()` Foo<int> fi; fi.Bar(); // 特例化的`Foo<int>::Bar()` } ``` <br><br><br> # 模版参数 模板由一个或多个**模板参数**进行参数化,包括**类型模板参数**、**非类型模板参数**和**模板模板参数**三种。 > ![image-20240113221856745|350](_attachment/02-开发笔记/01-cpp/模版与泛型编程/cpp-模版基本概念.assets/IMG-cpp-模版基本概念-6A2A13C573622FF41F64129C948063F2.png) > [!caution] 模版内不能"重用 '模版' 参数名",否则编译错误。 <br> ## 模版参数的种类 ### 类型模版参数(type template parameter) 类型参数由**关键字 `typename` 或 `class` 指定**,表示将 "**类型**" 作为参数传递给模版。 这可以是**任意的 C++类型**,包括自定义类、结构体、联合体、枚举等。 ```cpp template <typename T> // 或者 template <class T> class MyClass { T data; // ... }; ``` <br> ### 非类型模版参数(non-type template paramter) 非类型模版参数用于**将一个 "==值=="(而非类型) 作为参数传递给模版**。 > [!caution] 非类型模版参数的实参必须是 "**==常量表达式==**",因为模版实例化发生在编译时,所以**要求值必须在编译时已确定**。 > > - 绑定到 "**==指针或引用非类型参数==**" 的实参**必须**具有 **==静态存储持续性==**(`static` 或全局变量,从而在编译时能够绑定;而自动变量和动态变量都是运行时在栈 or 堆上分配的) > - 指针非类型参数也可以用 `nullptr` 或者值为 `0` 的常量表达式进行初始化。 > > 在**模版定义内部**,**非类型模版参数也将作为一个==常量值==**,**可用在任何需要常量表达式的地方**,例如指定数组大小。 ```cpp template <int size> struct FixedArray { int data[size]; // ... }; ``` 非类型模版参数使用示例: 下例中,`for_each` 只能接受一个具有单参数的函数,因此通过 "模版" 可以最灵活地实现。 ```cpp // 非类型模版参数, 编译时确定 template <int value> // 将一个int型"值"(常量表达式)作为模版形参 void add(int& elem) { elem += value; } int main() { vector<int> coll; // 将"10"作为模版参数传入. for_each(coll.begin(), coll.end(), add<10>); // 将"25"作为模版参数传入. for_each(coll.begin(), coll.end(), add<25>); } // 另一种实现方式是: 定义一个双参数版本的add, 再通过bind绑定/固定"增加值"参数. ``` <br> ### 模版模版参数(Template template parameter) 模版参数是指将 "**另一个模版**" 作为当前模版的**模版参数**。 模版模版参数用于高级泛型编程场景,例如**编写一个可以接收任意 "容器" 类型的泛型算法**。 ```cpp // 第一项模版参数是个"类模版" tempalte <template <typename> class Container, typename T> class MyClass { Container<T> data; // 用类型参数`T`来实例化`Container`类模版, 得到一个模版类类型. /// ... } ``` <br><br> ## 默认模版实参 C++ 11 起,允许为 "类模版" 和 "函数模版" 提供**默认模版实参**"。 与函数默认实参一样,对于实参列表中的一个模版参数,**只有当其右侧所有参数都有默认实参时,其才可以拥有默认实参**。 ```cpp template <typename T = int, typename V = char> class MyClass { /* 类模版定义 */}; MyClass<> obj; // 使用全部默认模版实参来进行实例化, 必须提供空的模版参数列表`<>`. template <typename T = int> void myFunction(T param = T()) { /* 函数模版实现 */ } myFunction<>(); // 使用默认模版实参进行实例化. ``` > [!caution] 当需要使用全部默认实参时,必须提供空的模版参数列表 `<>`,而不是省略模版参数列表。 ##### 使用示例 使用示例:实现一个泛型的比较函数 ```cpp // 使用默认模版实参, 实现一个泛型比较函数 // 函数默认进行"小于""比较, 但可以通过第三个参数传入自定义的比较函数 template <typename T, typename F = std::less<T>> int compare(const T& v1, const T& v2, F f = F()) { if (f(v1, v2)) return -1; if (f(v2, v1)) return 1; return 0; } struct MyCompare { // 自定义仿函数. 实现比较逻辑 int operator()(int a, int b) { return 1; } }; int main() { int res1 = compare(0, 42); // 使用默认的比较函数 cout << res1 << endl; int res2 = compare(0, 42, std::greater<int>()); cout << res2 << endl; int res3 = compare(0, 42, MyCompare()); cout << res3 << endl; } ``` <br><br> ## 可变模版参数 > 参见 [^7] **模版参数**以及**函数模版中的参数**可以是 "**可变**" 的,可变数目的参数称之为 "**==参数包==**",包括两种: - **模版参数包**(template parameter packet) - **函数参数包**(function parameter packet) > [!NOTE] 用法说明 > > (1)**声明参数包**: > > - 模版参数列表中,`class` 或 `typename` 后跟**省略号**,指示其后参数为 "**==模版参数包==**",代表 "**零个或多个类型名**" 的列表。 > - 函数参数列表中,一个**类型名**后跟**省略号**,指示其后参数为 "**==函数参数包==**",代表"**零个或多个非类型参数**" 的列表。 > > (2)**使用参数包**: > > 提供一个包含 "**模版参数包**" 或 "**函数参数包**" 的 "**==模式==**"(例如包名,以包名为参数的函数调用语句等), > 在 "==**模式**==" 后跟省略号,表示进行 "**==包扩展==**"——**将 "模式" ==独立地应用于包中的每个元素==**。 > [!info] `sizeof...` 运算符:专为**可变模版参数**提供的操作符,用于获取 "**==参数包==**" 中的数量。 说明示例: ```cpp // 可变模版参数 template <typename... Args> // `Args`是模版参数包, 代表0或多个类型名 void func(Args&&... args) { // `args`是函数参数包, 代表0或多个参数, 对应`Args...`中类型. // 1) `const Args&...`, 展开包, 会将模式`T&&` 独立用于包中的每个类型T, 作为函数参数 // 2) `std::forward<Args>(args)...`, 展开"模版参数包Args" 与"函数参数包args" // 会将模式`std::forward<T>(t)`独立用于模版包中每个类型T & 函数包中每个对应参数t work(std::forward<Args>(args)...); } ``` #### 使用示例 > [!NOTE] STL 容器中的 `emplace_back()` 系列函数即应用了 "完美转发" 和 "可变参数" 两个特性。 获取参数包中的参数数量: ```cpp template <typename... Args> void func(Args... args) { cout << sizeof...(Args) << endl; // 类型参数的数目 cout << sizeof...(args) << endl; // 函数参数的数目 } ``` 使用示例: > [!NOTE] 可变参数函数模版通常是 "**==递归==**" 的,需要定义一个接收 "**不可变数量参数**" 的重载版本,作为**递归终止情况**。 ```cpp // 示例一: 可变的"类型模版参数" template<typename T, typename... Args> void process(T first, Args... rest); // 模版声明. template<typename... Args> void func(Args... args) { // `Args...`展开"模版参数包" process(args...); // `args...`展开具体的"函数参数包", 调用process(). } template<typename T, typename... Args> void process(T first, Args... rest) { cout << first << endl; // 打印第一个参数 // 若参数包非空, 继续递归调用. // 如果不使用下列语句判断参数情况, 就需要再定义一个无参数的`process()` 的版本, 以便终止递归. if constexpr (sizeof...(rest) > 0) { // `sizeof...()`是专为可变模版参数提供的操作符, 用于获取模版参数包中的数量. process(rest...); // 继续展开剩余参数包 } } // 示例二: 可变的非类型模版参数 template<int... Args> void countInts() { cout << "Number of interges: " << sizeof...(Args) << endl; // 输出可变参数数量 } int main() { func(1, "Hello", 3.14, 'A'); countInts<99, 98, 97, 95>(); } ``` <br><br> # `typename` 关键字的作用 `typname` 关键字用于**向编译器明确指示 "==一个名称==" 为==类型名==**。 当作用域运算符 `::` 前是一个 "**==模版类==**" 或者 "**==模版类型参数==**" 时,例如 `MyClass<T>::mem` 或 `T::mem`, **编译器无法确定通过该作用域运算符访问的是** "**==变量名==**" 还是 "**==类型名==**", **其==默认为是 "非类型名=="**, 因此**当需要==将 `::mem` 用作 "类型名" 使用==时,必须明确通过关键字 `typename` 指出**,否则会编译错误。 > [!NOTE] 对于 **已知的明确的类类型 `MyClass`** 以及**使用 `using` 声明的别名模版 `MyAliasTemp<T>`** 而言,编译器明确知道其定义,故不会产生歧义。 说明示例: ```cpp // 编译明确知道MyClass定义, 故知道其作用域下的成员`mem`是类型名还是变量 MyClass::mem // 对于模版类`MyClass<T>`, 编译器不能确定`mem`是类型名还是变量名, 因为可能定义有某个"模版特化", 在该特化中`mem`的含义与原始模版不同. // 因此, 当明确将`mem`用作一个类型名时, 必须通过`typename`关键字告知编译器, 否则将编译错误. typename MyClass<T>::mem ``` ##### 使用示例 示例一: ```cpp template <typename T> struct MyClass { // 在该类模版中`MyClass<T>::mem`是一个成员变量. int mem = 25; }; template <> struct MyClass<int> { using mem = int; // 在该模版特化中`MyClass<Wine>::mem`是一个类型名`int`; }; template <typename T> struct ExamClass { // 此处, 编译器不能确定`MyClass<T>::mem`是类型名还是变量名, 其默认为是"非类型名". // 当明确需要将其用作"类型名"时, 必须使用关键字`typename`显式指出, 否则编译错误. typename MyClass<T>::mem obj; }; ``` 示例二:`type_traits` 中的接口使用 ![[02-开发笔记/01-cpp/模版与泛型编程/cpp-type_traits 头文件#^1ecwlu]] 示例三: ```cpp template <typename T> class MyClass { typename T::SubType * ptr; }; // `typename`指明SubType是一个定义在类T中的类型, 因此ptr是一个指向T::SubType的指针. // 如果没有typename, SubType会被认为是一个类T的静态成员 // 于是T::SubType * ptr将被认为是类T的静态成员SubType乘以ptr; // 使用`typename`声明限定后, 任何用以实际替换T的类型, 都必须提供一个内部类型SubType. 例如: class Q { typedef int SubType; // 内部类型定义. ... }; MyClass<Q> x; // OK ``` <br><br><br> # 模版实参推断 > **==模板实参推断==** (template argument deduction)[^2] 模版实参推断是一种**编译时机制**,支持编译器**自动实例化模板时所需要的==模板参数的类型**==,无需显式指定。 C++中有以下两种模版实参推断: - **函数模版实参推断** - 根据传递给 "**函数调用**" 的参数推导模版参数类型 - **类模版实参推断**(CTAD) since C++17 - 根据传递给 "**构造函数**" 的参数推导模版参数类型 ## 模版实参推断规则 模版类型实参的推导规则**遵循 C++中整体的类型推导规则**,参见 [[02-开发笔记/01-cpp/类型相关/cpp-类型推导#类型推导规则总结 ⭐|cpp-类型推导#类型推导规则总结]] 其中,模版类型参数 `T` 只是个占位符,**带有模版参数的"==函数形参==" 的类型** 由**函数形参声明形式**决定。 而**模版类型参数 `T` 则由编译器根据实参类型进行推导**。 ##### 对 "模版参数实参" 的自动类型转换 将实参传递给带 "模版类型参数" 的形参时,**能够自动应用的类型转换只有两种**: - **==const 转换==**:可以将一个**non-const 对象的引用(或指针)** 传递给一个 **const 引用(或指针)形参**。 - **数组名或函数名到指针的转换**: - 数组名转换为指向数组元素类型的指针; - 函数名转换为函数指针; **其余情况下**,编译器通常将 "**==生成一个新的模版函数实例==**",而**不会对实参进行下列类型转换**,包括: - 算术转换; - 派生类向基类的向上转型; - 用户定义的转换; 说明示例: ```cpp template <typename T> void func_obj(T, T) {} template <typename T> void func_ref(const T&, const T&) {} int main() { string s1("hello"); const string s2("world"); // 1.忽略顶层const func_obj(s1, s2); // 模版参数类型推导为string, 忽略`s2`的顶层const. // 2.const转换: 非const转为const func_ref(s1, s2); // 模版参数类型推导为`const string&`, 将`s1`的string转换`const string&` // 3.数组或函数指针的转换 int a[10], b[42]; func_obj(a, b); // 模版参数类型推导为`int*`, 根据数据名推导为指向数组首元素的指针 // func_ref(a, b); // error: 无法推导出模版参数类型 } ``` <br> ## 函数模版实参推断 函数模版在**被使用时(==隐式实例化==)可以==省略模版参数**==,也可以提供 "**显式模版实参列表**"。 如果省略,则编译器将**根据函数调用时使用的实参类型进行推断**,从而生成 **==函数模版实例==**(也称"**模版函数**"),该过程称之为 "**==模版实参推断==**" [^6]。 > [!caution] 当一个模版类型参数 "**==只==出现在函数返回类型**" 时,无从进行推断,必须在调用函数时**显式提供 "模版参数列表"**。 > [!info] 类模版不支持 "模版实参" 推断,必须显式给出。 ```cpp template <typename T> void print(const T& value) { std::cout << value << std::endl; } int main() { print(42); print(3.125); print('G'); print("ABCDEFG); } ``` ##### 函数模版隐式实例化的场景 - (1)**直接调用**时 => 模版实参根据 "**==传入的调用参数==**" 进行推断; - (2)**初始化或赋值一个函数指针**时 => 模版实参根据 "**==函数指针的类型==**" 进行推断[^5]; 示例:将一个"**函数模版**" 用于**初始化或赋值一个函数指针**时,编译器**将根据==函数指针的类型==来推断模版实参**: ```cpp template <typename T> int compare(const T&, const T&) { return 0; } // 一个函数模版 int main() { // 函数模版赋给函数指针时的模版实参推断 int (*pf) (const int&, const int&) = compare; // 用函数模版初始化一个函数指针 pf = compare; //将函数模版赋给一个函数指针 // 显式指出 pf = compare<int>; // 显式指出模版实参 // pf = compare<char>; // error: 指针所指类型不匹配, 不能赋值 } ``` <br> ## 类模版参数推断(CTAD) 类模板参数推断(Class Template Argument Deduction, CTAD) C++17 引入的类模版参数推断允许在**创建==类模版实例的对象==时省略模版参数**,编译器将**根据==构造函数的参数==自动推断模板参数的类型**。 ```cpp template <typename T> struct MyClass { MyClass(T v) : value(v) {} T value; }; // C++17之前, 必须指定类模版参数. MyClass<int> obj_i(25); MyClass<char> obj_ch('G'); // C++17起, 支持类模版参数推断 MyClass obj_i2(25); MyClass obj_ch2('G'); ``` <br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later # ♾️参考资料 # Footnotes [^1]: 《Effective Modern C++》 [^2]: [Template argument deduction - cppreference.com](https://en.cppreference.com/w/cpp/language/template_argument_deduction) [^6]: 《C++Primer》P601 [^7]: 《C++ Primer》P619