现代C++特性:使用用户字面量干活
现代C特性使用用户字面量干活这个仓库已经开源现代化 CC11/14/17/20从基础到进阶的系统教程都在这里力争做一条完备的现代 C 学习路径欢迎各位大佬前来参观喜欢的话点个⭐Github 一键直达: git clone https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP看看超酷的新网站https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/上一篇我们学了用户自定义字面量的基础语法——operator的各种形式、标准库字面量、命名规则。这一篇我们要把这些知识用起来构建一个真正实用的类型安全单位系统。我们的目标是让100_m 500_m返回一个长度让100_m / 2_s返回一个速度让100_m 50_s直接编译报错。所有转换在编译期完成运行时零开销。第一步长度单位系统先从最简单的长度单位开始。我们用模板来定义一个通用的带单位的值然后为不同的长度单位定义字面量#includecstdint#includetype_traits/// 单位标签用于区分不同类型的物理量structMeterTag{};structSecondTag{};/// 带单位的值templatetypenameT,typenameUnitTagstructQuantity{T value;constexprexplicitQuantity(T v):value(v){}constexprQuantityoperator(Quantity other)const{returnQuantity{valueother.value};}constexprQuantityoperator-(Quantity other)const{returnQuantity{value-other.value};}constexprQuantityoperator*(T scalar)const{returnQuantity{value*scalar};}constexprQuantityoperator/(T scalar)const{returnQuantity{value/scalar};}constexprbooloperator(Quantity other)const{returnvalueother.value;}constexprbooloperator(Quantity other)const{returnvalueother.value;}};/// 标量 × 单位反向乘法/// 注意这个模板要求标量类型 T 必须与 Quantity 的 T 完全匹配/// 如果需要支持类型转换需要提供额外的重载templatetypenameT,typenameUnitTagconstexprQuantityT,UnitTagoperator*(T scalar,QuantityT,UnitTagq){returnq*scalar;}/// 支持整数标量 × long double Quantity 的重载templatetypenameUnitTagconstexprQuantitylongdouble,UnitTagoperator*(intscalar,Quantitylongdouble,UnitTagq){returnQuantitylongdouble,UnitTag{q.value*scalar};}QuantityT, UnitTag是一个模板UnitTag是一个空的标签类型唯一的作用是让不同单位的物理量成为不同的类型。MeterTag和SecondTag之间没有任何继承关系所以Quantitydouble, MeterTag和Quantitydouble, SecondTag是完全不同的类型——你不可能把一个赋给另一个。现在定义长度类型别名和字面量usingLengthQuantitylongdouble,MeterTag;// 字面量以米为基准单位constexprLengthoperator_m(longdoublev){returnLength{v};}constexprLengthoperator_km(longdoublev){returnLength{v*1000.0L};}constexprLengthoperator_cm(longdoublev){returnLength{v/100.0L};}constexprLengthoperator_mm(longdoublev){returnLength{v/1000.0L};}// 整数版本constexprLengthoperator_m(unsignedlonglongv){returnLength{static_castlongdouble(v)};}constexprLengthoperator_km(unsignedlonglongv){returnLength{static_castlongdouble(v)*1000.0L};}来测试一下voidtest_length(){constexprautod11.5_m;// 1.5 米constexprautod22.0_km;// 2000 米注意2_km 会失败因为只定义了浮点重载constexprautod3100.0_cm;// 1 米constexprautod4500.0_mm;// 0.5 米// 编译期计算constexprautototal1.0_km500.0_m;// 1500 米static_assert(total.value1500.0L);// 标量乘法现在支持整数了constexprautodoubled2*100.0_m;// 200 米static_assert(doubled.value200.0L);// 类型安全不能把长度和时间相加// auto bad 100_m 50_s; // 编译错误}1.0_km 500.0_m在编译期就被计算为1500.0_m。如果你试图把长度和时间相加编译器会直接报错——因为Quantitylong double, MeterTag和Quantitylong double, SecondTag是不同的类型。第二步时间与速度单位长度系统可以独立运作但物理计算的魅力在于不同单位的组合。长度除以时间得到速度——我们需要让Quantity支持这种跨单位的运算/// 速度标签structSpeedTag{};usingTimeDurationQuantitylongdouble,SecondTag;usingSpeedQuantitylongdouble,SpeedTag;// 时间字面量以秒为基准constexprTimeDurationoperator_s(longdoublev){returnTimeDuration{v};}constexprTimeDurationoperator_ms(longdoublev){returnTimeDuration{v/1000.0L};}constexprTimeDurationoperator_min(longdoublev){returnTimeDuration{v*60.0L};}constexprTimeDurationoperator_h(longdoublev){returnTimeDuration{v*3600.0L};}// 整数版本constexprTimeDurationoperator_s(unsignedlonglongv){returnTimeDuration{static_castlongdouble(v)};}constexprTimeDurationoperator_ms(unsignedlonglongv){returnTimeDuration{static_castlongdouble(v)/1000.0L};}/// 长度 / 时间 速度constexprSpeedoperator/(Length len,TimeDuration time){returnSpeed{len.value/time.value};}/// 速度 * 时间 长度constexprLengthoperator*(Speed spd,TimeDuration time){returnLength{spd.value*time.value};}constexprLengthoperator*(TimeDuration time,Speed spd){returnLength{spd.value*time.value};}现在可以做物理计算了voidtest_physics(){// 速度 距离 / 时间constexprautospeed100.0_m/10.0_s;// 10 m/sstatic_assert(speed.value10.0L);// 距离 速度 * 时间constexprautodistancespeed*60.0_s;// 600 米static_assert(distance.value600.0L);// 换算36 km/h 10 m/sconstexprautov136.0_km/1.0_h;// 36000 / 3600 10 m/sstatic_assert(v1.value10.0L);// 类型安全// auto bad 100_m 10_s; // 编译错误长度 时间// auto bad2 100_m * 10_s; // 编译错误长度 * 时间未定义}这段代码的美妙之处在于编译器帮你做了单位检查——你不可能不小心把毫秒当成秒用也不可能把速度和距离相加。第三步温度转换字面量温度是一个特殊的物理量因为不同温标之间不是简单的线性缩放——摄氏度和华氏度之间的转换包含偏移量。这正好是 UDL 的一个好用例structTemperatureTag{};usingTemperatureQuantitylongdouble,TemperatureTag;// 摄氏度以开尔文为基准存储constexprTemperatureoperator_degC(longdoublev){returnTemperature{v273.15L};}// 华氏度 - 开尔文constexprTemperatureoperator_degF(longdoublev){returnTemperature{(v-32.0L)*5.0L/9.0L273.15L};}// 开尔文constexprTemperatureoperator_degK(longdoublev){returnTemperature{v};}// 辅助函数从开尔文转换到各温标constexprlongdoubleto_celsius(Temperature t){returnt.value-273.15L;}constexprlongdoubleto_fahrenheit(Temperature t){return(t.value-273.15L)*9.0L/5.0L32.0L;}constexprlongdoubleto_kelvin(Temperature t){returnt.value;}使用voidtest_temperature(){constexprautot10.0_degC;// 冰点273.15 Kconstexprautot2100.0_degC;// 沸点373.15 Kconstexprautot332.0_degF;// 冰点华氏273.15 Kstatic_assert(to_kelvin(t1)273.15L);// 温度差可以相减在开尔文空间中constexprautodelta10.0_degC-0.0_degC;// 10Kstatic_assert(delta.value10.0L);// 摄氏 - 华氏constexprautobody_temp37.0_degC;// to_fahrenheit(body_temp) ≈ 98.6°F}这里我们用开尔文作为内部存储所有字面量在构造时都转换到开尔文。这样温度差就可以正确地相加减了。第四步字符串处理字面量UDL 不只能用于物理单位。在通用 C 开发中字符串处理字面量也很常用#includestring#includestring_view#includealgorithm#includecctype/// 编译期字符串哈希——用于高效的字符串比较constexprstd::uint32_toperator_hash(constchar*str,std::size_t len){std::uint32_thash2166136261u;for(std::size_t i0;ilen;i){hash(hash^static_caststd::uint8_t(str[i]))*16777619u;}returnhash;}/// 运行时转大写std::stringoperator_upper(constchar*str,std::size_t len){std::stringresult(str,len);std::transform(result.begin(),result.end(),result.begin(),[](unsignedcharc){returnstd::toupper(c);});returnresult;}/// 运行时 trim 空白std::stringoperator_trim(constchar*str,std::size_t len){std::string_viewsv(str,len);while(!sv.empty()std::isspace(sv.front()))sv.remove_prefix(1);while(!sv.empty()std::isspace(sv.back()))sv.remove_suffix(1);returnstd::string(sv);}voidtest_string_literals(){constexprautoidsensor_temp_hash;// 编译期整数autoupperhello world_upper;// HELLO WORLDautotrimmed padded _trim;// padded// 用于 switch-case比字符串比较高效constexprautocmdstart_hash;switch(cmd){casestart_hash:/* 启动 */break;casestop_hash:/* 停止 */break;default:break;}}字符串哈希字面量在嵌入式场景中特别有用——你可以用编译期生成的整数代替运行时字符串比较既节省 Flash不需要存储字符串又提升性能整数比较 vs 字符串比较。嵌入式实战在嵌入式开发中UDL 最实用的场景是频率/波特率字面量和寄存器地址字面量。来看具体例子。频率与波特率#includecstdintstructFrequency{std::uint32_thz;constexprstd::uint32_tto_hz()const{returnhz;}constexprstd::uint32_tto_khz()const{returnhz/1000;}/// 频率转周期纳秒constexprstd::uint64_tperiod_ns()const{return1000000000ULL/hz;}};constexprFrequencyoperator_Hz(unsignedlonglongv){returnFrequency{static_caststd::uint32_t(v)};}constexprFrequencyoperator_kHz(longdoublev){returnFrequency{static_caststd::uint32_t(v*1000.0)};}constexprFrequencyoperator_MHz(longdoublev){returnFrequency{static_caststd::uint32_t(v*1000000.0)};}/// 波特率寄存器计算STM32 USARTconstexprstd::uint16_tcompute_brr(Frequency periph_clock,Frequency baud){returnstatic_caststd::uint16_t(periph_clock.to_hz()/baud.to_hz());}voidconfigure_uart(){constexprautosysclk72.0_MHz;// 注意必须用浮点字面量constexprautobaud115200_Hz;// USART1-BRR compute_brr(sysclk, baud);// 生成的代码等价于直接写 USART1-BRR 625;constexprautobrrcompute_brr(sysclk,baud);static_assert(brr625,BRR calculation mismatch);}内存大小与静态断言structBytes{std::uint64_tvalue;constexprstd::uint64_tto_bytes()const{returnvalue;}};constexprBytesoperator_KiB(unsignedlonglongv){returnBytes{v*1024};}constexprBytesoperator_MiB(unsignedlonglongv){returnBytes{v*1024*1024};}// 编译期资源检查constexprautokFlashSize512_KiB;constexprautokAppSize256_KiB;constexprautokStackSize4_KiB;constexprautokRamSize128_KiB;static_assert(kAppSize.to_bytes()kFlashSize.to_bytes(),Application too large for flash!);static_assert(kStackSize.to_bytes()kRamSize.to_bytes(),Stack exceeds RAM!);这些static_assert在编译期就能发现资源分配的问题而不是等到运行时才发现 RAM 不够用。寄存器地址字面量在嵌入式裸机开发中寄存器操作非常频繁。虽然通常用 CMSIS 提供的宏来访问寄存器但如果你需要自定义外设或者调试时快速查看地址一个地址字面量可以增加可读性structRegisterAddress{std::uintptr_t addr;};constexprRegisterAddressoperator_reg(unsignedlonglongv){returnRegisterAddress{static_caststd::uintptr_t(v)};}// 使用voiddebug_example(){// STM32F103 USART1 基地址 0x40013800constexprautousart1_base0x40013800_reg;constexprautogpioa_base0x40010800_reg;// volatile auto* usart1_sr // reinterpret_castvolatile std::uint32_t*(usart1_base.addr);}练习实现一个长度单位系统作为这一篇的练习尝试自己实现一个完整的长度单位系统包含以下功能定义_m、_km、_mi英里三个字面量以米为基准单位支持加减运算和标量乘法支持长度除以时间得到速度用static_assert验证编译期计算的正确性参考框架#includecstdintstructMeterTag{};structSecondTag{};structSpeedTag{};templatetypenameT,typenameTagstructQuantity{T value;constexprexplicitQuantity(T v):value(v){}// TODO: 实现加、减、标量乘法、比较运算};usingLengthQuantitylongdouble,MeterTag;usingDurationQuantitylongdouble,SecondTag;usingSpeedQuantitylongdouble,SpeedTag;// TODO: 定义 _m, _km, _mi 字面量// TODO: 定义 _s 字面量// TODO: 实现 Length / Duration - Speed// 验证voidtest(){constexprautomarathon26.2_mi;// 英里转米// constexpr auto pace marathon / 4.0_h; // 配速米/小时// 注意需要先定义 _h 字面量才能使用// 提示1 英里 1609.344 米static_assert(marathon.value42000.0);}这个练习会帮你巩固模板、运算符重载、constexpr和 UDL 的组合使用。完成之后你就有了一个可以直接用在项目中的轻量级单位系统。参考资源cppreference: User-defined literalsBjarne Stroustrup: The C Programming Language, Chapter 18.6