每周技巧 #229:用于模板元编程的分级重载
本节阅读量:
本文翻译自 Abseil 官网的 Tip of the Week #229: Ranked Overloads for Template Metaprogramming。
原文最初作为 TotW #229 发布于 2024 年 2 月 5 日。
作者:Miguel Young de la Sota 和 Matt Kulukundis
更新于 2024 年 2 月 20 日。
快捷链接:abseil.io/tips/229
警告:这是给进行模板元编程的人看的高级技巧。一般来说,除非有非常非常好的理由,否则请避免模板元编程。如果你在读这篇文章,那是因为你需要做一些模板元编程,或者只是想学点有趣的东西。
一个很酷的小技巧
通常,C++ 要求每个函数调用解析到单个“最佳”函数,否则就产生歧义错误。“最佳”的准确定义比我们想展开的更复杂,涉及隐式转换、类型限定符等。
在会产生歧义错误的情况下,我们可以使用显式类层次结构,强制“最佳”的定义变成我们偏好的样子。这种“分级重载”技术使用具有类层次关系的结构体,让它们具有优先级顺序,编译器会先选择最高优先级方法。我们会定义一族相关的空标签类型 Rank0、Rank1 等,让它们通过继承关联,并用它们引导重载解析过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
// 带有良好注释的公共 API。
template <typename T>
size_t Size(const T& t);
// 下面所有内容是一个可工作的分级重载示例,
// 你可以复制粘贴作为起点!
namespace internal_size {
// 使用技巧 #229 进行分派。
struct Rank0 {};
struct Rank1 : Rank0 {};
struct Rank2 : Rank1 {};
struct Rank3 : Rank2 {};
template <typename T>
size_t SizeImpl(Rank3, const std::optional<T>& x) {
return x.has_value() ? Size(*x) : 0;
}
template <typename T>
size_t SizeImpl(Rank3, const std::vector<T>& v) {
size_t res = 0;
for (const auto& e : v) res += Size(e);
return res;
}
template <typename T>
size_t SizeImpl(Rank3, const T& t)
requires std::convertible_to<T, absl::string_view>
{
return absl::string_view{t}.size();
}
template <typename T>
size_t SizeImpl(Rank2, const T& x)
requires requires { x.length(); }
{
return x.length();
}
template <typename T>
size_t SizeImpl(Rank1, const T& x)
requires requires { x.size(); }
{
return x.size();
}
template <typename T>
size_t SizeImpl(Rank0, const T& x) { return 1; }
} // namespace internal_size
template <typename T>
size_t Size(const T& t) {
// 从最高等级 Rank3 开始。
return internal_size::SizeImpl(internal_size::Rank3{}, t);
}
auto i = Size("foo"); // 命中 string_view 重载
auto j = Size(std::vector<int>{1, 2, 3}); // 命中 vector 重载
auto k = Size(17); // 命中最低等级的“兜底”重载
|
注意,absl::string_view、std::optional 和 std::vector 重载都使用 Rank3。当重载互不兼容(调用按构造就不可能产生歧义)时,可以使用同一等级类型。你可以把同一等级的所有重载想成并行尝试。
注意:敏锐的读者可能会好奇为什么 absl::string_view 重载声明为模板。这样做能确保签名中除了 rank 结构体之外不会发生隐式转换。如果这个重载声明为接受 absl::string_view 参数,那么调用会产生歧义:Rank2{} -> Rank0{} 算作一次转换,而 const char[] 到 absl::string_view 也算一次,调用就会再次变得有歧义。
详细示例
假设我们希望 Size(x) 根据传入类型 x 实现了什么,返回 x.length()、x.size() 或 1。朴素做法不能工作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
template <typename T>
size_t Size(const T& x)
requires requires { x.length(); }
{
return x.length();
}
template <typename T>
size_t Size(const T& x)
requires requires { x.size(); }
{
return x.size();
}
template <typename T>
size_t Size(const T& x) { return 1; }
auto i = Size(std::string("foo")); // 有歧义。
|
因为 size 和 length 两个重载等级相同,它们对这次调用同样匹配。由于重载解析没有消除到只剩一个候选,编译器会声明调用点有歧义。可以用可变参数函数或 int/long 提升等聪明技巧为两个选项创建顺序,但这些技巧无法扩展到 N 个递减等级的重载。
按我们建议使用分级重载,会以继承形式给特定重载附加显式等级。这个等级基于以下规则:使用派生程度更高的类的重载,比使用较不具体类的重载等级更高。也就是说,如果两个重载只因一个实参类型不同,且两种类型都是该实参的基类,那么继承层次中距离最近的类型等级更高,是更好的匹配。
这意味着我们可以构建一座空结构体塔,每个结构体都派生自前一个,从而给每个重载一个显式数字等级。使用这个技巧,Size 可以写成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
// 带有良好注释的公共 API。
template <typename T>
size_t Size(const T& t);
namespace internal_size {
// 使用分级重载进行分派。
struct Rank0 {};
struct Rank1 : Rank0 {};
struct Rank2 : Rank1 {};
template <typename T>
size_t SizeImpl(Rank2, const T& x)
requires requires { x.length(); }
{
return x.length();
}
template <typename T>
size_t SizeImpl(Rank1, const T& x)
requires requires { x.size(); }
{
return x.size();
}
template <typename T>
size_t SizeImpl(Rank0, const T& x) { return 1; }
} // namespace internal_size
template <typename T>
size_t Size(const T& t) {
// 从最高等级开始
return internal_size::SizeImpl(internal_size::Rank2{}, t);
}
auto i = Size(std::string("foo")); // 3
|
现在可以把这些重载读成一个 if/else 链。首先尝试 Rank2 重载;如果替换失败,就退回下一个等级 Rank1,再到 Rank0。当然,这个特定方法会以不同方式处理 Size(std::string("foo")) 和 Size("foo")。这凸显了泛型编程的一些危险,不过修复相对直接:添加一个显式等级处理字符串,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// 带有良好注释的公共 API。
template <typename T>
size_t Size(const T& t);
namespace internal_size {
// 使用分级重载进行分派。
struct Rank0 {};
struct Rank1 : Rank0 {};
struct Rank2 : Rank1 {};
struct Rank3 : Rank2 {};
template <typename T>
size_t SizeImpl(Rank3, const T& t)
requires std::convertible_to<T, absl::string_view>
{
return absl::string_view{t}.size();
}
template <typename T>
size_t SizeImpl(Rank2, const T& x)
requires requires { x.length(); }
{
return x.length();
}
template <typename T>
size_t SizeImpl(Rank1, const T& x)
requires requires { x.size(); }
{
return x.size();
}
template <typename T>
size_t SizeImpl(Rank0, const T& x) { return 1; }
} // namespace internal_size
template <typename T>
size_t Size(const T& t) {
// 从最高等级开始
return internal_size::SizeImpl(internal_size::Rank3{}, t);
}
auto i = Size("foo"); // 3
|
现在把它扩展到 vector 和 optional 就相当直接了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
// 带有良好注释的公共 API。
template <typename T>
size_t Size(const T& t);
namespace internal_size {
// 使用分级重载进行分派。
struct Rank0 {};
struct Rank1 : Rank0 {};
struct Rank2 : Rank1 {};
struct Rank3 : Rank2 {};
template <typename T>
size_t SizeImpl(Rank3, const std::optional<T>& x) {
return x.has_value() ? Size(*x) : 0;
}
template <typename T>
size_t SizeImpl(Rank3, const std::vector<T>& v) {
size_t res = 0;
for (const auto& e : v) res += Size(e);
return res;
}
template <typename T>
size_t SizeImpl(Rank3, const T& t)
requires std::convertible_to<T, absl::string_view>
{
return absl::string_view{t}.size();
}
template <typename T>
size_t SizeImpl(Rank2, const T& x)
requires requires { x.length(); }
{
return x.length();
}
template <typename T>
size_t SizeImpl(Rank1, const T& x)
requires requires { x.size(); }
{
return x.size();
}
template <typename T>
size_t SizeImpl(Rank0, const T& x) { return 1; }
} // namespace internal_size
template <typename T>
size_t Size(const T& t) {
// 从最高等级开始
return internal_size::SizeImpl(internal_size::Rank3{}, t);
}
auto i = Size("foo"); // 命中 string_view 重载
auto j = Size(std::vector<int>{1, 2, 3}); // 命中 vector 重载
auto k = Size(17); // 命中最低等级的“兜底”重载
|
结语
既然你已经学会了这个强大能力,请记得谨慎使用。正如我们在 absl::string_view 重载中看到的,泛型编程很微妙,可能导致意外结果。
每周技巧 #227:小心空容器和无符号算术
上一节
每周技巧 #231:这里和那里之间:几个容易忽视的小算法
下一节