news 2026/10/5 8:40:08

C++ Templates 04:不止传类型,还能传值——聊聊非类型模板参数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ Templates 04:不止传类型,还能传值——聊聊非类型模板参数

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只是指针变量,指向内部链接字符串字面量

四、总结回顾📝

  1. C++ 模板分为类型模板参数(typename T)和非类型模板参数,后者接收编译期常量值,在编译阶段完成实例化,很多场景可以规避运行时内存开销。

  2. 同一个模板,非类型参数数值不同,生成完全不同的类型,互相之间不能赋值转换,也要留意模板实例代码膨胀风险。

  3. 类模板、函数模板都支持非类型参数;和 STL 算法配合的时候要留意重载函数集合带来的强制转换问题。

  4. 参数有严格限制:只接受整型、枚举、外部链接指针;浮点数、普通类对象、字符串字面量都是黑名单成员,踩了直接编译报错。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 8:38:22

Linux下python-snap7找不到snap7库的根源与解决

1. 项目概述&#xff1a;这不是Python代码的问题&#xff0c;是系统级链接失效的典型症状 “python-snap7 报错&#xff1a;can’t find snap7 library. If installed, try running ldconfig”——这句话我第一次在客户现场看到时&#xff0c;正蹲在PLC机柜旁调试西门子S7-120…

作者头像 李华
网站建设 2026/10/5 8:38:18

Amlogic以太网调试实战:RGMII时序与PHY适配全解析

1. 项目概述&#xff1a;为什么Amlogic平台的以太网调试不是“插上线就能用”的事Amlogic芯片——尤其是S905X3、S922X、A311D这些被大量用于NAS盒子、边缘计算网关、工业HMI和智能终端的SoC——在实际落地时&#xff0c;十次有七次会卡在以太网环节。不是网口不亮&#xff0c;…

作者头像 李华
网站建设 2026/10/5 8:38:15

YOLOv11边缘部署实战:INT8量化与TensorRT加速全解析

简介&#xff1a;这份PDF文档面向边缘计算与目标检测方向的开发者、算法工程师及高校学生&#xff0c;聚焦YOLOv11模型量化与TensorRT加速的完整实战路径&#xff0c;帮助读者解决边缘设备上目标检测推理效率低、部署成本高的实际问题。资源包共1个PDF文件&#xff0c;大小约1.…

作者头像 李华
网站建设 2026/10/5 8:38:06

Simulink S-function内存与实时性底层原理详解

1. 为什么S-function不是“写个C函数就完事”——从一个被反复踩爆的初始化陷阱说起 我第一次在汽车电控项目里用S-function&#xff0c;是为了解决Simulink自带的CAN模块无法解析某款国产ECU自定义报文格式的问题。当时信心满满&#xff1a;不就是写个C函数&#xff0c;把 md…

作者头像 李华
网站建设 2026/10/5 8:37:56

Angular加载本地JSON文件的正确姿势与工程化实践

1. 项目概述&#xff1a;Angular中访问本地JSON文件&#xff0c;到底在解决什么问题&#xff1f; 在Angular项目里读一个本地JSON文件&#xff0c;听起来简单得像“把水倒进杯子里”——但实际动手时&#xff0c;90%的开发者会在第3步卡住&#xff1a;要么控制台报404&#xff…

作者头像 李华
网站建设 2026/10/5 8:36:45

安卓学习记录

&#x1f7e2; 阶段一&#xff1a;新手村&#xff08;语言与工具&#xff09;目标&#xff1a;能看懂代码&#xff0c;能让程序跑起来。Kotlin 基础&#xff1a;只需要学 变量、if/else、for/while、函数、类。协程、泛型、高阶函数统统先跳过。Android Studio&#xff1a;学会…

作者头像 李华