[页眉: 30. 用户定义字面量]
1. 基本概念
1) C++ 中的各种字面量:[i:在 C++ 标准中一般认为存在以下五种字面量
a. 整型字面量:如 123, 12l, 11u, 7lu 等;
b. 浮点型字面量:如 1.23, 1.23f 等;
c. 布尔型字面量:如 true,false;
d. 字符型字面量:如 '1', 'a' 等;
e. 字符串字面量:如 "hello", "123" 等;
]
2) 传统字面量的痛点:[i:传统数字型字面量如 3600、3.14 等存在以下痛点
a. 隐藏了业务语义,导致代码缺乏可读性;「i:如代码中的 `if (dur > 86400)`、`int distance = 100` 等,其中的 86400 和 100 如果缺少必要的注释或明显的上下文解释,会让代码阅读者(包括未来的自己)无法立即理解这些数字的含义(如为啥 dur 大于 86400 就执行后面的操作?100 表示厘米还是米还是英尺等等);」
b. 仅基本类型支持使用字面量,自定义类型无法使用字面量、最多通过临时对象实现类似效果,但是写法仍然比较复杂;「i:例如定义了一个表示秒数的 seconds 类,我们希望存在类似“1s、1.23s”的形式来简洁的表示一个 seconds 对象,而不是每次都定义具名对象并初始化再使用,即便用临时对象`seconds(1)`、`seconds(1.23)`、也需要每次都书写构造函数调用`seconds()`;」
]
3) 什么是用户定义字面量:[i:用户定义字面量能同时携带数值和业务语义(如单位)信息;「i:例如定义了一个表示秒数的 seconds 类,则可以通过用户定义字面量让形如“1s、1.23s”的标识符分别表示 1s 和 1.23s,且让它们都是 seconds 的临时对象;」]
2. 用户定义字面量的使用
1) 用户定义字面量的基本使用(C++11):[b:
```c++
返回值类型 operator"" 后缀(类型 形参) {
statement
}
// 返回值类型指定了用户定义字面量表示的对象的类型,可以是自定义类型和基本类型
// operator"" 和后面的空格是固定写法
// 后缀可自定义、需满足标识符明明规则,一般只使用一个或少量字母,对于我们自己定义的后缀一般以单下划线做为开头,因为没下划线和双下划线一般是标准库的做法
// 形参用于接收用户定义字面量中的数值部分,形参类型只能是以下类型
unsigned long long(std::size_t) // 整型
long double // 浮点
const char* // 字符串
const wchar_t* // 宽字节字符串
const char16_t* // 16 位数序列
const char32_t* // 32 位数序列
// statement 用于对用户定义字面量中的数值进行处理、并返回此用户定义字面量真正代表的对象
```
「i:例如」「b:
```c++
// 假定基本数字类型(int, double 等)的单位为米
#include <iostream>
#include <cmath>
long double operator"" _m(long double count) {
return count;
}
long double operator"" _cm(long double count) {
return count / 100;
}
long double compute_distance(long double x1, long double y1, long double x2, long double y2) {
return ::sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
int main(int argc, char** argv) {
auto res1 = compute_distance(0.1, 0, 0.13, 0.04);
auto res2 = compute_distance(0.1_m, 0.0_m, 13.0_cm, 4.0_cm);
std::cout << res1 << " " << res2 << std::endl; // 返回 0.05 0.05
return 0;
}
```
「i:显然,使用用户定义字面量计算 res2 的可读性要比使用字面量计算 res1 的可读性强上不少,用户能一眼看出 compute_distance() 各个参数的含义;」
」
]
2) 编译时计算用户定义字面量的值(C++14):[i:用 constexpr 修饰 operator"" 即可;即][b:
```c++
constexpr 返回值类型 operator"" 后缀(类型 形参) {
statement
}
// 与 constexpr 修饰一般函数一样,对函数及其调用有以下限制
/*
* 1) 函数体限制:
* a. 所有局部变量都要初始化为编译时常量;
* b. 应尽量只包含一个 return 语句,因为多个 return 语句往往意味着要根据运行时条件决定要返回什么值;
* 2) 编译期求值:constexpr 函数只能包含编译期可求值的语句,不能包含运行期语句;
* 3) 返回类型和参数类型:constexpr 函数的返回类型和所有参数类型必须是字面值类型;
*/
// 其中 1) 和 3) 对于用户定义字面量自然满足,只需要注意 2) 即可;
```
「i:例如我们可以对前面求两个点之间距离的例子做以下优化」「b:
```c++
// 假定基本数字类型(int, double 等)的单位为米
#include <iostream>
#include <cmath>
constexpr long double operator"" _m(long double count) {
return count;
}
constexpr long double operator"" _cm(long double count) {
return count / 100;
}
long double compute_distance(long double x1, long double y1, long double x2, long double y2) {
return ::sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
int main(int argc, char** argv) {
auto res1 = compute_distance(0.1, 0, 0.13, 0.04);
auto res2 = compute_distance(0.1_m, 0.0_m, 13.0_cm, 4.0_cm);
std::cout << res1 << " " << res2 << std::endl; // 返回 0.05 0.05
return 0;
}
```
「i:同样,使用用户定义字面量计算 res2 的可读性要比使用字面量计算 res1 的可读性强上不少,用户能一眼看出 compute_distance() 各个参数的含义;」
」
]
3) 练习 - 让 1010_b 表示一个二进制数:[i:算法为——让数值依次除以 1、10、100、……每次得到的商如果是 1,就让结果加上 2 的次数次幂,如果商是 0,就什么都不做;][b:
```c++
constexpr unsigned long long operator"" _b(unsigned long long bin) {
unsigned long long res = 0;
unsigned long long base = 1; // 依次变成 1、10、100 ...
int i = 0; // 依次变成 1、2、3 ...
unsigned long long quotient = 0; // 记录 bin/base 的值
while ((quotient = bin / base) > 0) {
if (quotient % 10 == 1) {
res += ::pow(2, i);
}
else if (quotient % 10 == 0) {}
else {
// C++14
throw std::runtime_error("Binary literal contains non-binary digit (2-9)...");
}
++i;
base *= 10;
}
return res;
}
```
注意在 C++14 中,constexpr 修饰的函数可以抛出异常,但只能在运行时;即如果使用合法的用户定义字面量如 111_b,则每次走的都是 if 分支、所有语句都可以编译时计算,因此就会编译时计算 111_b 的值;但如果包含非二进制字符,如 21_b,则必然会走一次 else 分支,其中包含运行时才能执行的代码,因此 21_b 只会在运行时计算并抛出异常;
]