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