目录
- 10.1 函数模板的引入
- 10.2 调用模板函数
- 10.2.1 显式实例化
- 10.2.2 隐式实例化
- 10.3 模板函数应用实例
- 10.4 C++ concept(C++20)
- 10.4.1 一个错误
- 10.4.2 创建
- 10.4.3 使用
- 10.4.4 实例
- 10.5 可变参数模板
- 10.5.1 实现
- 10.5.2 编译器运行可变参数模板
- 10.5.3 可变模板参数类型
- 10.6 模板元编程
- 10.6.1 例子:阶乘与斐波那契
- 10.6.2 Boost MPL库
- 10.6.3 模板元编程是图灵完备的
- 10.6.4 模板元编程的规范化实现
- 10.7 何时用模板?
10.1 函数模板的引入
编写min函数
min(106,107);// int, returns 106min(1.2,3.4);// double, returns 1.2min("Jacob","Fabio");// string, returns "Fabio"方法(函数重载):
intmin(inta,intb){returna<b?a:b;}doublemin(doublea,doubleb){returna<b?a:b;}std::stringmin(std::stringa,std::stringb){returna<b?a:b;}我们可以用模板(templates)!
template<typenameT>Tmin(T a,T b){returna<b?a:b;}// 用引用避免拷贝template<typenameT>Tmin(constT&a,constT&b){returna<b?a:b;}函数模板VS函数
template<typenameT>Tmin(T a,T b)// 这是函数模板不是函数min<std::string>// 这是一个函数,是模板实例化10.2 调用模板函数
10.2.1 显式实例化
函数模板会自动生成代码
intmin(inta,intb){// Compiler generatedreturna<b?a:b;// Compiler generated}// Compiler generateddoublemin(doublea,doubleb){// Compiler generatedreturna<b?a:b;// Compiler generated}min<int>(106,107);// Returns 106min<double>(1.2,3.4);// Returns 1.210.2.2 隐式实例化
让编译器为我们自动推断类型,有点像auto
min(106,107);// int, returns 106min(1.2,3.4);// double, returns 1.2隐式实例化有可能难以处理:
1、不好推断具体类型
2、两个参数类型不严格匹配会编译报错
一种解决方案:
template<typenameT,typenameU>// 解决:两个独立模板参数auto min(constT&a,constU&b){// 解决:auto推导返回值returna<b?a:b;}min(106,3.14);// 调用成功:T=int,U=double,返回值为double双模板参数 + auto 返回值(C++14+ 支持)
1、解决两个参数类型不严格匹配
模板参数从 typename T 改为 typename T, typename U,意味着:
• 第一个参数的类型由 T 推导(实参 106 → T=int);
• 第二个参数的类型由 U 推导(实参 3.14 → U=double);
• 两个参数类型不再强制统一,完美支持不同类型的输入(如 int+double、float+long 等)。
2、解决“不好推断具体类型”
返回值用 auto 关键字,让编译器根据「三元运算符的结果类型」自动推导返回值:
• 三元运算符 a < b ? a : b 中,a 是 int,b 是 double;
• C++ 会遵循「usual arithmetic conversions」(常用算术转换),将 int 隐式转换为 double,最终返回 double 类型;
• 无需手动指定返回值类型(如 decltype(a < b ? a : b)),auto 直接搞定,避免了 “返回值类型与参数类型不匹配” 的问题。
10.3 模板函数应用实例
迭代器中
10.4 C++ concept(C++20)
10.4.1 一个错误
structStanfordID;// How do we compare two IDs?StanfordID jacob{"Jacob","jtrb"};StanfordID fabio{"Fabio","fabioi"};min<StanfordID>(jacob,fabio);// ❌ Compiler error$ g++ main.cpp --std=c++20 main.cpp:9:12: error: invalid operands to binary expression('const StanfordID'and'const StanfordID')returna<b ? a:b;~ ^ ~ main.cpp:20:3: note:ininstantiation offunctiontemplate specialization'min<StanfordID>'requested here min<StanfordID>(jacob, fabio);^1error generated.编译器实例化我们的模板,只有在执行时才抛出错误。
编译器仅在实例化后才会发现错误。
如何给模板施加类型约束呢?
10.4.2 创建
创建一个Comparable concept
使用concept的两个原因:
- 更好的编译器错误消息
- 更好的 IDE 支持(智能感知 / 自动补全等)
概念仍是一项新功能,STL还未支持它们。
(截至2025春)
使用Comparable concept
template<typenameT>requires Comparable<T>Tmin(constT&a,constT&b);// Super slick shorthand for the abovetemplate<Comparable T>Tmin(constT&a,constT&b);10.4.3 使用
使用概念(concept)的前后报错差异
10.4.4 实例
Concepts 库
Iterator concepts
10.5 可变参数模板
10.5.1 实现
可变参数模板通过模板+递归实现!
// 基本案例函数:需要停止递归template<Comparable T>Tmin(constT&v){returnv;}template<Comparable T,Comparable...Args>// 可变参数模板:匹配0个或多个类型Tmin(constT&v,constArgs&...args){// 参数打包:0个多多个参数autom=min(args...);// 打包展开:用实际参数替换args...returnv<m?v:m;}10.5.2 编译器运行可变参数模板
此时,编译器为我们自动生成了重载,形成了min(a0,a1,a2)另一个模板实例化。
继续递归生成! 结果生成了下列所有的函数:
10.5.3 可变模板参数类型
可变模板参数类型不必同构!
format("Queen {}, Protector of the {} Kingdoms", "Rhaenyra",7);
异构:
format("Queen {}, Protector of the {} Kingdoms","Rhaenyra",7);// Prints: Queen Rhaenyra, Protector of the 7 Kingdomsstd::cout<<std::boolalpha;format("The {} enemy won't {} out the {}",true,"wait","storm");// Prints: The true enemy won't wait out the stormformat("Winter is coming");// Prints: Winter is coming执行format:
voidformat(conststd::string&fmt){std::cout<<fmt<<std::endl;}template<typenameT,typename...Args>voidformat(conststd::string&fmt,T value,Args...args){autopos=fmt.find("{}");if(pos==std::string::npos)throwstd::runtime_error("Extra arg");std::cout<<fmt.substr(0,pos);std::cout<<value;format(fmt.substr(pos+2),args...);}在C++中,std::format是一个 variadic 模板函数,用于格式化字符串。当我们"实例化"(instantiate)format时,实际上是编译器根据提供的模板参数和函数参数生成具体的函数实例。
例子:
voidformat(conststd::string&fmt){std::cout<<fmt<<std::endl;}template<typenameT,typename...Args>voidformat(conststd::string&fmt,T value,Args...args){autopos=fmt.find("{}");if(pos==std::string::npos)throwstd::runtime_error("Extra arg");std::cout<<fmt.substr(0,pos);std::cout<<value;format(fmt.substr(pos+2),args...);}format("Lecture {}: {} (Week {})", 9, "Templates",5);- 这是最常见的用法,编译器会自动推导模板参数类型
- 从参数推断出类型:
int, const char*, int - 编译器生成匹配这些类型的
format函数实例
format<int, std::string, int>()- 显式指定模板参数:第一个类型为
int,后续参数类型为std::string和int - 这里
T = int,可变参数包Args = [std::string, int]
- 显式指定模板参数:第一个类型为
format<std::string, int>()- 显式指定模板参数:
T = std::string,Args = [int]
- 显式指定模板参数:
format<int>()- 只指定了一个模板参数:
T = int,没有其他参数(Args为空)
- 只指定了一个模板参数:
format()- 无模板参数的基础情况
- 这不是模板实例化,而是调用无参数版本的
format函数
简单来说,format模板函数的实例化过程就是编译器根据指定的类型参数或从函数参数推导出的类型,生成特定版本的format函数代码,以匹配具体的调用需求。这种模板机制让format能够处理各种不同类型和数量的参数。
总结:
编译器使用递归生成任意数量的重载,这使我们能够支持任意数量的函数参数。
实例化发生在编译时
10.6 模板元编程
10.6.1 例子:阶乘与斐波那契
这段代码展示了使用C++模板元编程(TMP)在编译期计算阶乘的方法,核心思想是利用模板特化和递归实现编译时计算:
基础情况:
template<>structFactorial<0>{enum{value=1};// 0的阶乘定义为1};这是模板的全特化版本,当模板参数N=0时使用,直接定义阶乘结果为1,终止递归。
递归模板:
template<size_t N>structFactorial{enum{value=N*Factorial<N-1>::value};};通用模板通过递归方式计算:N的阶乘 = N × (N-1)的阶乘,编译器会在编译时展开递归,直到触发N=0的特化版本。
编译期计算特性:
使用enum是为了在编译期存储常量值。当调用Factorial<7>::value时,编译器会在编译阶段就完成7! = 5040的计算,运行时直接使用结果,这比运行时计算更高效。
这种技术展示了模板元编程的核心能力:将计算从运行时转移到编译时,实现"编译期执行代码"的效果。
这段代码展示了编译期计算阶乘的汇编输出结果,核心特点是结果在编译时就已计算完成并直接嵌入到可执行文件中。
Factorial<7>::value是一个模板元编程实现,编译器在编译阶段就会计算出 7! = 5040- 汇编代码中直接出现了
5040这个值,说明没有运行时计算过程 - 程序只是简单地将预计算好的 5040 通过
std::cout输出 - 这种方式比运行时计算更高效,因为结果已经 “烘焙”(baked in) 到可执行文件中
这体现了模板元编程的优势:将计算从运行时转移到编译时,提高程序执行效率。
另一个例子:
template<>structFibonacci<0>{enum{value=0};};template<>structFibonacci<1>{enum{value=1};};template<size_t N>structFibonacci{enum{value=Fibonacci<N-1>::value+Fibonacci<N-2>::value};};实际应用:
- 在编译时将结果嵌入可执行文件(例如阶乘)
- 优化矩阵、树和其他数学结构
- 基于策略的设计:通过模板传递行为
- Boost MPL 库
10.6.2 Boost MPL库
usingnamespaceboost;usingMove=mpl::vector<MoveUp,MoveRight>;usingMoveRotate=mpl::push_back<Move,Rotate45>::type;template<typenameTransformations>voidapply(Object&);apply<Move>(object);// move object up and rightapply<MoveRotate>(object);// move object up/right, rotate 45deg这段代码涉及C++元编程(Metaprogramming)的概念和Boost.MPL库的使用,核心解析如下:
- 元编程(TMP)特性:
- TMP(Template Metaprogramming)允许在编译期通过类型进行"编程",将类型作为数据来操作
- 编译器会为不同的类型组合生成特定代码,实现编译期多态和代码生成
- Boost MPL库:
- 是C++中最流行的元编程库(mpl即MetaProgramming Library)
- 提供了类似容器、算法等元编程工具,用于在编译期处理类型集合
- 代码解析:
using Move = mpl::vector<MoveUp, MoveRight>:定义了一个包含MoveUp和MoveRight两种类型的元容器using MoveRotate = mpl::push_back<Move, Rotate45>::type:通过元函数push_back,在Move基础上添加Rotate45类型,生成新的类型集合
- 编译期代码生成:
template <typename Transformations> void apply(Object&):模板函数根据传入的类型参数生成特定代码apply<Move>(object)会生成执行"上移+右移"的代码apply<MoveRotate>(object)会生成执行"上移+右移+45度旋转"的代码
这种方式的优势是将操作组合的决策移到编译期,减少运行时开销,同时通过类型系统保证操作的合法性。
10.6.3 模板元编程是图灵完备的
1. 先明确两个基础概念
- 图灵完备性:一个计算系统(或语言、工具)若能模拟“图灵机”的所有行为,即具备“条件分支”“循环(或递归)”“数据存储”这三大核心能力,就能计算任何图灵机可解的问题,称为“图灵完备”。简单说:能实现“根据条件做不同操作”“重复执行一段逻辑”“保存中间结果”,就满足图灵完备的核心要求。
TMP,即模板元编程:C++ 中的一种特殊编程范式——利用模板的编译期展开机制,让代码在“编译阶段”而非“运行阶段”执行计算。例如通过模板特化实现分支、通过递归模板实现循环,最终生成编译期确定的结果。
2. TMP 如何满足图灵完备的三大条件?
TMP 之所以是图灵完备的,关键在于它在编译期实现了图灵机的核心能力:
- 条件分支:通过模板特化(Template Specialization)实现。例如定义一个通用模板,再为特定条件(如“数值等于 0”)写特化版本,编译时会根据输入参数匹配对应的模板,本质就是“根据条件选择不同逻辑”。
// 通用模板(默认分支)template<intN>structIsZero{staticconstboolvalue=false;};// 特化模板(条件分支:N=0 时触发)template<>structIsZero<0>{staticconstboolvalue=true;}; - 循环/递归:通过递归模板(Recursive Templates)实现。利用模板参数的递推(如
N-1)重复展开模板,直到触发“终止条件的特化模板”,本质是“编译期的循环”。例如计算阶乘(编译期确定5! = 120):// 通用递归模板(循环体:N! = N * (N-1)!)template<intN>structFactorial{staticconstintvalue=N*Factorial<N-1>::value;};// 特化模板(终止条件:0! = 1)template<>structFactorial<0>{staticconstintvalue=1;};// 编译期计算:Factorial<5>::value 直接等于 120 - 数据存储:通过模板参数、静态常量、类型别名实现。模板参数(如上述
int N)可存储数值,静态常量(static const int value)可存储计算结果,类型别名(using)可存储“类型数据”——这些都是编译期的“数据载体”,对应图灵机的“纸带存储”。
3. 关键结论
TMP 虽依赖 C++ 模板的编译期机制,且执行阶段(编译期)与常规编程语言(运行期)不同,但它完全具备“条件分支、递归(循环)、数据存储”三大图灵完备核心能力,因此可以模拟任何图灵机可解的计算问题,即“TMP 是图灵完备的”。
(注:TMP 的“计算”局限于编译期,且受编译器模板递归深度、类型复杂度等限制,但这是工程实现约束,不影响其图灵完备性的理论属性。)
10.6.4 模板元编程的规范化实现
在C++20中,constexpr和consteval是用于编译期计算的关键字,它们将模板元编程的思想制度化,让编译期计算更直观:
constexpr(如第一个阶乘函数):告诉编译器"尽量在编译期运行我"。它既可以在编译期执行,也能在运行时调用,具有灵活性。示例中使用了C++17的constexpr if,进一步增强了编译期分支能力。consteval(如第二个阶乘函数):强制要求"必须在编译期运行我",被称为"立即函数"。它只能在编译期执行,确保计算结果在程序运行前就已确定,常用于需要严格编译期保证的场景。
两者都简化了编译期计算的实现,避免了传统模板元编程的晦涩语法,让开发者能更自然地编写可在编译阶段执行的代码(如阶乘计算)。
10.7 何时用模板?
我希望编译器能自动完成重复性的编码任务——模板函数template function、变长模板variadic templates
我想要更好的错误信息——概念concept
我不想等到运行时——模板元编程template metaprogramming、constexpr/consteval