C++ Templates 04:不止传类型,还能传值——聊聊非类型模板参数
- Bilibili 同步视频
- 一、类模板实战:编译期定容量的栈
- 使用这个栈
- ⚠️一个超级容易踩的坑:实例之间完全不兼容!
- 二、函数模板也能用非类型参数✨
- 三、划重点!非类型模板参数有严格的准入黑名单🚫
- 1. 浮点数不行
- 2. 类对象不能直接塞进去
- 3. 字符串字面量 `“hello”` 直接传,不行!
- 四、总结回顾📝
摘要:提起 C+C++ Templates 04:不止传类型,还能传值——聊聊非类型模板参数+ 模板,绝大多数人第一反应都是
template<typename T>,用 T 来代表任意类型。但模板的能力远不止于此!除了把类型当参数,我们还能直接把编译期常量值塞进模板,这就是「非类型模板参数」。用好它可以在编译阶段就完成一部分计算,省去运行时的开销,但它也有不少容易踩坑的语法陷阱,今天我们结合实例把这套机制讲透。
在日常写 C++ 代码的时候,容器大小往往是运行时确定的。比如std::vector,运行时动态分配内存,灵活是灵活,但免不了堆内存分配、释放的开销。
那如果我们已经在写代码的时候就知道容器最大需要存多少元素,能不能干脆用固定大小数组实现,把容量交给编译器处理?答案就是非类型模板参数。
简单理解:普通模板参数传的是「类型」,而非类型模板参数,传给模板的是一个编译期就确定的常量值。编译器拿到这个常量,在实例化阶段直接生成对应版本代码,全程不需要运行时参与,天然拥有编译期优化的红利🚀。
Bilibili 同步视频
C++ Templates 04:不止传类型,还能传值——聊聊非类型模板参数
一、类模板实战:编译期定容量的栈
我们来实现一个栈。传统动态栈需要自己管理堆内存,容易出现内存泄漏;如果直接写死数组大小,写T elems[100],容量写死,太小会溢出,太大又白白浪费内存。
借助非类型模板参数,我们把栈的最大容量变成模板的第二个参数,让使用者在实例化的时候指定最大存储数量。
#include<stdexcept>#include<iostream>#include<string>// T:栈存储元素类型;MAXSIZE:非类型模板参数,编译期常量,代表栈最大容量template<typenameT,intMAXSIZE>classStack{private:T elems[MAXSIZE];// 编译期确定大小的数组,无堆内存开销intnumElems;// 当前栈内元素计数public:Stack():numElems(0){}voidpush(Tconst&elem){if(numElems==MAXSIZE){throwstd::out_of_range("Stack<>::push(): stack is full");}elems[numElems]=elem;++numElems;}voidpop(){if(numElems<=0){throwstd::out_of_range("Stack<>::pop(): empty stack");}--numElems;}Ttop()const{if(numElems<=0){throwstd::out_of_range("Stack<>::top(): empty stack");}returnelems[numElems-1];}boolempty()const{returnnumElems==0;}boolfull()const{returnnumElems==MAXSIZE;}};使用这个栈
intmain(){try{Stack<int,20>int20Stack;// 最多容纳20个intStack<int,40>int40Stack;// 最多容纳40个intStack<std::string,40>strStack;int20Stack.push(99);std::cout<<int20Stack.top()<<"n";int20Stack.pop();strStack.push("Hello Template");std::cout<<strStack.top()<<"n";strStack.pop();// strStack.pop(); // 取消注释会抛出空栈异常}catch(std::exceptionconst&ex){std::cerr<<"异常捕获:"<<ex.what()<<std::endl;}return0;}⚠️一个超级容易踩的坑:实例之间完全不兼容!
Stack<int,20>和Stack<int,40>,看着几乎一模一样,仅仅是非类型参数MAXSIZE数值不一样,但在编译器眼中,这是两个完全无关的不同类型。
下面这段代码直接编译报错:
Stack<int,20>s1;Stack<int,40>s2;s1=s2;// ❌编译失败!类型不一样,不能赋值,也不存在隐式转换💡性能小知识:
因为数组大小是编译期确定,这个栈全程使用栈上数组,没有 new/delete,没有堆内存分配开销。代价是:不同容量就会实例化出一份全新的类代码,如果定义大量不同 MAXSIZE 的栈,会带来模板代码膨胀。
我们也可以给非类型模板参数设置默认值:
template<typenameT=int,intMAXSIZE=100>classStack{/* ... */};但实际开发不建议随便给默认参数。默认值应当贴合业务场景,int、100这种通用默认值往往没什么实际意义,不如让调用方显式指定,代码可读性更高。
二、函数模板也能用非类型参数✨
不要以为非类型模板参数只属于类模板,函数模板同样支持。我们可以把常量固化到函数内部,编译期生成特化版本。
举个例子,写一个函数:给传入的值,加上一个编译期固定常量。
template<typenameT,intVAL>TaddValue(Tconst&x){returnx+VAL;}这里VAL就是函数模板的非类型参数。调用的时候addValue<int,5>(10),就得到 15;相当于编译出一个固定 + 5 的函数。
这个模板和 STL 算法搭配非常香,比如std::transform批量对容器元素做运算。但是这里藏着一个经典 "坑"👇
#include<algorithm>#include<vector>intmain(){std::vector<int>source{1,2,3,4};std::vector<int>dest(4);// 错误写法!直接传 addValue<int,5> 会编译报错// std::transform(source.begin(), source.end(), dest.begin(), addValue<int,5>);// ✅正确写法:强制转换成函数指针std::transform(source.begin(),source.end(),dest.begin(),(int(*)(intconst&))addValue<int,5>);return0;}为什么直接写会报错?addValue<int,5>虽然我们只实例化了一份,但编译器仍然把它看作重载函数集合。C++ 标准不允许直接把重载集合传入模板参数,必须手动强转成具体函数指针类型。
小道消息:C++ 标准库工作组已经有提案希望消除这个别扭的强制转换,未来版本的 C++ 有可能可以直接写,但是现在写可移植代码还得老老实实加上转换。
三、划重点!非类型模板参数有严格的准入黑名单🚫
非类型模板参数听着很强,但不是什么东西都能往里塞,它对传入的实参有硬性约束:
✅允许传入:整型常量、枚举值、拥有外部链接的指针 / 数组
❌禁止传入:浮点数、类对象、字符串字面量、内部链接的指针
1. 浮点数不行
template<doubleVAT>doublecalc(doublev){returnv*VAT;}// calc<1.2>(100); // ❌编译报错,浮点数不能作为非类型模板实参虽然从技术实现角度,现代编译器完全有能力支持浮点常量,但出于历史标准原因,C++ 目前不支持把浮点数作为模板非类型参数。
2. 类对象不能直接塞进去
#include<string>template<std::string Name>// ❌报错,类对象不允许classDemo{};3. 字符串字面量“hello”直接传,不行!
template<constchar*Name>classDemo{};Demo<"hello">obj;// ❌报错!字符串字面量属于内部链接对象字符串字面量,在不同编译单元,就算文字一模一样,也是完全独立两份对象,C++ 标准禁止拿来当模板实参。
那如果我就想传字符串常量指针怎么办?要用extern 外部链接全局字符数组:
template<constchar*Name>classDemo{};// 外部链接全局字符数组externconstcharmyStr[]="Hello C++ Template";Demo<myStr>obj;// ✅合法重点区分:
constchar*s="abc";Demo<s>obj2;// ❌依然错误!s只是指针变量,指向内部链接字符串字面量
四、总结回顾📝
C++ 模板分为类型模板参数(
typename T)和非类型模板参数,后者接收编译期常量值,在编译阶段完成实例化,很多场景可以规避运行时内存开销。同一个模板,非类型参数数值不同,生成完全不同的类型,互相之间不能赋值转换,也要留意模板实例代码膨胀风险。
类模板、函数模板都支持非类型参数;和 STL 算法配合的时候要留意重载函数集合带来的强制转换问题。
参数有严格限制:只接受整型、枚举、外部链接指针;浮点数、普通类对象、字符串字面量都是黑名单成员,踩了直接编译报错。
拓展思考:C++17 之后新增了 auto 非类型模板参数,
template<auto N>,可以自动推导非类型参数的类型,这又是另外一个有趣的知识点,后续可以再聊聊。