资讯动态

函数模板与类模板:从最简写法到参数推导规则

发布时间:2026/10/15 3:48:22 来源:尧图企业网站定制
模板template是 C 泛型generics的基石写一份代码让编译器替你为不同类型各生成一份具体版本。很多人卡在「T 到底被推导成了什么」「类模板为什么不能像函数那样自动推导」。这篇把函数模板和类模板的推导规则一次讲透并配真跑出来的输出和一张实例化流程图。1. 引子为什么T有时不是你以为的类型下面这段看似简单的函数模板新手常踩坑templateclassTvoidf(T x);如果你传一个const intT会是const int吗答案是不会按值传参时引用和顶层const会被剥掉T只是int。这条规则和auto x ...;的推导完全一致呼应《auto 类型推导到底推成了什么》。先记住结论后面用实测验证。官方文档Function template — cppreference2. 函数模板实参怎么推导出 T按值接收T x时编译器从实参推导T并去掉顶层 const 与引用引用折叠前的「引用」本身是顶层属性会被去掉// tmpl_deduce.cpp — 编译: g -stdc17 -Wall -O2 tmpl_deduce.cpp -o td#includecstdio#includetype_traitstemplateclassTvoidinspect(T x){if(std::is_same_vT,int)std::puts(T int);elseif(std::is_same_vT,double)std::puts(T double);elsestd::puts(T 其它);}intmain(){inti7;constintci5;// 顶层 constdoubled3.5;inspect(i);// T intinspect(ci);// const 被剥掉 → T int不是 const intinspect(d);// T double}T int T int T doubleci是const int但inspect按值收参副本的const没有意义于是T推导成int而非const int。这和auto x ci;中x的类型是int是同一套规则。类型推导一致性本身就是 C 的设计基调。按值传参只是推导的一种模式。同样一个T换成template class T void g(T x)结果就完全不同这时改的是原对象而不是副本const必须保留所以传const int得到T const int。传参形式决定T长什么样。写模板函数的第一件事是问自己「我要的到底是副本还是原对象」。3. 显式指定模板参数 多个模板参数当参数类型无法从实参唯一确定、或你想强制转换时就在尖括号里显式写fint(x)。多个模板参数则各自推导或指定// tmpl_explicit.cpp — 编译: g -stdc17 -Wall -O2 tmpl_explicit.cpp -o te#includecstdiotemplateclassTTwiden(T a,T b){returnab;}templateclassA,classBvoidtwo_types(A,B){std::puts(A 和 B 是不同类型也能各自推导);}intmain(){// 两个 int 相加却想拿到 double显式指定 Tdouble实参被提升std::printf(widendouble(1, 2) %f\n,widendouble(1,2));// 多模板参数Aint, Bdouble 分别推导two_types(1,3.5);}widendouble(1, 2) 3.000000 A 和 B 是不同类型也能各自推导widendouble(1, 2)强制Tdouble于是两个int实参被提升成double再相加。显式指定的典型用途实参推导不出你想要的类型时由你接管。4. 类模板必须显式指定但 C17 有了 CTAD函数模板能从实参推导T类模板在 C17 之前不能你必须老老实实写std::vectorint。C17 起加入CTADClass Template Argument Deduction类模板参数推导让std::pair p{1, 2.0};这种写法能自动推断出std::pairint, double// tmpl_ctad.cpp — 编译: g -stdc17 -Wall -O2 tmpl_ctad.cpp -o tc#includecstdio#includearray#includeutilitytemplatestd::size_t Nconstexprstd::size_tarr_len(conststd::arrayint,N){returnN;}intmain(){std::arrayint,3a{1,2,3};// 传统显式写全std::printf(显式 N %zu\n,arr_len(a));std::array b{1,2,3};// C17 CTAD推导出 std::arrayint, 3std::printf(CTAD 元素数 %zu\n,b.size());std::pair p{1,2.0};// CTAD 推导出 std::pairint, doublestd::printf(pair 第二元素 %f\n,std::get1(p));}显式 N 3 CTAD 元素数 3 pair 第二元素 2.000000CTAD 很方便但有时推导出的不是你想要的比如std::array从花括号列表推导元素类型取「能容纳所有元素的公共类型」。如果你想要long却写std::array{1, 2}两个int拿到的是std::arrayint,2。需要精确控制时仍要显式写明。CTAD 是便利不是万能。官方文档Class template argument deduction — cppreference5. 非类型模板参数编译期常量模板参数不只有「类型」还可以是值叫非类型模板参数non-type template parameter。最常见的是template std::size_t NN是编译期常量常用于std::array这种大小固定在编译期的容器// tmpl_nontype.cpp — 编译: g -stdc17 -Wall -O2 tmpl_nontype.cpp -o tn#includecstdiotemplatestd::size_t Nconstexprstd::size_tsquare(){returnN*N;}intmain(){// N 在编译期就是常量能用于需要编译期常量的地方如数组维度intbuf[square8()];std::printf(square8() %zubuf 大小 %zu\n,square8(),sizeof(buf)/sizeof(buf[0]));}square8() 64buf 大小 64square8()在编译期算出 64连int buf[...]的维度都能当编译期常量用。这正是std::arrayint, N背后那套机制。注意N必须是编译期已知的常量字面量、 constexpr 变量等不能是运行期变量。C17 起还能写template auto N让编译器自己推导这个非类型参数的类型int还是std::size_t省掉手写类型但值本身仍必须在编译期确定。把运行期变量传进去会直接编译失败这正是非类型模板参数与普通函数参数的分水岭前者是「编译期常量」后者是「运行期数据」。6. 默认模板参数类型或非类型参数都能给默认值调用时省略就采用默认// tmpl_default.cpp — 编译: g -stdc17 -Wall -O2 tmpl_default.cpp -o tdef#includecstdiotemplateclassTintTmake(T x){returnx;}intmain(){std::printf(默认 Tint: %d\n,make(42));std::printf(显式 Tdouble: %f\n,makedouble(3.14));}默认 Tint: 42 显式 Tdouble: 3.1400007. 为什么模板成员函数通常要放头文件这是 ODROne Definition Rule单一定义规则和模板实例化instantiation模型共同决定的模板不是函数而是「生成函数的蓝本」。只有编译器在看到调用点、且能看到模板完整定义时才能为具体类型实例化出真正的函数。如果定义藏在 .cpp 里别的翻译单元调用时只见到声明、见不到定义链接期就会报「未定义的引用undefined reference」。模板蓝本写在头文件 编译器在调用点做的事 template class T ┌──────────────────────────┐ void f(T x) { ... } │ 用实参类型替换 T │ │ 被 f(3) 调用 │ → 实例化出 fint │ ▼ │ → 生成一份具体函数机器码 │ 具体函数 fint 落进调用者所在的目标文件 所以定义必须可见写在头文件里官方文档Template — cppreference 与 Templates — cppreference实务建议普通模板把声明和定义都放头文件若实在要把定义藏进 .cpp如减少编译时间可用「显式实例化explicit instantiation」在 .cpp 末尾写template void fint(int);但那也要你预先知道会用哪些类型。8. 完整示例非类型参数 默认参数合起来把第 5、6 节的要点合进一个「编译期定容、默认存 int」的小缓冲// tmpl_full.cpp — 编译: g -stdc17 -Wall -O2 tmpl_full.cpp -o tf#includecstdiotemplatestd::size_t N,classTintstructBuf{T data[N]{};Ttotal()const{T s{};for(std::size_t i0;iN;i)sdata[i];returns;}};intmain(){Buf4b;// N4T 取默认 intb.data[0]10;b.data[1]20;b.data[2]30;b.data[3]40;std::printf(sum %d\n,b.total());std::printf(容量 N %zu\n,sizeof(b.data)/sizeof(b.data[0]));}sum 100 容量 N 4Buf4用非类型参数定容量T int走默认模板参数data是真正的编译期定长数组没有堆分配遵守 Core Guidelines 不用裸 new。一个例子覆盖「非类型参数 默认参数 编译期常量」三件事。9. 函数模板 vs 类模板一张表看清差异维度函数模板templateclass T void f(T)类模板templateclass T struct C能否自动推导参数从实参推导TC17 前必须显式写C17 起靠 CTAD 推导显式指定写法fint(x)Cint x或 C17C x{...}多个参数各自推导/指定各自显式/默认默认参数支持支持非类型参数templatesize_t N如std::array定义放哪通常头文件实例化需可见通常头文件同上10. 延伸阅读Function template — cppreference —— 实参推导规则的权威描述Class template argument deduction — cppreference —— C17 CTAD 的细节与「推导陷阱」Templates — cppreference —— 模板整体模型、实例化与 ODR 说明Compiler Explorer —— 看模板实例化后生成的具体函数汇编本知识库内的相关篇目《CTAD 与推导指引C17 类模板参数推导怎么用、哪里会踩坑》 —— C17 的 CTAD 让 std::pair、std::vector 这类类模板不用再写模板参数。《模板特化与偏特化为特定类型开小灶》 —— 模板特化与偏特化同体系里最该一起看的另一篇。《可变参数模板与 C17 折叠表达式从递归展开到一行求和》 —— 从 C11 时代的递归展开讲起11. 一句话总结函数模板能从实参自动推导T且按值传参时T会剥掉顶层 const 与引用和auto规则一致类模板在 C17 前必须显式指定类型C17 起靠 CTAD 自动推导但要警惕「推导出的类型不是你想要的」。非类型模板参数如size_t N是编译期常量、常用于定长容器模板本质是「生成函数的蓝本」定义需对调用点可见因此通常写在头文件里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