代码存储位置:shanchuann/Modern_CPP

在 C++ 中,用户定义字面量(User-Defined Literals ,简称 UDL) 是 C++11 引入的特性,允许开发者为特定场景定义自定义的字面量形式。通过 UDL,我们可以为数值、字符串等添加有意义的后缀(如 100m 表示 100 米、30s 表示 30 秒),让代码更直观、可读性更强。

从一个例子开始

类似于定义一个运算符重载

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
void operator""_kg(const char* str, size_t) {
cout << str << " 千克" << endl;
}

int main() {
"10.5"_kg; // 调用自定义字面量 //10.5 千克
return 0;
}

同样的,我们可以将他的返回值改为 string 类型

1
2
3
4
5
6
7
8
string operator""_kg(const char* str, size_t) {
return string(str) + " 千克";
}

int main() {
cout << "10"_kg; // 调用自定义字面量 //10 千克
return 0;
}

我们很快发现用户(也就是我)定义的后缀均由_开始,这是因为:

用户定义后缀(ud-suffix)必须以下划线 _ 开头:不以下划线开头的后缀保留给标准库提供的字面量运算符。后缀也不能包含双下划线 __:此类后缀同样为保留项。

标准库的用户定义后缀

BTW,标准库的用户定义后缀不以_开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_EXPORT_STD _NODISCARD _CONSTEXPR20 string operator""s(const char* _Str, size_t _Len) {
return string{_Str, _Len};
}
//例如:`s`表示秒,`h`表示小时,`kg`表示千克,`m`表示米,`cm`表示厘米,`mm`表示毫米,`km`表示千米,`g`表示克等。
#include<chrono>
using namespace std::literals::chrono_literals;

int main() {
auto str = "hello world"s; // 使用标准库的用户定义字面量
cout << str << endl;
auto duration = 10min; // 使用标准库的用户定义字面量
cout << duration.count() << " 分钟" << endl;
return 0;
}

参数列表

当我们阅读 chrono_literals 源码时会发现,形式参数里并没有使用 int,double 等数据类型,这是因为字面量运算符仅允许以下参数列表:

参数列表 序号 说明
(const char*) (1)
(unsigned long long int) (2)
(long double) (3)
(char) (4)
(wchar_t) (5)
(char8_t) (6) (C++20 起)
(char16_t) (7)
(char32_t) (8)
(const char*, std::size_t) (9)
(const wchar_t*, std::size_t) (10)
(const char8_t*, std::size_t) (11) (C++20 起)
(const char16_t*, std::size_t) (12)
(const char32_t*, std::size_t) (13)

1)具有此参数列表的字面量运算符是原始字面量运算符,用作用户定义整数和浮点字面量的后备(见上文)

2)具有此参数列表的字面量运算符是用户定义整数字面量的首选运算符

3)具有此参数列表的字面量运算符是用户定义浮点字面量的首选运算符

4-8)具有此参数列表的字面量运算符由用户定义字符字面量调用

9-13)具有此参数列表的字面量运算符由用户定义字符串字面量调用

不允许默认参数。不允许 C 语言链接。

除上述限制外,字面量运算符和字面量运算符模板与普通函数(和函数模板)无异:可声明为 inlineconstexpr、具有内部或外部链接、可被显式调用、可获取其地址等。

1
2
3
4
5
6
7
8
string operator""_i(size_t size) {
return to_string(size);
}

int main() {
cout << 10_i << endl; // 调用自定义字面量
return 0;
}

size_t 便是由 unsigned long long typedef 而来

例子

实现 “{} {}”_f(a,b..) 等价于 std::format("{} {}",a,b)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//.cpp
// 引入包含 _f 字面量的命名空间
using namespace my::literals;

int main() {
// 现在可以正确识别 _f 后缀
cout << "The Grade is: {}"_f("A") << endl;
cout << "{} {}"_f(5, 5.6);
return 0;
}
//.h
namespace my {
struct A {
const char* str;
constexpr A(const char* s) : str(s) {}
};
namespace literals {
template<A a>
constexpr auto operator""_f() {
return[=]<typename... T>(T... Args) { return format(a.str, Args...); };
}
}
}

输出结果:

1
2
The Grade is: A
5 5.6

很奇怪,虽然程序可以正常运行,但是 VS 有错误提示

这是因为 C++ 对非类型模板参数的类型有严格限制,而 my::A 类型不符合这些限制条件。

C++ 标准规定,非类型模板参数(即模板参数不是类型,而是具体的值)必须是以下类型之一:

  1. 算术类型(整数、浮点数等)
  2. 枚举类型
  3. 指针类型(函数指针、对象指针)
  4. 引用类型
  5. std::nullptr_t
  6. C++20 起允许的一些字面类型(literal type)的聚合体

很显然 struct A默认情况下不满足非类型模板参数的要求,因为它不是 C++ 标准明确允许的基础类型,即使在 C++20 中,自定义类型作为非类型模板参数也需要满足严格的 “字面类型” 条件(如所有成员都是 public 且可 constexpr 初始化等)

正是因为 template<A a> 中使用 A 作为非类型模板参数的类型违反了上述规则,编译器才会报错。

但代码可以正常运行通常是编译器对非标准语法有一定的 “宽容度” 或扩展支持,会尝试编译不符合标准的代码。