当前位置: 首页 > news >正文

现代C++特性指南——constexpr 构造函数与字面类型

现代C++特性指南——constexpr 构造函数与字面类型

仓库已经开源!仍然在持续建设中,喜欢的话点个⭐!相关的链接如下:

clone me!: git clone https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP

静态网页体验极大改进,点击这里直接阅览:https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/

引言

上一章我们讨论了constexpr变量和constexpr函数,但所有的例子都局限在标量类型——整数、浮点数、指针这些"原始"类型。你可能会问:我能不能把自定义的类也放到编译期去用?比如在编译期构造一个复数对象,或者编译期算好一个日期,然后在运行时直接拿来用?

答案是肯定的,但有一个前提:你的类型必须是"字面类型"(literal type)。这个概念听起来有点学术,但实际上它就是编译器能理解并在编译期操作类型的约束清单。这一章我们来搞清楚什么是字面类型,怎么给自定义类型加上constexpr构造函数,以及 C++14 之后这些限制是如何被逐步放宽的。

第一步——什么是字面类型

字面类型这个名字确实容易让人困惑。它跟"字面量"(literal,比如42"hello")不是一回事。字面类型指的是一种满足特定约束的类型——编译器能够在编译期完整地构造、操作和销毁这种类型的对象。

具体来说,一个类型是字面类型需要满足以下条件:对于标量类型(算术类型、指针、引用、枚举)来说它们天然就是字面类型,不需要做任何额外的事情;对于类类型来说它需要有一个constexpr构造函数(至少一个,可以是拷贝或移动构造),所有非静态数据成员本身也必须是字面类型或其数组,并且它的析构函数要么是平凡的(trivial destructor),要么在 C++20 之后是constexpr的。

用更直白的话说:编译器需要在编译期就能完整地理解这个类型的内存布局和初始值,不需要运行时动态分配、虚函数表查找、或者复杂的析构逻辑。

// 这是一个字面类型structPoint{floatx;floaty;constexprPoint(floatx_,floaty_):x(x_),y(y_){}// 隐式的析构函数是平凡的,满足条件};constexprPoint kOrigin{0.0f,0.0f};static_assert(kOrigin.x==0.0f);static_assert(kOrigin.y==0.0f);

而下面这个就不是字面类型:

