文章目录
- 一、函数模板
- 二、类模板与成员函数模板
- 三、Concepts(C++20)
- 四、模板相关内容
- 1.数值模板参数与模板模板参数
- 2.别名模板与变长模板
- 3.包展开与折叠表达式
- 4.完美转发与lambda表达式模板
- 5.消除歧义与变量模板
一、函数模板
在C++中,函数模板是一种允许你编写可以处理多种数据类型的函数的方式。函数模板通过使用模板参数来实现泛型编程,这样同一个函数就可以用不同的数据类型来调用。
函数模板不是函数。
使用 template 关键字引入模板:
使用 template<typename T>
或者 template<class T>
来定义一个函数模板。typename
和 class
在这里可以互换使用,但通常 typename
更常用于模板参数。
template<typename T>
void fun(T)
{
//...
}
函数模板的声明与定义:
在一个翻译单元中,函数声明可以包含多次,但函数定义只能包含一次。函数模板的声明和定义通常写在一起。例如:
template<typename T>
void functionTemplate(T param) {
// 函数实现
}
这个函数模板 functionTemplate
可以接受任何类型的参数 param
。
函数模板参数:
函数模板中包含了两对参数:函数形参 / 实参;模板形参 / 实参。
- 函数形参:函数定义中的参数,如上面例子中的
param
。 - 函数实参:调用函数时传递给函数的具体参数值。
- 模板形参:在模板定义中使用的类型或值的占位符,如
T
。 - 模板实参:在调用模板时,提供给模板形参的具体类型或值
函数模板的显式实例化:
在C++中,函数模板的显式实例化是一种告诉编译器创建一个特定函数模板实例的操作。
-
显式实例化的语法
显式实例化使用模板函数名后跟尖括号内指定的类型参数来完成。例如,
fun<int>(3)
告诉编译器实例化模板函数fun
并使用int
作为模板参数,然后调用这个实例化函数并传递整数3
作为参数。 -
实例化会使得编译器产生相应的函数(函数模板并非函数,不能调用)
使用C++ Insights可知:
-
编译期的两阶段处理(函数模板的实例化发生在编译期)
-
模板语法检查
编译器首先检查模板代码的语法是否正确
-
模板实例化
编译器根据提供的模板参数来生成具体的函数代码。
-
-
模板必须在实例化时可见–翻译单元的一处定义原则
模板的定义只在一个翻译单元中出现一次,以避免链接错误。
-
与内联函数的异同
虽然函数模板与内联函数都满足翻译单元级别的一次定义原则而非程序级别的一次定义原则,但原因是不同的。
- 共同点:两者都可以在编译时进行优化,并且都可以在头文件中定义。
- 不同点:内联函数不涉及类型参数,而函数模板是类型安全的泛型函数。
函数模板的重载:
在C++中,函数模板的重载指的是可以定义多个具有相同名称但模板参数不同的函数模板。当编译器尝试确定哪个函数模板实例与给定的调用匹配时,它会根据传递给函数的实参类型来解析重载。如果存在多个匹配的模板实例,编译器将选择最匹配的一个。
示例:
#include <iostream>
template<typename T>
void fun(T input)
{
std::cout << input << std::endl;
}
template<typename T>
void fun(T* input)
{
std::cout << *input << std::endl;
}
template<typename T, typename T2>
void fun(T input, T2 input2)
{
std::cout << input << std::endl;
std::cout << input2 << std::endl;
}
int main()
{
double x = 3.14;
fun<int>(3);
fun<double>(&x);
}
编译后的结果为
#include <iostream>
template<typename T>
void fun(T input)
{
(std::cout << input) << std::endl;
}
/* First instantiated from: insights.cpp:25 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void fun<int>(int input)
{
std::cout.operator<<(input).operator<<(std::endl);
}
#endif
template<typename T>
void fun(T * input)
{
(std::cout << *input) << std::endl;
}
/* First instantiated from: insights.cpp:26 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void fun<double>(double * input)
{
std::cout.operator<<(*input).operator<<(std::endl);
}
#endif
template<typename T, typename T2>
void fun(T input, T2 input2)
{
(std::cout << input) << std::endl;
(std::cout << input2) << std::endl;
}
int main()
{
double x = 3.1400000000000001;
fun<int>(3);
fun<double>(&x);
return 0;
}
模板实参的类型推导:(隐式实例化)
在C++中,模板实参的类型推导是一个自动确定模板参数类型的过程,推导是基于函数实参(表达式)确定模板实参的过程。如果函数模板在实例化时没有显式指定模板实参,那么系统会尝试进行推导。
模板实参类型推导的基本原则:(与auto类型推导相似)
-
当函数形参是左值引用或指针时
忽略实参表达式的引用部分,并尝试匹配表达式的类型与形参类型来确定模板实参。
示例:
template<typename T> void func(T& param) { // ... } int main() { const int a = 5; func(a); // T& 推导为 const int }
-
当函数形参是万能引用(使用
T&&
声明)时模板实参的类型推导将根据实参表达式的值类别(左值或右值)来确定:
- 如果实参是一个右值,模板实参将被推导为被推导为去掉引用的基本类型
- 如果实参是一个左值,模板实参将被推导为左值引用类型,这将触发引用折叠规则。
示例:
template<typename T> void func(T&& param) { // ... } int main() { const int a = 5; func(a); // T 推导为 const int&,引用折叠指的是const int& && ==> const int& func(10.1); //T 推导为double }
-
当函数形参不包含引用时
模板实参的类型推导将忽略实参表达式的引用部分和顶层
const
,并且:- 数组和函数类型将转换成相应的指针类型。
- 其他类型将直接推导为该类型。
示例:
template<typename T> void func(T param) { // ... } void someFunction() { } int main() { int x = 2; const int& y = x; func(x); //T推导为 int,忽略引用与顶层const const int* const ptr = &x; func(ptr); //T推导为const int*,忽略顶层const int arr[] = {1, 2, 3}; func(arr); // T 推导为 int*,因为 arr 是数组类型 void (*funcPtr)() = someFunction; func(funcPtr); // T 推导为 void (*)(),因为 funcPtr 是函数指针 }
模板实参并非总是能够推导得到:
-
如果模板形参与函数形参的类型无关,则编译器可能无法从函数实参推断出模板实参的类型。
例如:
template<typename T, typename U> U func(T param) { // ... }
-
即使相关,也不一定能进行推导,
-
推导成功也可能存在因歧义而无法使用
例如:
template<typename T> void func(T param1, T param2) { // ... } int main() { func(3, 5.0); }
在无法推导时,编译器会选择使用缺省模板实参,可以为任意位置的模板形参指定缺省模板实参。
注意与函数缺省实参的区别,不需要保证缺省实参右边全部为缺省实参
例如:
template<typename T, typename U = int>
U func(T param) {
// ...
}
int main()
{
func(3);
}
显式指定部分模板实参:
即在调用模板函数时明确指定其中一些模板参数,而让编译器自动推导剩余的参数。
-
显式指定的模板实参必须从最左边开始,依次指定
一旦你开始显式指定模板实参,编译器将自动推导剩余未指定的模板参数。如果推导失败,将导致编译错误。
-
模板形参的声明顺序会影响调用的灵活性
例如:
//这种模板形参的声明顺序会编译错误 template<typename T, typename U> U func(T param) { // ... } template<typename U, typename T> U func(T param) { // ... } int main() { func<int>(3); }
函数模板自动推导时会遇到几种情况:
-
函数形参无法匹配—— SFINAE (替换失败并非错误)
当模板参数推导失败时,如果是因为类型不匹配导致的,编译器会认为这不是一个错误,而是简单地排除这个模板实例化选项。
-
模板与非模板同时匹配,匹配等级相同,此时选择非模板的版本
如果模板函数和非模板函数都可以匹配同一个调用,并且它们的匹配等级相同,那么编译器将选择非模板函数。这是因为在C++中,非模板函数的优先级高于模板函数:
例如:
void func(int i) { // ... } template<typename T> void func(T t) { // ... } int main() { func(5); // 调用非模板 func(int i),因为匹配等级相同 }
-
多个模板同时匹配,此时采用偏序关系确定选择”最特殊“的版本
如果有多个模板实例都可以匹配同一个调用,C++的重载解析规则将根据偏序关系来确定“最特殊”的版本。这通常涉及到模板参数的特化程度,更具体的模板实例会被优先选择:
例如:
template<typename T> void func(T t) { // ... } template<> void func<int>(int i) { // ... } int main() { func(5); // 调用模板特化 func<int>(int i),因为它更具体 }
函数模板的实例化控制:显式实例化但不调用
-
显式实例化定义
显式实例化定义是告诉编译器为特定的模板参数生成一个实例。这通常在模板定义的实现文件中完成。
template void fun<int>(int); 或者 template void fun(int);
-
显式实例化声明
显式实例化声明用于告诉编译器存在一个显式实例化的定义。
extern template void fun<int>(int); 或者 extern template void fun(int);
如果引入了显式实例化声明,就不会产生模板实例,减轻了编译器的负担,在链接过程中也不需要将相同的实例删除掉,提升编译与链接速度。
-
注意一处定义原则(程序级别)
C++要求每个模板显式实例化在程序中只能有一个定义。这意味着显式实例化定义只能出现在一个编译单元(通常是一个.cpp文件)中。违反这一原则会导致链接错误。
-
注意实例化过程(显式实例化定义)中的模板形参推导
函数模板的(完全)特化:
C++中的函数模板特化是为特定类型提供特定实现的一种方式。与函数重载不同,特化是为已经存在的模板函数提供针对特定类型的特定实现。本质上函数模板特化就是函数模板实例化。
完全特化语法:
// 模板定义
template<typename T>
void f(T t) {
// 通用实现
}
// 函数模板的完全特化
template<>
void f<int>(int t) {
// int 类型的具体实现
}
-
并不引入新的(同名)名称,只是为某个模板针对特定模板实参提供优化算法
特化不会创建新的函数名称。它只是为模板函数提供了一个针对特定参数的定制版本。这意味着特化和非特化版本在函数名上是相同的,只是参数类型不同。
-
注意与重载的区别
- 重载:涉及创建多个具有相同名称但参数类型或数量不同的函数。
- 特化:为模板函数提供针对特定类型的定制实现,不增加新的函数名称。
-
注意特化过程中的模板形参推导
特化可以影响模板形参的推导过程。
避免使用函数模板的特化:
-
不参与重载解析,会产生反直觉的效果(重载解析是在函数模板特化之前完成的)
函数模板特化不参与普通的重载解析过程。这意味着即使存在一个更匹配的非特化版本,编译器也可能选择特化版本,因为特化版本在重载候选中具有更高的优先级。这可能导致一些反直觉的结果。
-
通常可以用重载代替(函数重载会参与重载解析)
优先使用函数重载而不是模板特化。函数重载遵循标准的重载解析规则,这使得代码的行为更加可预测和直观。
-
一些不便于重载的情况:无法建立模板形参与函数形参的关联,可以考虑一下替代方案
-
使用
if constexpr
解决if constexpr
是C++17引入的一个特性,它允许在编译时根据模板参数的值选择执行不同的代码路径。这可以用来模拟重载的效果#include <type_traits> template<typename T> void func(T t) { if constexpr (std::is_same_v<T, int>) { // 针对 int 类型的代码 } else { // 通用代码 } }
-
引入“假”函数形参
-
通过类模板特化解决
使用类模板特化,然后在类中定义需要重载的函数。这种方法可以将重载的复杂性封装在类内部:
template<typename T> struct MyClass { void func(T t) { // 通用实现 } }; template<> struct MyClass<int> { void func(int t) { // 针对 int 类型的实现 } };
-
函数模板的简化形式(C++20):使用auto定义模板参数类型
在C++20中,引入了使用auto
来定义函数模板参数类型的简化形式。
-
优势:书写简捷
-
劣势:在函数内部需要间接获取参数类型信息
二、类模板与成员函数模板
在C++中,类模板是一种泛型编程工具,允许你创建可以处理多种数据类型的类。
类模板不是类
-
使用template关键字引入类模板:
使用
template<typename T>
或template<class T>
来声明一个类模板。typename
和class
在这里可以互换使用,但typename
更常用于模板参数。template<typename T> class B { // 包括成员变量、成员函数的实现 };
-
类模板的声明与定义:翻译单元级别的一处定义原则
类模板的声明和定义通常写在一起。类模板的定义包括成员变量、成员函数的实现等。
-
成员函数只有在调用时才会被实例化
类模板的成员函数只有在被调用时才会被实例化。这意味着编译器会根据成员函数调用时提供的实参来生成具体的函数实现。
#include <iostream> template<typename T> class B { public: void fun(T input) { std::cout << input << std::endl; } }; int main() { B<int> x; x.fun(3); }
经编译器实例化后如下:
#include <iostream> template<typename T> class B { public: inline void fun(T input) { (std::cout << input) << std::endl; } }; /* First instantiated from: insights.cpp:14 */ #ifdef INSIGHTS_USE_TEMPLATE template<> class B<int> { public: inline void fun(int input) { std::cout.operator<<(input).operator<<(std::endl); } // inline constexpr B() noexcept = default; }; #endif int main() { B<int> x; x.fun(3); return 0; }
类模板中的成员函数本质上是内联函数
-
类内类模板名称的简写
在类模板的成员函数中,可将类模板名称进行简写
-
类模板成员函数的定义(类内、类外)
类模板的成员函数可以在类内定义(内联定义)或类外定义。
-
类内定义
成员函数在类模板内部定义时,不需要再次使用
template
关键字,编译器能够从上下文中推断出模板参数。template<typename T> class B { public: void memberFunc() { // 实现 } };
-
类外定义
当成员函数在类外部定义时,需要使用模板关键字,并显式指定模板参数。
template<typename T> class B { public: void memberFunc(); }; template<typename T> void B<T>::memberFunc() { // 实现 }
-
成员函数模板:
-
类的成员函数模板
位于类内部的函数模板,同上也可分为类内定义与类外定义
class B { public: template<typename T> void func(T input); }; template<typename T> void B::func(T input) { } int main() { B x; x.func<int>(3); }
-
类模板的成员函数模板
类模板可以包含成员模板函数,这些函数在类内或类外定义
template<typename T> class B { public: template<typename T2> void func(T2 input); }; template<typename T> template<typename T2> void B<T>::func(T2 input) { } int main() { B<int> x; x.func<int>(3); }
友元函数模板:(很少使用)
- 可以声明一个函数模板为某个类(模板)的友元
- C++11 支持声明模板参数为友元
template<typename T>
class B {
public:
template<typename T2>
friend void func(T2 input);
private:
int x;
};
template<typename T2>
void func(T2 input)
{
B<int> tmp1;
tmp1.x;
B<char> tmp2;
tmp2.x;
}
int main()
{
func<float>(3);
}
类模板的实例化:
C++中的类模板实例化与函数模板实例化在概念上是相似的,都涉及到根据提供的模板实参生成具体的类型或函数。
详细内容可参考:https://en.cppreference.com/w/cpp/language/class_template
-
实例化过程
类模板的实例化是通过替换模板参数来创建一个具体类的版本。这个过程可以是隐式的,也可以是显式的。
-
隐式实例化
当你创建一个类模板的对象或调用其成员函数时,如果模板参数没有明确指定,编译器会自动推导这些参数,从而实例化类或成员函数。
template<typename T> class Box { T item; public: Box(T t) : item(t) {} T getItem() const { return item; } }; int main() { Box<int> myBox(10); // 隐式实例化 Box<int> return 0; }
-
显式实例化
显式地要求编译器实例化类模板的特定版本。这通常在类模板的定义完成后进行
template class Box<int>; // 显式实例化 Box<int>
-
可以实例化整个类模板或者类模板中的某个成员函数
-
实例化整个类模板,即为该类创建一个具体的类型版本
Box<double> anotherBox(5.5); // 实例化 Box<double> 的对象
-
实例化类模板中的某个成员函数,特别是当成员函数是模板时
template<typename T> class MyClass { public: template<typename U> U templatedMethod(U u) { return u; } }; int main() { // 实例化 MyClass<int> 的 templatedMethod<double> MyClass<int> myObject; double result = myObject.templatedMethod<double>(3.14); }
-
类模板的(完全)特化 / 部分特化:
-
完全特化
完全特化是指为类模板的特定类型参数提供完全定制的类定义。特化版本与基础版本可以完全不同,不继承或包含基础模板的任何成员。
// 基础模板类定义 template<typename T> class MyClass { public: void func() { /* ... */ } }; // 完全特化版本 template<> class MyClass<int> { public: void func() { /* 完全不同的实现 */ } };
MyClass<int>
是MyClass
的一个完全特化版本,它具有与基础模板完全不同的实现。 -
部分特化
部分特化或偏特化是指当类模板接受多个类型参数时,可以为其中一些参数提供特化,而其他参数保持通用。
// 基础模板类定义,接受两个类型参数 template<typename T1, typename T2> class MyClass { public: void func() { /* ... */ } }; // 部分特化版本,T2 特化为 int template<typename T1> class MyClass<T1, int> { public: void func() { /* 针对 T1 的任意类型和 T2 为 int 的特化实现 */ } };
MyClass<T1, int>
是一个部分特化版本,它只针对T2
为int
的情况提供了定制的实现,而T1
可以是任何类型。
类模板的实参推导:(从C++17开始)
-
基于构造函数的实参推导
C++17允许编译器根据构造函数的参数来推导类模板的模板参数。如果构造函数的参数能够明确地推导出模板参数的类型,编译器将自动实例化类模板。、
template<typename T> class Wrapper { public: T value; Wrapper(T v) : value(v) {} }; int main() { Wrapper w(42); // C++17 允许从 42 推导 T 为 int }
经编译器翻译后如下:
template<typename T> class Wrapper { public: T value; inline Wrapper(T v) : value(v) { } }; /* First instantiated from: insights.cpp:9 */ #ifdef INSIGHTS_USE_TEMPLATE template<> class Wrapper<int> { public: int value; inline Wrapper(int v) : value{v} { } }; #endif int main() { Wrapper<int> w = Wrapper<int>(42); return 0; } template<typename T> Wrapper(T v) -> Wrapper<T>; /* First instantiated from: insights.cpp:9 */ #ifdef INSIGHTS_USE_TEMPLATE template<> Wrapper(int v) -> Wrapper<int>; #endif
-
用户自定义的推导指引
C++17还引入了自定义的推导指引(Deduction Guide),允许开发者提供构造函数的重载来帮助编译器进行类型推导。推导指引需要在类模板的作用域外部定义,以便编译器能够在整个程序中找到并使用它们。
template<typename T> class Wrapper { public: T value; Wrapper(T v) : value(v) {} // 普通构造函数 }; // 自定义推导指引 template<typename T> Wrapper(T) -> Wrapper<T>;
-
注意:引入实参推导并不意味着降低了类型限制
即使类模板的参数可以从构造函数参数中推导出来,这并不意味着模板参数可以是任何类型。模板参数仍然需要满足类模板中定义的任何类型约束或要求。
-
C++ 17 之前的解决方案:引入辅助模板函数
template<typename T> class Wrapper { public: T value; Wrapper(T v) : value(v) {} // 普通构造函数 }; // 辅助模板函数 template<typename T> Wrapper<T> make_Wrapper(T v) { return Wrapper<T>(v); } int main() { auto w = make_Wrapper(42); // 使用辅助函数创建 Wrapper 实例 }
C++中有很多类似的函数,如:make_pair
-
类模板实参推导的限制
- 类模板实参推导只适用于构造函数。
- 如果构造函数重载,编译器需要能够从上下文推导出应该使用哪一个构造函数。
- 如果存在多个可能的模板实例化,编译器将尝试找到最佳匹配。
三、Concepts(C++20)
详细内容可参考:https://en.cppreference.com/w/cpp/language/constraints
C++模板的问题:没有对模板参数引入相应的限制,会造成如下两个问题:
-
参数是否可以正常工作,通常需要阅读代码进行理解
-
编译报错友好性较差(vector<int&>)
C++编译器在模板实例化失败时生成的错误信息通常很长且难以理解。这是因为模板在编译时展开,如果模板参数不符合要求,编译器需要报告所有相关的错误。例如,
vector<int&>
是一个错误用法,因为vector
需要其元素类型是可复制的,而引用类型int&
不满足这一要求。这将导致编译错误,但错误信息可能不会直接指出问题所在。
C++20 引入了 Concepts(概念),这是一种新的类型系统特性,用于在编译期对模板参数进行更严格的约束。Concepts 允许开发者定义模板参数必须满足的条件,这些条件被称为编译期谓词,它们返回 true
或 false
来表明模板参数是否符合预期。
基本概念:
-
concept:编译期谓词,它定义了一组类型必须满足的要求。
#include <iostream> #include <type_traits> template <typename T> concept IsAvail = std::is_same_v<T, int> || std::is_same_v<T, float>; int main() { return IsAvail<int>; }
-
constraints(约束):concept与 constraints ( require 从句)一起使用限制模板参数。通常置于表示模板形参的尖括号后面进行限制。
#include <iostream> #include <type_traits> template <typename T> concept IsAvail = std::is_same_v<T, int> || std::is_same_v<T, float>; template <typename T> requires IsAvail<T> void fun(T input) { } int main() { fun(3); }
concept 的定义与使用:
-
包含一个模板参数的 concept
-
使用 requires 从句
#include <iostream> #include <type_traits> template <typename T> concept IsAvail = std::is_same_v<T, int> || std::is_same_v<T, float>; template <typename T> requires IsAvail<T> void fun(T input) { } int main() { fun(3); }
-
直接替换 typename
#include <iostream> #include <type_traits> template <typename T> concept IsAvail = std::is_same_v<T, int> || std::is_same_v<T, float>; template <IsAvail T> void fun(T input) { } int main() { fun(3); }
-
-
包含多个模板参数的 concept
#include <iostream> #include <type_traits> template <typename T, typename T2> concept IsAvail = !std::is_same_v<T, T2>; template <typename T, typename T2> requires IsAvail<T, T2> void fun(T input, T2 input2) { } int main() { fun(3, 3.14); }
用做类型 constraint 时,少传递一个参数,推导出的类型将作为首个参数
#include <iostream> #include <type_traits> template <typename T, typename T2> concept IsAvail = !std::is_same_v<T, T2>; template <IsAvail<int> T> void fun(T input) { } int main() { fun(3.14); }
requires表达式:
注意区分requires表达式与requires从句的含义
requires
从句用于模板定义中,它指定了模板参数必须满足的条件。requires
表达式是requires
从句的一部分,它用于定义概念(Concepts)。
- 简单表达式:表明可以接收的操作
- 类型表达式:表明是一个有效的类型
- 复合表达式:表明操作的有效性,以及操作返回类型的特性
- 嵌套表达式:包含其它的限定表达式
requires 从句会影响重载解析与特化版本的选取
-
只有 requires 从句有效而且返回为 true 时相应的模板才会被考虑
当编译器进行函数调用时,它会尝试找到匹配的函数重载版本。如果一个模板的
requires
从句中的条件不满足,那么即使模板的其他部分与调用匹配,这个模板版本也不会被考虑。这意味着requires
从句充当了一种编译期的筛选器,确保只有当条件满足时,相应的模板实例才会被考虑。#include <iostream> #include <type_traits> template <typename T> requires std::is_same_v<T, float> void fun(T input) { std::cout << "float"; } template <typename T> requires std::is_same_v<T, int> void fun(T input) { std::cout << "int"; } int main() { fun(3); //第二个模板将会被调用 }
-
requires 从句所引入的限定具有偏序特性,系统会选择限制最严格的版本
当存在多个模板特化版本时,编译器会根据
requires
从句所引入的限定来选择最合适的特化。这些限定具有偏序特性#include <iostream> #include <type_traits> template <typename T> concept C1 = std::is_same_v<T, int>; template <typename T> concept C2 = std::is_same_v<T, float> || std::is_same_v<T, int>; template <C1 T> void fun(T input) { std::cout << "1"; } template <C2 T> void fun(T input) { std::cout << "2"; } int main() { fun(3); }
特化小技巧:在声明中引入“ A||B” 进行限制,之后分别针对 A 与 B 引入特化
#include <iostream>
#include <type_traits>
template <typename T>
requires std::is_same_v<T, int> || std::is_same_v<T, float>
class B;
template <>
class B<int> {};
template <>
class B<float> {};
int main()
{
B<double> x;
}
四、模板相关内容
1.数值模板参数与模板模板参数
数值模板参数:
模板可以接收(编译期常量)数值作为模板参数
-
使用int类型的编译器常量
其写法为:
template <int a> class Str;
示例:
template <int a> int fun(int x) { return x + a; } int main() { fun<3>(5); }
-
使用类型与编译器常量的组合
这种方式允许你指定一个类型
T
和一个该类型的编译期常量value
。其语法为:
template <typename T, T value> class Str;
示例:
template <typename T, T a> int fun(int x) { return x + a; } int main() { fun<int, 3>(5); }
-
使用
auto
关键字来简化模板参数的定义(C++17)在C++17中,可以使用
auto
关键字来简化模板参数的定义,使得模板参数可以自动推断为传递给它的值的类型。其语法为:
template <auto value> class Str { // ... };
示例:
template <auto a> int fun(int x) { return x + a; } int main() { fun<3>(5); fun<true>(5); }
-
接收字面值类对象与浮点数作为模板参数(C++20)
C++20进一步扩展了模板非类型参数,允许使用字面值类对象和浮点数作为模板参数。
其语法为:
template <double value> class FloatStr { // ... };
支持还不完整,有些编译器不支持
模板模板参数:
在 C++ 中,模板可以接收另一个模板作为参数
-
模板的模板参数(C++17之前)
即一个模板的模板参数为模板T。在 C++17 之前,模板的模板参数需要显式指定类型说明符
class
。其语法为:
template <template<typename T> class C> class Str { // ... };
-
C++17开始允许省略类型说明符
C++17 标准放宽了对模板的模板参数的语法要求,允许在模板的模板参数中省略类型说明符
class
。其语法为:
template <template<typename T> typename C> class Str { // ... };
示例:
#include <vector> template <template<typename T> typename C> void fun() { C<int> tmp; }; int main() { fun<std::vector>(); }
-
C++17 开始,模板的模板实参考虑缺省模板实参
如:上面的vector类模板实际有两个参数,第二个参数为缺省实参
支持还不完整,有些编译器不支持
2.别名模板与变长模板
别名模板:
在 C++ 中,using
关键字可以用于引入别名,可以使用 using
引入别名模板。
-
为模板本身引入别名
template <typename T> class MyClass { // ... }; // 为模板本身引入别名 template <typename T> using MyAlias = MyClass<T> ; int main() { // 使用别名创建对象 MyAlias<int> myObject; }
-
为类模板的成员引入别名
template <typename T> class MyClass { public: template <typename U> class InnerClass { // ... }; using InnerType = InnerClass<T>; // 为 InnerClass 模板的特定实例引入别名 }; int main() { // 使用别名访问类模板的成员 MyClass<int>::InnerType myInnerObject; }
-
别名模板不支持特化,但可以为基于类模板的特化引入别名,以实现类似特化的功能
template <typename T> class MyClass { // ... }; // 特化模板 template <> class MyClass<double> { // ... }; int main() { // 为特化的模板引入别名 using MyDoubleClass = MyClass<double>; }
变长模板:
详细内容可参考:https://zh.cppreference.com/w/cpp/language/parameter_pack
C++中的变长模板(Variadic Templates),也称为参数包(Parameter Packs),是一种强大的模板特性,允许模板接受任意数量的模板参数。
-
变长模板参数与参数包
变长模板参数使用省略号(
...
)来表示,可以与模板参数列表中的其他参数一起使用template <typename... Types> class Tuple { // Types 是一个类型参数包,可以包含任意数量的类型 };
-
变长模板参数可以是数值、类型或模板
-
类型参数包:可以用于模板的类型参数。
#include <iostream> template <typename ... T> void fun(T... args) { } int main() { fun<int, double, char>(3, 5.3, 'c'); }
变长函数模板可以用任意数量的函数实参调用
-
数值参数包:可以用于模板的非类型参数。
template <int... Values> void printInts() { // 使用递归或迭代来打印 Values 中的整数 };
-
模板参数包:可以用于模板的模板参数。
template <template <typename> class... Templates> class TemplateHolder { // Templates 是一个模板参数包 };
-
-
sizeof...
操作(C++11)sizeof...
操作符用于获取参数包中的参数数量template<class... Types> struct count { static const std::size_t value = sizeof...(Types); };
-
注意变长模板参数的位置
-
在主类模板中,模板形参包必须是模板形参列表的最后一个形参。特化模板没有这个限制
-
在函数模板中,模板参数包可以在列表中更早出现,只要其后的所有形参都可以从函数实参推导或拥有默认实参即可:
template<typename U, typename... Ts> // OK:能推导出 U struct valid; // template<typename... Ts, typename U> // 错误:Ts... 不在结尾 // struct Invalid; template<typename... Ts, typename U, typename=void> void valid(U, Ts...); // OK:能推导出 U // void valid(Ts..., U); // 不能使用:Ts... 在此位置是不推导语境 valid(1.0, 1, 2, 3); // OK:推导出 U 是 double,Ts 是 {int, int, int}
-
3.包展开与折叠表达式
包展开(C++11):
通过包展开技术操作变长模板参数。
-
模式
T
后随省略号且其中至少有一个形参包的名字会被展开成零个或更多个逗号分隔的模式实例,其中形参包的名字按顺序被替换成包中的各个元素。示例:
template<class... Us> void f(Us... pargs) {} template<class... Ts> void g(Ts... args) { f(&args...); // “&args...” 是包展开 // “&args” 是它的模式 } int main() { g(1, 0.2, "a"); // Ts... args 会展开成 int E1, double E2, const char* E3 // &args... 会展开成 &E1, &E2, &E3 // Us... 会展开成 int* E1, double* E2, const char** E3 }
编译器会翻译成
template<class ... Us> void f(Us... pargs) { } /* First instantiated from: insights.cpp:7 */ #ifdef INSIGHTS_USE_TEMPLATE template<> void f<int *, double *, const char **>(int * __pargs0, double * __pargs1, const char ** __pargs2) { } #endif template<class ... Ts> void g(Ts... args) { f(&args... ); } /* First instantiated from: insights.cpp:12 */ #ifdef INSIGHTS_USE_TEMPLATE template<> void g<int, double, const char *>(int __args0, double __args1, const char * __args2) { f(&__args0, &__args1, &__args2); } #endif int main() { g(1, 0.20000000000000001, "a"); return 0; }
-
如果两个形参包在同一模式中出现,那么它们同时展开而且长度必须相同:
template<typename...> struct Tuple {}; template<typename T1, typename T2> struct Pair {}; template<class... Args1> struct zip { template<class... Args2> struct with { typedef Tuple<Pair<Args1, Args2>...> type; // Pair<Args1, Args2>... 是包展开 // Pair<Args1, Args2> 是模式 }; }; int main() { typedef zip<short, int>::with<unsigned short, unsigned>::type T1; // Pair<Args1, Args2>... 会展开成 // Pair<short, unsigned short>, Pair<int, unsigned int> // T1 是 Tuple<Pair<short, unsigned short>, Pair<int, unsigned>> // typedef zip<short>::with<unsigned short, unsigned>::type T2; // 错误:包展开中的形参包包含不同长度 }
使用包展开技术操作变长模板参数的基本应用:
#include <iostream>
void fun()
{
}
template <typename U, typename... T>
void fun(U u, T... args)
{
std::cout << u << std::endl;
fun(args...);
}
int main()
{
fun(1, 2, "hello", "world");
}
运行结果:
1
2
hello
world
折叠表达式(C++17):简化变长模板参数操作
详细内容可参考:https://zh.cppreference.com/w/cpp/language/fold
-
基于逗号的折叠表达式应用
示例:对上述代码进行改写
#include <iostream> void fun() { } template <typename... T> void fun(T... args) { ((std::cout << args << std::endl), ...); } int main() { fun(1, 2, "hello", "world"); }
-
折叠表达式用于表达式求值,无法处理输入(输出)是类型与模板的情形
4.完美转发与lambda表达式模板
完美转发(C++11): std::forward
函数
完美转发允许模板函数在转发参数时保留参数的值类别(左值或右值)。这是通过 std::forward
函数和万能引用(也称为转发引用)实现的。
-
万能引用
万能引用是一个使用双
&&
声明的引用类型。它可以接受左值、右值,或者通过模板参数推导为T&
或T&&
。template <typename T> void func(T&& arg) { // arg 是一个万能引用,可以绑定到左值或右值 }
-
std::forward
函数std::forward
是一个模板函数,用于实现完美转发。它的作用是:- 当模板参数
U
与T
相同的时候,std::forward<T>(arg)
将arg
视为T
类型的左值引用或右值引用,这取决于arg
在声明时的类型。 - 当
U
与T
不同的时候,std::forward<T>(arg)
将arg
视为T
类型的右值引用。
template <typename T> void func(T&& arg) { // 使用 std::forward 实现完美转发 someOtherFunction(std::forward<T>(arg)); }
- 当模板参数
-
完美转发的使用场景
完美转发通常用于模板函数或模板类中,特别是那些需要转发其参数给其他函数或构造函数的模板。这样可以保证:
- 如果原始参数是一个左值,它在转发后仍然是左值。
- 如果原始参数是一个右值,它在转发后仍然是右值。
示例:
#include <iostream>
void process(int& i) {
std::cout << "process(int&)" << std::endl;
}
void process(int&& i) {
std::cout << "process(int&&)" << std::endl;
}
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main()
{
int x = 3;
wrapper(x);
wrapper(3);
}
编译器翻译结果:
运行结果:
process(int&)
process(int&&)
在这个示例中,wrapper
函数接受一个万能引用参数。根据传入 wrapper
的参数是左值还是右值,std::forward
将正确地将参数转发给 process
函数,保持其值类别。
lambda表达式模板(C++20):
详细内容可参考:https://zh.cppreference.com/w/cpp/language/lambda
5.消除歧义与变量模板
使用 typename 与 template 消除歧义:
-
使用 typename 表示一个依赖名称是类型而非静态数据成员
当你在类型上下文中使用依赖名称,并且该名称表示一个类型时,你可以使用
typename
来消除歧义。这通常发生在通过模板参数访问嵌套类型时。template <typename T> class Outer { public: template <typename U> class Inner { }; // 使用 typename 来消除歧义,表示 Inner 是一个类型 typedef typename Outer<T>::Inner<int> TypedInner; };
在这个例子中,
Outer<T>::Inner<int>
是一个依赖名称,它依赖于模板参数T
。使用typename
告诉编译器Inner<int>
是一个类型 -
使用 template 表示一个依赖名称是模板
当你需要指定一个依赖名称是模板时,可以使用
template
关键字。template <typename T> class MyClass { public: template <typename U> void function() { // 使用 template 来消除歧义,表示 function 是一个模板 MyClass<T>::template function<U>(); } };
在这个例子中,
MyClass<T>::function<U>()
是一个依赖名称,它表示一个模板。使用template
告诉编译器function
是一个模板,而不是一个静态成员或类型。 -
template 与成员函数模板调用
成员函数模板是类模板内部定义的模板。当你在类模板外部实例化一个成员函数模板时,你需要使用
template
来指定模板实例化。template <typename T> class MyClass { public: template <typename U> void memberFunction(U param) { // ... } }; int main() { MyClass<int> myObject; // 调用成员函数模板,需要使用 template 来指定模板参数 myObject.template memberFunction<double>(3.14); }
编译器翻译成
template<typename T> class MyClass { public: template<typename U> inline void memberFunction(U param) { } }; /* First instantiated from: insights.cpp:11 */ #ifdef INSIGHTS_USE_TEMPLATE template<> class MyClass<int> { public: template<typename U> inline void memberFunction(U param); /* First instantiated from: insights.cpp:13 */ #ifdef INSIGHTS_USE_TEMPLATE template<> inline void memberFunction<double>(double param) { } #endif // inline constexpr MyClass() noexcept = default; }; #endif int main() { MyClass<int> myObject; myObject.memberFunction<double>(3.1400000000000001); return 0; }
在这个例子中,
memberFunction
是MyClass
的一个成员函数模板。在main
函数中,我们使用template
关键字来指定memberFunction
的模板参数double
。
变量模板(C++14):
C++14 引入了变量模板,这是一种新的模板类型,允许模板定义变量。
-
基本形式的变量模板
template <typename T> T pi = T(3.1415926);
pi
是一个变量模板,它对于每个类型T
都有一个与之对应的实例。注意,这里使用类型转换T(3.1415926)
来确保值3.1415926
根据模板参数T
的类型进行适当的转换。 -
使用变量模板
只需要指定所需的类型
double piDouble = pi<double>; // 使用 double 类型的 pi int piInt = pi<int>; // 使用 int 类型的 pi
-
其他形式的变量模板
-
编译时常量
-
类型属性
-
内联变量模板
-