本周小贴士#229:模板元编程的分级重载

作为TotW#229最初始发表于2024年2月5日

由 Miguel Young de la Sota 和 Matt Kulukundis 创作

警告:这是给做模板元编程的人的高级提示。一般来说,除非有非常非常好的理由,否则应避免使用模板元编程。如果你正在阅读这篇文章,那是因为你需要做一些模板元编程,或者只是想学一些漂亮的东西。

一个很酷的技巧

通常,c++要求每个函数调用都解析为单个“最佳”函数,否则就会产生歧义错误。“最佳”的确切定义比我们想要讨论的要复杂得多,但它涉及到隐式转换和类型限定符等内容。

在可能产生歧义错误的情况下,我们可以使用显式的类层次结构来强制使用我们喜欢的“best”定义。这种“分级重载”技术使用具有类层次结构的结构,因此它们具有优先级排序,编译器将首先选择优先级最高的方法。我们将定义一系列空标记类型Rank0、Rank1等,它们通过继承相互关联,并使用它们来指导重载解析过程。

// Public API with good comments.
template <typename T>
size_t Size(const T& t);

// Everything below here is a working example of ranked overloads, that
// you can copy and paste to get you started!
namespace internal_size {

// Use go/ranked-overloads for dispatching.
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) {
  // Start with the highest rank, Rank3.
  return internal_size::SizeImpl(internal_size::Rank3{}, t);
}

auto i = Size("foo");                      // 匹配 string_view 重载
auto j = Size(std::vector<int>{1, 2, 3});  // 匹配 the vector 重载
auto k = Size(17);                         // 匹配最底层重载

注意absl::string_view、std::optional和std::vector重载使用Rank3。当重载相互不兼容时(调用不能通过构造实现二义性),可以使用相同的秩类型。您可以将所有具有相同等级的重载看作是并行尝试。
注意:精明的读者可能想知道为什么要将absl::string_view重载声明为模板。这样做可以确保签名中不会发生除秩结构之外的隐式转换。如果使用absl::string_view参数声明此重载,则调用将是二义性的。

详细示例

假设我们希望Size(x)返回x.length()、x. Size()或1,具体取决于传递的类型x实现了什么。这种天真的方法行不通:

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:

// Public API with good comments.
template <typename T>
size_t Size(const T& t);

namespace internal_size {

// Use go/ranked-overloads for dispatching.
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) {
  // Start with the highest rank
  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”)区别对待。这突出了泛型编程的一些危险,尽管修复方法相对简单:添加显式的秩来处理字符串,如下所示。

// Public API with good comments.
template <typename T>
size_t Size(const T& t);

namespace internal_size {
// Use go/ranked-overloads for dispatching.
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) {
  // Start with the highest rank
  return internal_size::SizeImpl(internal_size::Rank3{}, t);
}

auto i = Size("foo");  // 3

现在将其扩展到vector和optional是非常简单的!

// Public API with good comments.
template <typename T>
size_t Size(const T& t);

namespace internal_size {
// Use go/ranked-overloads for dispatching.
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) {
  // Start with the highest rank
  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重载中看到的那样,泛型编程很微妙,可能导致意想不到的结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值