C++元编程教程00:模板基础回顾
C++模版基础回顾
前言:为什么我们需要模版?
在使用编程语言进行程序设计的时候,有这样一个概念会反复的出现,这就是抽象(abstraction)。以一个简单的返回两数最大值的函数max
为例:
1 | auto max(int lhs, int rhs) -> int { |
这个函数可以看作对一类实际的表达式2 > 1 ? 2 : 1
进行了抽象,将1、2这样实际的数值抽象为了函数形参lhs
、rhs
,从而将代码逻辑从具体的表达式值中解耦。抽象的强大之处在于它能极大地减少重复代码,如果一段代码逻辑在我们所写的代码中反复的出现,将其抽象为一个函数可以有效的减少我们的工作量。
在使用max
函数时,我们可能会发现我们也需要对浮点数求最大值,这时候我们可能会想到通过重载函数的方式添加支持:
1 | auto max(float lhs, float rhs) -> float { |
添加一两个重载的工作量尚可接受,但是如果我们还想支持更多的数据类型,甚至是不同的数据类型之间的混合比较的时候,那么需要编写的代码很快就会增长到无法接受的程度。为了避免代码重复,我们就需要继续使用抽象,类似于前文将逻辑与具体的表达式值解耦类似,我们进一步将逻辑与具体的类型解耦。为此,我们需要使用模板(template):
1 | template <typename T> |
与函数参数类似,我们通过让类型也成为可变的形参来完成抽象。每当需要在某个类型的值上使用max
函数(严谨来说,现在应该叫做函数模板)时,我们除了需要传入lhs
、rhs
之外,还需要传入它们的类型:
1 | auto main() -> int { |
这个简单的max
函数,精准地反映了泛型编程(Generic Programming)的核心思想:不为每种类型重复编写逻辑相同的代码,而是将类型本身参数化,让编译器在编译期自动生成不同类型的版本。这种思想,不仅直接催生了C++标准容器库的产生,更成为现代C++的基石之一。
函数模板
基本语法
函数模板(Function template)的一般定义方式为:
1 | template <模板形参列表> |
以前文中的max
函数模板为例
1 | template <typename T> |
可以看到,max
具有一个模板形参typename T
,typename
表示T
是一个类型形参,所以当在调用max
的时候,T
只能是一个类型(如int
)而不能是一个值(如42
)。我们也可以使用class T
来声明类型形参,这种写法完全等价于typename T
,但笔者认为使用typename T
更加清晰(使用class
容易让人认为T
只能说一个类,但实际上T
能是任意类型)。
函数模板实例化
函数模板并不是实际的函数,单纯的定义一个函数模板并不会在编译后生成任何代码,为了使用函数模板,我们需要实例化(instantiate)模板:
1 | template <typename T> |
Compiler Explorer链接
我们可以将实例化看作编译器自动地将函数定义中对模板形参的使用全部替换为模板实参的过程,上面的代码在编译时,编译器会实例化产生两个max
函数,我们将这些生成的函数成为函数模板的实例:
1 | auto max(int lhs, int rhs) -> int { |
注意:
- 同一个函数模板实例化出的不同实例间没有任何关系,如同两个相互独立的函数。
- 如果一个函数模板已经对某些模板实参完成实例化,那么再次用这些模板实参来调用函数模板将不会再次实例化,而是使用之前已经产生的实例。(注意在上面的例子中,我们在
T=double
的情况下使用了两次max
,而实际只实例化生成了一个函数)
函数模板的实例化可以分为显式实例化(Explicit Template Instantiation)和隐式实例化(Implicit Template Instantiation)两种,继续以max
模板为例:
1 | template <typename T> |
与函数形参类似,我们也可以为模板形参指定默认值:
1 | template <typename T = int> |
非类型模板形参
既然我们有只接受类型的类型模板形参,自然也有非类型模板形参(NTTP,Nontype Template Parameter),NTTP接受一个对象的值:
1 | template <int N> |
NTTP的类型可以不是具体类型:
1 | template <auto N> |
为什么在上面的例子中,我们不能使用std::string
来作为非类型模板实参呢?这是由于C++为了保证相同的模板实参列表所对应的模板实例是唯一的,换而言之就是需要判断模板实参之间的相等性。对于类型模板实参而言,其相等性判断非常容易实现(即判断两个类型是否为同一类型)。但是对于非类型模板实参,判断其相等性可能会较为复杂,同样以std::string
为例,假设我们有两个std::string
对象:
1 | std::string str1{"awa"}; |
在忽略短字符串优化(SSO,Short String Optimization)和字符串常量池优化的情况下,这两个std::string
对象的内部指针会指向两个不同的内存地址,但是两个字符串的存储内容是相同的,这就会造成相等性判断时的歧义:单从类型的定义上看,编译器没法知道你需要比较两个std::string
的存储内容,只能从最简单的比较相应成员变量的方式来判断相等性,然而这会导致编译器认为这两个对象是不相等的,从而导致以下两行代码(假设它们能通过编译):
1 | print<str1>(); |
所指的实际上是不同的模板实例,这显然与我们的预期不一致。为了解决这个问题,C++规定用于NTTP的对象的类型必须是结构化类型(Structural Type),结构化类型的定义较为复杂,此处不展开讲解,详细定义参见CppReference上相关页面。
至于NTTP的进一步的引用场景,我们会在后续章节详细讲解。
简写函数模板
对于一些简单的函数模板,如上文例子中所用的max
,C++20标准引入了一种更为简洁的写法,称为简写函数模板(Abbreviated Function Template):
1 | auto max(auto lhs, auto rhs) { |
这种写法相当于:
1 | template <typename T1, typename T2> |
简写函数模板这个特性非常直观且容易理解,故此处不再展开。
转发引用与引用折叠
在使用模板时,我们经常会想要保留原实参的表达式的值类别,通俗得讲就是左值和右值(关于什么是左值和右值不在本教程的范围之内,如果你不了解,推荐阅读HackingCpp上关于移动语义的这一章),这个时候就要用到转发引用(Forwarding Reference):
1 | template <typename T> |
引用折叠规则(Reference Collapsing Rule)可以用一句话来总结:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
1 | using LRef = int&; |
如果我们想将转发引用传递给另一个函数,并让转发引用继续保持其值类别,就需要使用标准库中的转发函数std::forward
:
1 | template <typename T> |
模板形参包
在实际场景中,我们常常会遇到需要编写变长参数的函数的情景,我们可以通过C++模板来实现这种函数。以一个能够接收任意多参数并计算所有参数的和并返回的函数sum
为例:
1 | int sum_result = sum(1, 4, 6, 8); |
为了实现这样的函数,我们需要用到模板形参包(Template Parameter Pack):
1 | template <typename ... ArgTypes> |
其中,typename ... ArgTypes
声明了一个类型模板形参包(Type Template Parameter Pack,我不说你应该也能猜到还有非类型模板形参包),并通过这个模板形参包声明了一个函数形参包(Function Parameter Pack)ArgTypes ... args
。ArgTypes
包含了所有函数形参的类型,而args
则包含了函数所有的参数。这些包能够接受任意数量的实参,比如:
1 | auto main() -> int { |
Compiler Explorer链接
以上几行代码会导致以下实例的产生:
1 | template |
除了声明形参包之外,sum
函数还用到了两个特性:包展开(Pack Expansion)和折叠表达式(Fold Expression),我们逐一讲解这些特性。
包展开
想要使用一个形参包里的内容,就需要用到包展开。所谓包展开,就是将包在代码中按照特定的模式展开成为一个列表,以sum
函数的返回类型为例:
1 | std::common_type_t<ArgTypes...> |
其中,std::common_type_t
是C++标准元编程库中的一个类型特征(Type Trait),它的作用就是找出它的模板实参的公共类型,也就是所有模板实参都能显式转换到的类型。这个类型特征的原理我们会在后面的章节讲到。而ArgTypes...
就是我们所说的包展开。假设ArgTypes
接受了三个类型:int, float, unsigned long long
,那么包展开的结果为
1 | std::common_type_t<int, float, unsigned long long> |
我们将ArgTypes...
中的ArgTypes
称为包展开的模式(Pattern),包展开会将包中的每个元素按照我们所指定的模式展开成为一个逗号分隔的列表,比如:
1 | template <typename ... ArgTypes>//模板形参包展开为bool, float, char const* |
一般而言,常见的包展开方式有:
- 递归展开:通过递归模板实例化逐个处理参数
- 逗号运算符展开:结合逗号运算符生成初始化列表
- 初始化列表展开:利用
{}
初始化语法展开
通过这些包展开形式,我们就可以写出一些有用的函数。
示例1:递归展开求和:
1 | //递归模板实例化终止条件:只有一个实参 |
Compiler Explorer链接
示例2:使用逗号运算符展开输出所有实参
1 | template <typename ... ArgTypes> |
Compiler Explorer链接
对于这个例子,没有接触过形参包的人总是会写出这样的包展开:
1 | template <typename ... ArgTypes> |
但实际上这是不行的,这是因为包展开需要在一定的包展开场所中进行,包展开场所的完整列表相当长,故此处不做展开,如有需要可以查看CppReference相关页面。一般而言,常用的包展开场所有函数或模板的实参列表,及大括号/圆括号包围的初始化器。
示例3:多个包的同时展开
我们可以在一个包展开中同时展开多个包,当然,这些包的长度必须一致,我们可以使用sizeof...(pack)
来获取包的长度(这个例子里使用了模板类,但是不是很深入,如果你看不懂可以先行阅读一下下一节[[#类模板]]的基础部分):
1 | template <typename ... Args1> |
示例4:嵌套包展开
若包展开内嵌于另一个包展开中,那么包展开按从里到外的顺序依次进行:
1 | template <typename ... ArgTypes> |
折叠表达式
通过刚才的例子,不难发现包展开的语法较为复杂,且在处理计算的时候(如上文中的sum
函数)时较为复杂。C++17标准中所引入的折叠表达式,可以让我们通过简洁的方式来完成这些包展开。在函数模板
1 | template <typename ... ArgTypes> |
中,(args + ...)
就是一个折叠表达式。折叠表达式的语法为:
1 | (形参包 运算符 ...) //一元右折叠 |
注意折叠表达式的括号是不可省略的。
一个折叠表达式是左折叠还是右折叠,取决于...
出现在形参包的左侧还是右侧。左折叠和右折叠的区别是展开后的括号结合性:
- 一元右折叠:
(E op ...)
展开为(E1 op (E2 op (... op (EN-1 op EN))))
- 一元左折叠:
(... op E)
展开为((((E1 op E2) op ...) op EN-1) op EN)
- 二元右折叠:
(E op ... op init)
展开为(E1 op (E2 op (... op (EN-1 op (EN op init)))))
- 二元左折叠:
(init op ... op E)
展开为(((((init op E1) op E2) op ...) op EN-1) op EN)
以sum
函数模板为例:
1 | auto main() -> int { |
我们也可以使用其他运算符,比如逻辑与&&
:
1 | template <typename ... ArgTypes> |
注意,左折叠和右折叠会影响计算顺序,因此如果运算符不满足结合性,那么左折叠和右折叠的计算结果也是不同的:
1 | template <typename ... ArgTypes> |
Compiler Explorer链接
将折叠表达式与逗号运算符相结合,我们能够更加优雅的写出很多东西,比如printAll
函数模板可以简化成:
1 | template <typename ... ArgTypes> |
包索引
在C++26之前,我们并没有一个直接访问包中第n个元素的方式,唯一使用形参包的方式就是将其展开。为了访问第n个元素,我们需要使用一个递归模板:
1 | template<size_t N, typename... Ts> |
为了实现GetNthType
,此处使用了模板特化,模板特化会在[[#模板特化与部分特化]]一节中详细讲解。
这种方法不仅效率低下(尝试思考一下获取第n个元素需要实例化多少个模板),而且不够通用(显然GetNthType
只能处理类型形参包)。为了解决这个问题,C++26标准引入了包索引(Pack Indexing),包索引的语法如下:
1 | 形参包...[索引] |
这一特性大大简化了跟包相关的模板代码。将包索引和折叠表达式结合在一起,我们能够写出非常简洁优雅的代码:
1 | template <std::size_t ... Indexes, typename ... ArgTypes> |
类模板
与函数模板相类似,我们也可以声明类模板(Class Template),类模板的语法与函数模板非常类似:
1 | template <模板形参列表> |
类模板的一大使用案例是实现容器,标准库当中常用的std::vector
、std::map
等容器都是类模板。在此我们通过实现一个简单的固定容量容器FixedArray
来演示类模板的使用:
1 | template <typename ElementType> |
与函数模板类似,类模板并不是实际存在的类,在不进行实例化的情况下编译器不会为类模板生成任何代码。要使用类模板,首先要做的就是实例化:
1 | template <typename ElementType> |
Compiler Explorer链接
类模板的显式实例化较函数模板更加复杂,我们可以一次性显式实例化整个类模板,也可以单独实例化类中的成员(包括成员函数、成员类、静态成员变量):
1 | template <typename T> |
类模板的隐式实例化与函数模板类似,当我们需要使用一个被完整定义的类模板且该类模板没有对应显式实例化的时候,隐式实例化就会发生:
1 | template <typename ElementType> |
在上面这个例子中,最后一处使用FixedArray<char>*
指针的例子尤其值得注意。由于使用一个类型的指针类型并不需要该类型有定义,故此行代码不会触发隐式实例化。
这是因为C++作为静态类型语言,需要在定义一个类型的变量之前知道该类型的大小,而指针类型的大小仅由平台决定,与指针所指向的类型大小无关,故不需要指针指向的类型有完整定义。
类模板实参推导
类模板实参推导(CTAD,Class Template Argument Deduction)可以通过初始化表达式的类型来自动推导模板实参,CTAD的推导规则相当复杂,此处不进行展开说明,如有需要可以参考CppReference上的CTAD一节。
对于简单的类模板,CTAD可以很方便的自动推导出对应的模板实参,但是如果类较为复杂或者有特殊处理需求,导致CTAD不起作用时,可以通过定义推导指引(Deduction Guides)来改变CTAD的行为。推导指引的语法如下:
1 | template <模板形参列表> |
为了演示推导指引的作用,我们通过向FixedArray
加入一个int
类型的非类型模板形参来使其支持存储指定长度的数组:
1 | template <typename ElementType, int Length> |
很自然的,我们期望FixedArray
能够通过初始化列表来自动推导Length
:
1 | //这样写既简洁又不容易出错,但很可惜CTAD不支持这种情况下的模板实参推导 |
为了让CTAD能自动完成类似推导,我们需要定义以下推导指引:
1 | template <typename ElementType, typename... TailArgTypes> |
推导规则的工作原理是定义一个假想的函数模板,编译器会将无法默认规则无法推导的初始化表达式作为该函数模板的实参,再通过函数模板的推导规则来获取我们定义的类模板实参推导结果。就拿FixedArray
作为例子,当编译器遇到一个无法被默认推导规则推导的规则时,如:
1 | FixedArray array{1, 2, 3, 4}; |
编译器会尝试查找FixedArray
的推导规则,在这个例子中,只有上文定义的一条推导规则。在编译器查找到之后,它会尝试“想象”一个函数模板调用:
1 | template <typename ElementType, typename... TailArgTypes> |
然后编译器就可以从这一调用推导出ElementType=int, TailArgTypes=<int, int, int>
,从而成功推导出类模板的实参。
当然,这一推导规则可以用我们前文所讲的[[#包索引]]简化:
1 | template <typename ... ArgTypes> |
模板模板形参
我们现在有了可以接受类型的类型模板形参,有了可以接受对象的非类型模板实参(即NTTP),还有什么是模板形参接受不了的呢?就是模板。为了解决这一问题,C++引入了模板模板形参(Template Template Parameter,很绕口就对了),模板模板形参的声明语法如下:
1 | template <template <模板形参列表> 模板类型 标识符> |
其中,模板类型可以为class
、typename
、concept
(C++26加入)、auto
(C++26加入)。我们此处只讲解typename
一种。
1 | template <typename ElementT> |
其中,template <typename> typename ContainerT
即为一个模板模板形参。ContainerT
接受一个模板类作为实参(typename ContainerT
说明ContainerT
是一个类型),且该模板类拥有一个类型模板形参(template <typename>
说明Container
是一个模板,且模板形参列表为<typename>
)。我们可以将符合这一要求的类模板(比如MyArray
)赋给ContainerT
。
成员函数模板
成员函数模板与类模板的相关性不大,类模板和普通的类都可以有成员函数模板,类模板的模板形参在其成员函数模板中都可用:
1 | class NonTemplateClass { |
注意,一个类的析构函数不能是函数模板。
成员模板函数与一般的模板函数没有太大差别,故此处不深入讲解。
类模板与形参包
与函数模板一样,形参包在类模板中亦有广泛的使用,其中最为知名的莫过于std::tuple
。通过使用形参包,std::tuple
有了在一个对象中存储不同类型对象的能力。
我们用一段简单的例子来演示形参包在类模板中的使用:
1 | template <typename ... ElementT> |
别名模板与变量模板
除了函数和类之外,类型别名与变量都可以成为模板,分别称作别名模板(Alias Template)和变量模板(Variable Template)。这两类模板的作用会在后面几篇教程详细讲解,在此处我们仅做简单演示:
1 | //别名模板 |
待决名
typename
消岐义符
我们使用一段代码来引入待决名(Dependent Name)这一概念:
1 | template <typename T> |
Compiler Explorer链接
上面这段代码在经过clang编译后会产生以下错误:
1 | <source>:5:2: error: missing 'typename' prior to dependent type name 'std::vector<T>::const_iterator' |
报错的原因也很简单:由于std::vector<T>
是一个依赖于模板形参T
的类型,所以编译器在实例化之前并不知道std::vector<T>
里面有什么东西。在编译器的视角中,std::vector<T>::const_iterator
可以是:
- 一个静态成员变量
- 一个成员类型别名
- 一个静态成员函数
- …
显然,编译器并不知道应该选择哪一种来解释const_iterator
,所以产生了这样的报错,我们称std::vector<T>::const_iterator
是一个待决名。为了避免报错,我们应该显式地告诉编译器(即“消岐义”,Disambiguate),const_iterator
是一个成员类型别名:
1 | template <typename T> |
那么,我们应该在何时使用typename
进行消岐义呢?
在模板(包括别名模版)的声明或定义中,不是当前实例化的成员且取决于某个模板形参的名字不会被认为是类型,除非使用关键词
typename
或它已经被设立为类型名(例如用typedef
声明或通过用作基类名)。
将这句话拆分成几个部分:
- 在模板的声明或定义中
- 不是当前实例化的成员且取决于某个模板形参的名字
- 除非使用关键词
typename
或它已经被设立为类型名
对应到上面的例子中: - 我们在一个函数模板
getIterator
的定义中,符合 std::vector<T>::const_iterator
不是当前实例化的成员,同时取决于我们的模板形参T
,符合
所以编译器不会认为std::vector<T>::const_iterator
是一个类型名,所以我们需要使用typename
来进行消岐义。
当然,如果名字并非待决,我们也可以在前面使用typename
。通俗来讲就是如果编译器已经知道某个标识符是个类型,你也可以继续跟编译器重复说这是个类型(当然不少编译器都会给你一个warning说这不是必要的)。
template
消岐义符
我们同样以一个例子做引入:
1 | template <typename T> |
Compiler Explorer链接
这段代码同样会发生编译错误,clang编译器的报错如下:
1 | <source>:10:11: error: use 'template' keyword to treat 'foo' as a dependent template name |
与typename
类似,编译器不认为foo_inst.foo<T>
是一个模板,所以相应的,我们需要加上template
消岐义符:
1 | template <typename T> |
使用template
消岐义的规则与typename
类似:
模板定义中不是当前实例化的成员的待决名同样不被认为是模板名,除非使用消歧义关键词
template
,或它已被设立为模板名。
对应上面的例子:
- 我们在
bar
模板函数的定义中 - 且
Foo<T>
不是当前实例化的成员
故foo_inst.foo<T>
亦是待决名,故编译器不认为它是一个模板。
同样的,template
消岐义符也可以用在不需要消岐义的标识符前面。
绑定规则
模板中标识符的绑定规则可以根据该标识符是否待决分为两类:
- 非待决名在模板定义点查找并绑定。即使在模板实例化点有更好的匹配,也保持此绑定。
- 待决名的绑定则延迟到查找发生时,即实例化时。
我们用一段代码来理解以上规则:
1 | auto foo(double dummy) -> void { |
Compiler Explorer链接
运行这段代码,输出结果为:
1 | foo(int) called |
为什么会出现这个结果呢?main
函数中对foo(42)
函数的调用很直观的绑定到了foo(int)
,但是在Bar<T>::meow
中的foo(42)
是一个非待决名,非待决名在模板的定义点查找并绑定,而非在实例化点绑定。所以当编译器编译到了meow
函数时,它会立刻开始查找目前已经定义的foo
函数,而由于此时编译器还没有看到后面的foo(int)
,所以此处只能绑定到已知的foo(double)
。但如果在meow
中foo
是一个待决名,那么绑定会在实例化的时候发生。
查找规则
1 | auto bar() -> void { std::println("::bar() called"); } |
Compiler Explorer链接
运行上面的代码,输出结果为:
1 | Base::bar() called |
首先我们明确在dependent
和independent
函数中,由于我们没有使用任何的作用域解析操作符::
,对bar
进行的查找都是无限定名字查找(UDL,Unqualified Name Lookup)。同时,在dependent
中,bar
是一个依赖于Base<T>
的待决名,而在independent
中,bar
是一个非待决名。非待决名和待决名的查找规则有所不同,高度概括地说:
- 非待决名在模版定义的时候就进行无限定名字查找。
- 待决名的名字查找会推迟到模板实例化时。
这称作二阶段名字查找(Two-phase Name Lookup)
所以对于independent
中对bar
的调用,编译器在该类模板定义时就会进行查找。按照UDL的查找顺序,编译器首先会在本类中查找(注意不会查找父类Base<T>
),在本类中查找不到bar
后编译器会进一步查找全局命名空间,随后就会查找到在全局命名空间中的bar
。
而对于dependent
中对bar
的调用,编译器会在实例化,即main
函数中Foo<void>
一行时进行查找,由于所有模板形参都已确定,编译器可以对父类进行查找,所以会查找到父类中对bar
。
模板特化与部分特化
模板全特化
模板特化(Template Specialization),说的直白点就是让模板能够对某些模板实参进行特殊处理,以前文中的FixedArray
为例(为了简洁我们将其长度固定为8):
1 | template <typename ElementT> |
我们都知道在C++中,一个bool
需要占用完整的一个字节,所以FixedArray<bool>
就会占用8个字节。但是bool
的信息存储只需要用到一个字节中的一位,所以我们想通过某种方式来优化FixedArray<bool>
,来使其只占用一个字节,这时候我们就要用到模板全特化(又称模板显式特化,Explicit (Full) Template Specialization):
1 | //模板主定义 |
Compiler Explorer链接
可以看到,array2
的大小仅为一个字节,说明我们的模板特化起了作用。
全特化函数模板
对于函数模板的全特化来说,其语法如下:
1 | template <> |
如果模板实参能从函数参数列表中推导,那么我们可以省略模板实参:
1 | //模板主定义 |
Compiler Explorer链接
注意一点,所有的模板特化都应该位于在第一次会引起隐式实例化的使用前,比如下面的代码不能通过编译:
1 | //模板主定义 |
注意要分清楚全特化与显式实例化之间的差异,两者的语法很接近,但特化的函数模板与模板主定义之间没有关系,而显式实例化是基于模板主定义进行的实例化:
1 | //模板主定义 |
注意,特化的函数模板可以跟主模板具有不同的说明符(inline
/constexpr
/constinit
/conseval
):
1 | template <typename T> |
全特化类模板
类模板全特化的语法与函数模板类似:
1 | template <> |
一个简单的小例子(虽说这里value
应该是constexpr
变量,但是由于我们还没有讲到相关知识,故省略):
1 | //类模板主定义 |
Compiler Explorer链接
注意,类模板的特化相当于一个全新的,与原类模板主声明无关的类。我们可以随意向类模板的特化中添加成员:
1 | //类模板主定义 |
全特化类模板成员
在类体外定义显式特化的类模板的成员时,不需要使用template<>
前缀,除非它是某个被特化为类模板的显式特化的成员类模板的成员:
- 成员函数及成员函数模板:
1 | //类模板主定义 |
- 成员类及成员类模板:
1 | template <typename T> |
我们还可以通过对类模板的隐式实例化进行特化来单独特化类模板中的部分成员,此时我们才需要用到template <>
前缀:
1 | template <typename T> |
Compiler Explorer链接
模板特化也可以进行嵌套,比如特化一个类模板中的特定成员函数模板:
1 | template <typename T> |
全特化变量模板
全特化变量模板会在后续章节中深入讲解,此处仅提供一个简单的例子说明:
1 | //变量模板主定义 |
模板部分特化
模板全特化,是提供一个定义给一组具体的模板实参。而所谓模板部分特化(又译模板偏特化,Partial Template Specialization)是将一个定义提供给一组具有某些特征的模板实参。我们以一个例子做引入:
1 | template <typename T> |
Compiler Explorer链接
在有多个模板形参时,完全指定一部分模板实参也是偏特化的一种:
1 | //类模板主定义 |
Compiler Explorer链接
注意,与模板全特化不同,模板偏特化只能特化类模板与变量模板。
模板偏特化这一特性十分强大,这相当于允许我们对模板实参进行模式匹配,并根据不同的匹配结果进行不同的处理,这一特性直接构成了我们后面要讲的模板元编程的核心工具之一。
模板偏特化的语法与全特化大致相同,故此处不再赘述。
- Title: C++元编程教程00:模板基础回顾
- Author: Zhige Chen
- Created at : 2025-04-09 23:13:53
- Updated at : 2025-04-09 21:07:41
- Link: https://nofe1248.github.io/2025/04/09/cpp-metaprogramming-00/
- License: This work is licensed under CC BY-NC-SA 4.0.