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 <typename T, int MAXSIZE>
class Stack {
private:
T elems[MAXSIZE]; // 编译期确定大小的数组,无堆内存开销
int numElems; // 当前栈内元素计数
public:
Stack() : numElems(0) {}
void push(T const& elem) {
if (numElems == MAXSIZE) {
throw std::out_of_range("Stack<>::push(): stack is full");
}
elems[numElems] = elem;
++numElems;
}
void pop() {
if (numElems <= 0) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
—numElems;
}
T top() const {
if (numElems <= 0) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elems[numElems – 1];
}
bool empty() const { return numElems == 0; }
bool full() const { return numElems == MAXSIZE; }
};
使用这个栈
int main()
{
try {
Stack<int, 20> int20Stack; // 最多容纳20个int
Stack<int, 40> int40Stack; // 最多容纳40个int
Stack<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::exception const& ex)
{
std::cerr << "异常捕获:" << ex.what() << std::endl;
}
return 0;
}
⚠️一个超级容易踩的坑:实例之间完全不兼容!
Stack<int,20> 和 Stack<int,40>,看着几乎一模一样,仅仅是非类型参数MAXSIZE数值不一样,但在编译器眼中,这是两个完全无关的不同类型。
下面这段代码直接编译报错:
Stack<int,20> s1;
Stack<int,40> s2;
s1 = s2; // ❌编译失败!类型不一样,不能赋值,也不存在隐式转换
💡性能小知识: 因为数组大小是编译期确定,这个栈全程使用栈上数组,没有 new/delete,没有堆内存分配开销。代价是:不同容量就会实例化出一份全新的类代码,如果定义大量不同 MAXSIZE 的栈,会带来模板代码膨胀。
我们也可以给非类型模板参数设置默认值:
template<typename T = int, int MAXSIZE = 100>
class Stack { /* … */ };
但实际开发不建议随便给默认参数。默认值应当贴合业务场景,int、100这种通用默认值往往没什么实际意义,不如让调用方显式指定,代码可读性更高。
二、函数模板也能用非类型参数✨
不要以为非类型模板参数只属于类模板,函数模板同样支持。我们可以把常量固化到函数内部,编译期生成特化版本。
举个例子,写一个函数:给传入的值,加上一个编译期固定常量。
template<typename T, int VAL>
T addValue(T const& x)
{
return x + VAL;
}
这里VAL就是函数模板的非类型参数。调用的时候addValue<int,5>(10),就得到 15;相当于编译出一个固定 + 5 的函数。
这个模板和 STL 算法搭配非常香,比如std::transform批量对容器元素做运算。但是这里藏着一个经典 "坑"👇
#include <algorithm>
#include <vector>
int main()
{
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(*)(int const&))addValue<int,5>
);
return 0;
}
为什么直接写会报错? addValue<int,5>虽然我们只实例化了一份,但编译器仍然把它看作重载函数集合。C++ 标准不允许直接把重载集合传入模板参数,必须手动强转成具体函数指针类型。
小道消息:C++ 标准库工作组已经有提案希望消除这个别扭的强制转换,未来版本的 C++ 有可能可以直接写,但是现在写可移植代码还得老老实实加上转换。
三、划重点!非类型模板参数有严格的准入黑名单🚫
非类型模板参数听着很强,但不是什么东西都能往里塞,它对传入的实参有硬性约束:
✅允许传入:整型常量、枚举值、拥有外部链接的指针 / 数组 ❌禁止传入:浮点数、类对象、字符串字面量、内部链接的指针
1. 浮点数不行
template<double VAT>
double calc(double v)
{
return v * VAT;
}
// calc<1.2>(100); // ❌编译报错,浮点数不能作为非类型模板实参
虽然从技术实现角度,现代编译器完全有能力支持浮点常量,但出于历史标准原因,C++ 目前不支持把浮点数作为模板非类型参数。
2. 类对象不能直接塞进去
#include <string>
template<std::string Name> // ❌报错,类对象不允许
class Demo{};
3. 字符串字面量 “hello” 直接传,不行!
template<const char* Name>
class Demo{};
Demo<"hello"> obj; // ❌报错!字符串字面量属于内部链接对象
字符串字面量,在不同编译单元,就算文字一模一样,也是完全独立两份对象,C++ 标准禁止拿来当模板实参。
那如果我就想传字符串常量指针怎么办?要用extern 外部链接全局字符数组:
template<const char* Name>
class Demo{};
// 外部链接全局字符数组
extern const char myStr[] = "Hello C++ Template";
Demo<myStr> obj; // ✅合法
重点区分:
const char* s = "abc";
Demo<s> obj2; // ❌依然错误!s只是指针变量,指向内部链接字符串字面量
四、总结回顾📝
C++ 模板分为类型模板参数(typename T)和非类型模板参数,后者接收编译期常量值,在编译阶段完成实例化,很多场景可以规避运行时内存开销。
同一个模板,非类型参数数值不同,生成完全不同的类型,互相之间不能赋值转换,也要留意模板实例代码膨胀风险。
类模板、函数模板都支持非类型参数;和 STL 算法配合的时候要留意重载函数集合带来的强制转换问题。
参数有严格限制:只接受整型、枚举、外部链接指针;浮点数、普通类对象、字符串字面量都是黑名单成员,踩了直接编译报错。

拓展思考:C++17 之后新增了 auto 非类型模板参数,template<auto N>,可以自动推导非类型参数的类型,这又是另外一个有趣的知识点,后续可以再聊聊。
网硕互联帮助中心


评论前必须登录!
注册