structNotLiteral{std::string name;// std::string 有非平凡的析构函数(C++20 之前)// 即使在 C++20 中,std::string 的析构虽然可以是 constexpr,// 但它内部涉及动态内存分配,在编译期求值时仍然受限};

std::string的问题在于它管理了动态内存。在 C++20 之前,constexpr函数中不允许使用new/delete,所以任何需要动态分配的类型都不可能在编译期使用。C++20 放宽了这个限制——允许在constexpr函数中使用new/delete,但有一个硬性约束:所有在编译期分配的内存必须在编译期求值结束前释放(不能泄漏到运行时)。这意味着你可以在编译期做复杂的字符串操作,但不能返回一个指向编译期分配内存的std::string到运行时(除非那个内存已经被释放或转移到可持久化的存储中)。

实际上,GCC 15.2.1 和 Clang 13+ 已经完整支持std::stringconstexpr操作,包括构造、拼接、子串等。你可以在编译期构建字符串、验证格式、生成查找表,只要所有动态内存在编译期被正确管理即可。

第二步——给自定义类型加上 constexpr 构造函数

最简单的情形:POD-like 类型

如果你的类就是一堆数据的聚合,没有虚函数、没有动态分配,那加上constexpr构造函数非常简单。

structColor{std::uint8_tr,g,b,a;constexprColor(std::uint8_tr_,std::uint8_tg_,std::uint8_tb_,std::uint8_ta_=255):r(r_),g(g_),b(b_),a(a_){}};constexprColor kRed{255,0,0};constexprColor kGreen{0,255,0};constexprColor kTransparentBlack{0,0,0,0};static_assert(kRed.r==255);static_assert(kTransparentBlack.a==0);

这就是一个字面类型了。构造函数用初始化列表把参数赋给成员,非常直接。

带逻辑的构造函数

构造函数里也可以有逻辑——前提是这些逻辑在constexpr允许的范围内。在 C++14 之后,你可以在构造函数里写循环、条件判断、局部变量。

structBcdDecimal{unsignedcharbcd;constexprexplicitBcdDecimal(intdecimal):bcd(0){// 将十进制整数转换为 BCD 编码intremainder=decimal;intshift=0;while(remainder>0){bcd|=(remainder%10)<<shift;remainder/=10;shift+=4;}}constexprintto_decimal()const{intresult=0;intmultiplier=1;unsignedchartemp=bcd;while(temp>0){result+=(temp&0x0F)*multiplier;temp>>=4;multiplier*=10;}returnresult;}};constexprBcdDecimal kDec42{42};static_assert(kDec42.bcd==0x42,"BCD of 42 should be 0x42");static_assert(kDec42.to_decimal()==42,"Round-trip conversion should work");

这段代码在构造函数中实现了十进制到 BCD 编码的转换。整个计算在编译期完成,kDec42bcd成员直接被写为0x42。这种模式在嵌入式开发中特别有用——你可以在编译期把人类可读的十进制值转换为硬件要求的 BCD 编码,运行时直接使用预计算的值,无需任何转换指令。

让我们验证一下:在 GCC 15.2.1(-std=c++20 -O2)下,访问kDec42.bcd的汇编代码只是一条mov指令从 .rodata 段加载常量,而运行时计算 BCD 需要多条除法、移位和循环指令。编译期版本确实实现了零运行时开销。

第三步——constexpr 成员函数

不仅构造函数可以constexpr,普通成员函数也可以。而且从 C++14 开始,constexpr成员函数可以修改对象的成员变量(只要调用上下文允许)。

编译期复数类

让我们来写一个可以在编译期使用的复数类。这个例子比较实用,因为在信号处理中复数运算无处不在。

structComplex{floatreal;floatimag;constexprComplex(floatr=0.0f,floati=0.0f):real(r),imag(i){}constexprComplexoperator+(constComplex&other)const{returnComplex{real+other.real,imag+other.imag};}constexprComplexoperator-(constComplex&other)const{returnComplex{real-other.real,imag-other.imag};}constexprComplexoperator*(constComplex&other)const{returnComplex{real*other.real-imag*other.imag,real*other.imag+imag*other.real};}constexprfloatmagnitude_squared()const{returnreal*real+imag*imag;}constexprbooloperator==(constComplex&other)const{returnreal==other.real&&imag==other.imag;}};// 编译期复数运算constexprComplex kI{0.0f,1.0f};// 虚数单位 iconstexprComplex kI_Squared=kI*kI;// i^2 = -1static_assert(kI_Squared==Complex{-1.0f,0.0f},"i^2 should equal -1");// 编译期生成复数序列(例如 FFT 的旋转因子)template<std::size_t N>constexprComplexcompute_twiddle_factor(std::size_t k){constexprdoublekPi=3.14159265358979323846;doubleangle=-2.0*kPi*static_cast<double>(k)/static_cast<double>(N);// 用泰勒展开近似 cos 和 sindoublecos_val=1.0-angle*angle/2.0+angle*angle*angle*angle/24.0;doublesin_val=angle-angle*angle*angle/6.0+angle*angle*angle*angle*angle/120.0;returnComplex{static_cast<float>(cos_val),static_cast<float>(sin_val)};}constexprComplex kTwiddle=compute_twiddle_factor<8>(1);static_assert(kTwiddle.magnitude_squared()>0.99f,"Twiddle factor should be on unit circle");

这个Complex类完全是字面类型。它的构造函数是constexpr的,所有运算符和成员函数也是。你可以在编译期做复数运算、生成 FFT 旋转因子表——所有这些计算结果都会被编译器优化为常量,直接嵌入到代码中或放入 .rodata 只读数据段(具体取决于优化级别和使用方式)。

例如,在 GCC 15.2.1(-std=c++20 -O2)下,kI_Squared会被放入 .rodata 段作为一个常量,访问它只是一条内存加载指令。而kTwiddleFactors数组会被完整地编译进二进制,运行时访问没有任何计算开销。如果这些值被内联到使用点,甚至可能连加载指令都被优化掉,直接成为立即数。

编译期日期计算

另一个实用场景是日期。很多协议和时间相关的逻辑需要验证日期的合法性。我们可以把这些验证放到编译期。

structDate{intyear;intmonth;intday;constexprDate(inty,intm,intd):year(y),month(m),day(d){// 编译期验证日期合法性// 如果日期非法,触发编译错误(通过让表达式非恒常)}constexprboolis_leap_year()const{return(year%4==0&&year%100!=0)||(year%400==0);}constexprintdays_in_month()const{constexprintkDays[]={0,31,28,31,30,31,30,31,31,30,31,30,31};if(month==2&&is_leap_year()){return29;}returnkDays[month];}constexprboolis_valid()const{if(month<1||month>12)returnfalse;if(day<1||day>days_in_month())returnfalse;if(year<0)returnfalse;returntrue;}};constexprDate kEpoch{1970,1,1};static_assert(kEpoch.is_valid());static_assert(!kEpoch.is_leap_year());constexprDate kY2K{2000,1,1};static_assert(kY2K.is_leap_year(),"2000 is a leap year (divisible by 400)");constexprDate kLeapDay{2024,2,29};static_assert(kLeapDay.is_valid(),"2024-02-29 is valid (2024 is a leap year)");// constexpr Date kInvalid{2023, 2, 29}; // 编译时不会直接报错// 需要用 static_assert 显式检查:// static_assert(Date{2023, 2, 29}.is_valid()); // 编译错误!

这里有一个要点:constexpr构造函数本身不会因为"逻辑上不合理"的值而报错。你需要在构造函数中主动触发编译期错误(比如用throw,在constexpr上下文中异常就是编译错误),或者用static_assert配合is_valid()来检查。

编译期字符串长度

让成员函数返回编译期可用的值也是constexpr的重要应用。比如一个简单的编译期字符串包装类。

#include<cstddef>structConstString{constchar*data;std::size_t length;template<std::size_t N>constexprConstString(constchar(&str)[N]):data(str),length(N-1){// N - 1 是因为字符串字面量的末尾有 '\0'}constexprcharoperator[](std::size_t i)const{returni<length?data[i]:'\0';}constexprboolstarts_with(charc)const{returnlength>0&&data[0]==c;}constexprboolequals(constConstString&other)const{if(length!=other.length)returnfalse;for(std::size_t i=0;i<length;++i){if(data[i]!=other.data[i])returnfalse;}returntrue;}};constexprConstString kHello{"Hello"};static_assert(kHello.length==5);static_assert(kHello[0]=='H');static_assert(kHello.starts_with('H'));static_assert(kHello.equals(ConstString{"Hello"}));

这个ConstString本质上就是 cppreference 官方示例中conststr类的简化版。它不拥有字符串数据,只是持有一个指针和长度,但足以在编译期做很多字符串操作了。

第四步——C++14 放宽的限制

前面已经提到了,C++14 对constexpr构造函数和成员函数的限制做了大幅放宽。具体到类类型,这些变化带来的影响是:

在 C++11 中,constexpr构造函数的函数体必须为空——所有初始化工作只能通过成员初始化列表完成,不能有循环、条件判断或局部变量。这意味着如果你的构造逻辑稍复杂(比如需要遍历数组、根据条件设置不同值),就得想办法用三元运算符和递归函数来绕过限制。

C++14 之后,构造函数里可以写任何constexpr允许的语句了。局部变量、for循环、if-else都没问题。这让很多原本不可能的编译期类成为现实。

// C++11 风格:构造函数体必须为空structOldStyle{intvalues[4];// 只能用初始化列表constexprOldStyle(inta,intb,intc,intd):values{a,b,c,d}{}};// C++14 风格:构造函数体可以有逻辑structNewStyle{intvalues[4];intsum;constexprNewStyle(intbase):values{},sum(0){for(inti=0;i<4;++i){values[i]=base+i;sum+=values[i];}}};constexprNewStyle kObj{10};static_assert(kObj.values[0]==10);static_assert(kObj.values[3]==13);static_assert(kObj.sum==46);// 10+11+12+13=46

第五步——constexpr 析构函数(C++20 预告)

在 C++20 之前,字面类型要求析构函数必须是平凡的(trivial)。这意味着你不能在析构函数里做任何清理工作。这个限制在 C++20 中被取消——你可以写constexpr析构函数了。

// C++20 才支持structResource{int*data;std::size_t size;constexprResource(std::size_t n):data{},size(n){// C++20 允许在 constexpr 上下文中使用 new// 但分配的内存必须在常量求值结束前释放}// C++20: constexpr 析构函数constexpr~Resource(){// 清理逻辑}};

这个特性在 C++20 中已经被主流编译器完整支持。GCC 10+、Clang 10+、MSVC 19.28+ 都支持constexpr析构函数。对于大多数嵌入式场景来说,constexpr析构函数的主要意义在于让std::vectorstd::string等标准容器能够更完整地参与编译期计算——你可以在编译期构造容器、操作元素,然后在编译期销毁它们。

这里值得顺带提一句的是 C++23 对constexpr的进一步放宽:constexpr函数不再要求返回类型和参数类型必须是字面类型(P2448R2),非字面类型的局部变量、goto语句和标签也被允许了。这意味着从 C++23 开始,constexpr函数的定义限制已经非常少了。当然,要在编译期实际调用(求值)这些函数,仍然受到常量表达式求值规则的约束——你只是可以写更自由的函数体了。

实战应用:嵌入式中的编译期配置

在嵌入式开发中,外设配置通常是一堆固定参数——波特率、数据位、停止位、校验方式等。我们可以用字面类型把这些配置打包成编译期常量。

enumclassParity{kNone,kEven,kOdd};enumclassStopBits{kOne,kTwo};structUartConfig{std::uint32_tbaud_rate;std::uint8_tdata_bits;StopBits stop_bits;Parity parity;constexprUartConfig(std::uint32_tbaud,std::uint8_tdata,StopBits stop,Parity par):baud_rate(baud),data_bits(data),stop_bits(stop),parity(par){}constexprboolis_valid()const{if(baud_rate==0)returnfalse;if(data_bits<5||data_bits>9)returnfalse;returntrue;}constexprstd::uint32_tcompute_brr(std::uint32_tclock_freq)const{// 简化的波特率寄存器值计算(STM32 风格)returnclock_freq/baud_rate;}};// 常用配置的编译期常量constexprUartConfig kDebugUart{115200,8,StopBits::kOne,Parity::kNone};constexprUartConfig kGpsUart{9600,8,StopBits::kOne,Parity::kNone};static_assert(kDebugUart.is_valid());static_assert(kDebugUart.compute_brr(72000000)==625);// 72MHz / 115200

kDebugUartkGpsUart在编译期就完成了所有验证和计算。如果有人改了波特率为 0 或者数据位为 3,static_assert会在编译期炸掉。波特率寄存器的值也被预计算好了,运行时直接写入寄存器即可。

常见陷阱

非平凡析构函数的阻塞

如果你的类有非平凡的析构函数(比如手动管理了资源),在 C++20 之前它就不能是字面类型。即使你的构造函数是constexpr的,析构函数不是constexpr(或平凡的)也会阻止编译期使用。一个常见的变通方法是把析构函数声明为= default,让编译器生成一个平凡的析构函数——前提是你的类确实不需要自定义析构逻辑。

mutable成员

mutable数据成员会导致一些意外行为。constexpr对象的mutable成员在编译期求值时被视为可修改的,但这会导致某些上下文中编译期求值失败(因为mutable破坏了"对象在编译期完全确定"的语义假设)。

虚函数与虚基类

有虚函数或有虚基类的类永远不可能是字面类型(直到目前的标准都是如此)。如果你需要在编译期使用一个类型层次结构,考虑用 CRTP(Curiously Recurring Template Pattern)来替代虚函数。

小结

这一章我们覆盖了字面类型的定义和约束、constexpr构造函数的写法、constexpr成员函数的使用,以及 C++14/C++20/C++23 对这些限制的逐步放宽。核心要点是:只要你的类型的内存布局和生命周期在编译期就能完全确定,编译器就可以在编译期构造和操作它。编译期复数、日期、字符串、配置结构这些类型都可以成为字面类型,从而参与到更复杂的编译期计算中去。

下一章我们会介绍 C++20 新增的constevalconstinit关键字,看看它们如何精确控制编译期求值的行为。

参考资源

  • cppreference: constexpr specifier
  • cppreference: LiteralType requirement
  • cppreference: constant expressions

相关阅读

  1. constexpr 基础:编译期求值的艺术 - 相似度 100%
  2. RVO 与 NRVO:编译器的返回值优化 - 相似度 67%
  3. 完美转发与移动语义实战 - 相似度 67%
http://www.gsyq.cn/news/1511803.html

相关文章:

  • AI科技热点日报 | 2026年06月12日
  • 深度解析AhMyth Android RAT:移动设备安全威胁的技术剖析与防御策略
  • 武汉科谷技工学校是公办还是民办?热门专业宠物医疗与护理值得关注 - 辛云教育资讯
  • 基于Nuvoton M451的WIFI室内安防报警系统(含原理图、Keil源码、设计报告)
  • 从数据混乱到游戏掌控:Snap Hutao原神工具箱三步提升你的提瓦特体验
  • Techwiz LCD:基板未对准分析
  • 2026年广西建筑资质服务选购指南:广西建筑资质转让、资质新办延期、工商地址托管、企业资质代办优选指南 - 海棠依旧大
  • DSC双哈佛架构与实时控制:从56F807看电机驱动与数字电源设计
  • 如何永久保存QQ空间青春记忆:GetQzonehistory完整备份方案
  • # 2026年华中山涧漂流乐园实力排行榜:河南尧山的5大权威推荐 - 十大品牌榜
  • 量子物理信息神经网络在多物种反应扩散系统中的应用
  • 2026 年合肥梅雨季|马桶堵了别硬通,家家通就近上门 - 吉修匠
  • Obsidian-Export:解锁Obsidian笔记跨平台迁移的智能转换方案
  • 郑州大牌包包回收实测|LV / 香奈儿 / 爱马仕真实成交价 - 讯息早知道
  • Snap Hutao:原神玩家的终极Windows工具箱完全指南
  • Boss-Key:Windows平台终极窗口隐藏解决方案,一键保护你的工作隐私
  • 【广州楼市研判系列34】2026番禺置业避坑指南|读懂片区能级分化,自住置换双向守住房产保值底盘 - 速递信息
  • 西安家谱印刷找五花马印务|西北专业宣纸家谱、精装族谱定制权威机构 - 小熊打盹
  • 2026年亨得利 官方售后服务网点实地考察报告(中国区60+门店全覆盖) - 亨得利中国服务中心
  • 2026年曲阳展馆雕塑定制全攻略:源头厂家选型、竞品对标与避坑指南 - 年度推荐企业名录
  • 从交叉熵到对比学习:InfoNCE Loss如何让模型学会“找不同”?
  • 2026 年新蔡县抖音内容闭环运营与同城快照 SEO:本地门店线上高效揽客完整方案 - 年度推荐企业名录
  • 2026 年青岛汛期回南天|马桶堵了别硬通,家家通就近上门 - 吉修匠
  • ComfyUI-Impact-Pack V8:AI图像增强的终极指南,让模糊图像秒变高清
  • Qt新手避坑指南:Q_PROPERTY声明属性时,NOTIFY信号到底该怎么写才不崩溃?
  • 2026年四川职业中学专业选择深度分析:成都热门方向解读 - 深度智识库
  • 2026石家庄黄金回收全攻略:七大正规渠道深度测评金条/金饰变现必看六月新出炉 - 薛定谔的梨花猫
  • 专升本语文真题|语文|资料已整理
  • Windows资源管理器变身3D画廊:Space Thumbnails让你的模型文件“开口说话“
  • 2026国内优质隐形车衣/车膜/改色车衣/车衣/汽车贴膜品牌厂家推荐,超佩车膜打造适配中国环境的专业汽车防护方案 - 十大品牌榜