# 纲要
> 主干纲要、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>
# 模版参数
模板由一个或多个**模板参数**进行参数化,包括**类型模板参数**、**非类型模板参数**和**模板模板参数**三种。
> 
> [!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