每周技巧 #229:用于模板元编程的分级重载

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #229: Ranked Overloads for Template Metaprogramming

原文最初作为 TotW #229 发布于 2024 年 2 月 5 日。

作者:Miguel Young de la SotaMatt Kulukundis

更新于 2024 年 2 月 20 日。

快捷链接:abseil.io/tips/229

警告:这是给进行模板元编程的人看的高级技巧。一般来说,除非有非常非常好的理由,否则请避免模板元编程。如果你在读这篇文章,那是因为你需要做一些模板元编程,或者只是想学点有趣的东西。

一个很酷的小技巧

通常,C++ 要求每个函数调用解析到单个“最佳”函数,否则就产生歧义错误。“最佳”的准确定义比我们想展开的更复杂,涉及隐式转换、类型限定符等。

在会产生歧义错误的情况下,我们可以使用显式类层次结构,强制“最佳”的定义变成我们偏好的样子。这种“分级重载”技术使用具有类层次关系的结构体,让它们具有优先级顺序,编译器会先选择最高优先级方法。我们会定义一族相关的空标签类型 Rank0Rank1 等,让它们通过继承关联,并用它们引导重载解析过程。1

 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_viewstd::optionalstd::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

现在把它扩展到 vectoroptional 就相当直接了!

 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:这里和那里之间:几个容易忽视的小算法

下一节

  1. 注意,这里的 “rank” 使用的是口语含义,与整数和浮点值的转换等级无关。 ↩︎