第一次在同事代码里看到 30_deg 这种写法时,我愣了一下——C++ 什么时候冒出来一个 deg 关键字?后来翻编译器的预处理器展开,发现这既不是宏也不是扩展,而是 C++11 就有的用户自定义字面量(User-Defined Literals)。当时的感觉是:这个特性被严重低估了。很多人知道怎么声明 operator""_xxx,但在真实工程里,它绝不只是“给数字起个别名”,而是能让一堆类型安全问题在编译期就消失的工具。这篇内容不聊基础 API 罗列,直接围绕“自定义字面量有多能打”展开:底层机制、编译期单位系统、非法值拦截、二进制字面量的手搓,顺带把那些新手踩了无数次的坑和坑背后的编译器原理一并说清。
1. 字面量自定义的底层机制:后缀不是魔法,是重载
1.1 先看一个最舒服的入门姿势:角度转换
假设你在写一个图形库,三角函数那一层天天被 sin(x) 的参数折磨。x 到底是弧度还是角度?一个人说 sin(90),另一个人说 sin(PI/2),同一个库两套语义,日志一旦打出数值根本没法判断是哪一套。这时候自定义字面量最直观的收益就出来了:
cpp复制#include <cmath>
constexpr long double operator"" _deg(long double deg) {
return deg * 3.14159265358979323846L / 180.0L;
}
constexpr long double operator"" _rad(long double rad) {
return rad;
}
// 调用侧
auto a = std::sin(90_deg);
auto b = std::sin(3.141592653589793L / 2.0L);
90_deg 看着就是 90 度,语义直接写进代码里了。这里的 _deg 后缀本质上是一个操作符函数的重载,编译器在词法分析阶段发现 90_deg 这个 token 后,会去作用域里找有没有匹配的 operator""_deg,然后按重载规则生成调用。它不是语法糖,是语言给用户留的一个“可编程字面量”插槽。
为什么这个例子值得先看?因为它解释了一个关键认知:自定义字面量不是“给数字取名字”,而是把字面量的类型转换时机提前到了编译期。constexpr 保证它在编译期就能算出数值,你甚至可以直接写在数组长度里:
cpp复制constexpr int kSize = 360_deg; // 编译期常量,没毛病
不过这里的 360_deg 会被隐式转换成 long double 再截断成 int,如果用在非 constexpr 上下文可能会丢失精度。这只是入门,真正的威力在后头。
1.2 编译器如何“看”这个后缀:cooked 与 raw 两条路线
要理解自定义字面量,关键是把编译器对字面量的处理拆成两段。一段叫 raw,字面量原始字符序列,例如 42 在预处理阶段就是字符 '4' 和 '2';另一段叫 cooked,即编译器的字面量解析器已经把它转换成了整数、浮点、字符、字符串等内部类型。
自定义字面量的操作符分两类,分别作用在这两条路线:
- cooked 版:接收已经转换好的类型。整数后缀接收
unsigned long long,浮点后缀接收long double,字符串后缀接收const char*和长度size_t。 - raw 版:直接接收原始字符序列。当某个后缀没有对应的 cooked 版本时,编译器会把字面量的字符数组原封不动地传给
operator""_xxx(const char*)。
举个例子:
cpp复制// cooked 版:会先按 long double 解析 3.14,再进入函数
constexpr long double operator"" _k(long double v) {
return v * 1000.0L;
}
// raw 版:拿到的是字符数组 "3.14",长度 4
constexpr long double operator"" _kraw(const char* s) {
// 解析字符串
return 0.0L;
}
如果同时定义了 operator""_k(long double) 和 operator""_k(const char*),整数和浮点字面量会优先匹配 cooked 版本;只有 cooked 版本不存在时,编译器才会退回 raw 版本去读字符数组。字符串字面量没有 raw 一说,因为字符串天然就是字符序列,它只会匹配 (const char*, size_t) 版本。
下面这张表把匹配规则收拢一下,方便查阅:
| 字面量形式 | cooked 版本的签名 | raw 版本签名(仅无 cooked 时) |
|---|---|---|
整数,如 42_x |
operator""_x(unsigned long long) |
operator""_x(const char*) |
浮点,如 3.14_x |
operator""_x(long double) |
operator""_x(const char*) |
字符,如 'c'_x |
operator""_x(char) |
无 |
| 字符串,如 `"st |
