【问题标题】:Default fallback overload of a function template函数模板的默认回退重载
【发布时间】:2018-11-29 11:38:52
【问题描述】:

简短的一般性问题:

有没有办法提供函数模板的默认“回退”重载?我在 Stack Overflow 上阅读了一些技术,但其中一个需要使用可变参数函数 (f(...)),这在我的情况下有一些严重的缺点,而其他需要列出所有重载的类型组合,这太冗长且不自动化足以满足我的需要。有人问过这个问题,但现在已经有几年了,所以我想知道是否有一些新的解决方案可以使用最新标准的功能,甚至可能有一些在 C++20 中使用概念可以实现的解决方案。

详细的问题:

我正在尝试在 C++ 中实现动态类型。关键是,我有几个“抽象”类型,如BoolIntegerDecimalString 等,它们用一些内置类型表示,即Integer 存储为long long , 但除bool 之外的任何整数类型都会转换为它。

现在我开始实现运算符。我可以手动实现所有可能的重载(即Bool + IntegerInteger + Integer,...),但我希望每个“类别”只有一个实现,即Decimal + any_lower_type_than<Decimal> (commutative),其中any_lower_type_than<Integer> 指的是某些层次结构的抽象类型。我有所有需要的元功能来区分实现的类别和层次结构。然后我将 SFINAE 与这些元函数一起使用来提供定义。例如:

// decimal + lower, commutative:
template <typename first_t, typename second_t,
    std::enable_if_t<commutative_with_lower<first_t, second_t, Decimal>::value>* = nullptr>
Decimal operator+ (const first_t& first, const second_t& second) {
    return first + second;
}

// string * lower than integer, commutative:
template <typename first_t, typename second_t,
    std::enable_if_t<commutative_with_lower_than<first_t, second_t, String, Integer>::value>* = nullptr>
String operator* (const first_t& first, const second_t& second) {
    String string_arg = is_string<first_t>::value ? first : second;
    auto other_arg = is_string<first_t>::value ? second : first;
    String result = "";
    for (decltype(other_arg) i = 0; i < other_arg; ++i)
        result += string_arg;
    return result;
}

所以没关系,但我现在需要的是一些默认的“后备”实现,当找不到匹配项时,它会作为最后的手段被调用。所以我需要以某种方式强制回退实现成为最糟糕的匹配,但同时要匹配任何情况。

我读到可变参数函数“sinkhole”比任何东西都差。因此,自然的方法是提供作为operator+ (...) 的回退,这当然是不可能的。所以我将所有运算符更改为常规函数,例如operator_add 然后通过调用适当的operator_add 重载在更高级别实现operator+ 本身,其中默认重载实现为可变参数函数operator_add(...)。但后来我遇到了另一个问题。可变参数函数只接受平凡的类型。但我也需要将这种技术用于用户定义的类型。

所以问题是:对于可变参数函数 sinkhole,是否有任何替代技术可以确保最差的匹配?有没有办法在不修改函数参数的情况下做到这一点,也许通过修改模板参数,以便我可以在具有固定函数参数签名的实际运算符上使用该技术?

【问题讨论】:

  • 我认为你想用传入的 Concept 创建重载。

标签: c++ templates overloading


【解决方案1】:

所以问题是:是否有任何替代可变参数函数 sinkhole 的技术可以确保最差的匹配?

您可以使用层次结构对重载进行排序:

template <std::size_t N> struct overloadPriority : overloadPriority<N -1> {};
template <> struct overloadPriority<0>{};

然后

template <typename T>
std::enable_if_t<MyTrait5<T>::value> foo_impl(T&&, overloadPriority<5>) {/*..*/}

template <typename T>
std::enable_if_t<MyTrait4<T>::value> foo_impl(T&&, overloadPriority<4>) {/*..*/}

// ...

template <typename T>
void foo_impl(T&&, overloadPriority<0>) {/*..*/} // Fallback


template <typename T>
auto foo(T&& t)
{
    return foo_impl(std::forward<T>(t), overloadPriority<42>{});
    // number greater or equal to max overload_priority used by that overload.
}

【讨论】:

  • 我喜欢这个解决方案。它增加了很多对重载决议的显式控制。但是它对代码添加了很多更改,因此我选择了另一个作为正确答案,因为它是一种简单的插入式解决方案,可将最低优先级赋予单个重载。
【解决方案2】:

您可以简单地将后备定义为具有尾随参数包:

template <typename A, typename B, typename... T>
whatever_t operator_add(A&& a, B&& b, T...)
{
    …
}

重载总是更喜欢非模板而不是模板,并且更喜欢更专业的模板而不是不太专业的模板。基于[temp.deduct.partial]/11,如果两个函数模板在其他方面同样适用,则带有尾随参数包的函数模板将失败。

这种方法的唯一缺点是,如果您计划添加需要两个以上参数的 operator_add() 重载,则会出现潜在问题。此外,如果有人不小心传递了两个以上的参数,则会调用回退并吞下它们。不幸的是,由于 Jarod42 在 cmets 中提到的问题,我们不能简单地使用 std::enable_if 来消除重载,以防实际上碰巧有参数包的参数(就像我最初建议的那样)。为了防止意外调用,您可以做的一件事就是添加另一个在这种情况下将被调用的重载并将其定义为已删除:

template <typename A, typename B, typename C, typename... T>
void operator_add(A&&, B&&, C&&, T...) = delete;

【讨论】:

  • 这似乎是我需要的技巧。其他答案看起来也很有趣,所以取决于目前的情况,但这一个似乎是我的情况最简单的一个。
  • 不幸的是,它是格式错误的 NDR temp.res#8.3
  • @Jarod42 我不知道这条规则,感谢您指出这一点。虽然这很不幸,但enable_if 并不是真的那么必要,你可以简单地删除它。这只是为了保护您有一天拥有一个带有两个以上参数的operator_add 版本,或者您不小心用两个以上的参数调用它。只要你不这样做,没有enable_if,它仍然可以正常工作……
  • @McSim 我更新了我的答案以说明 Jarod42 指出的内容。
  • @Jarod42 出于好奇,您知道这条规则背后的具体原理是什么吗?我不确定:什么算作“有效的专业化”?在模板定义中使用static_assert(sizeof...(T) &gt; 0) 是否有效(如果模板将使用非空参数包实例化,这将使程序格式错误)?
【解决方案3】:

向您的operator_adds 添加另一个参数,包括回退。

给回退的额外参数一个不同的类型(比如,long),而不是其他的(比如,int)。

让您的 operator+ 使用 int 参数调用它,这样后备的转换效果会更差。

【讨论】:

  • 谢谢。这看起来很简单,但需要对所有重载进行更多更改。我会测试一下。
猜你喜欢
  • 2014-02-13
  • 2014-05-18
  • 2011-06-11
  • 1970-01-01
  • 2011-03-19
  • 2016-05-23
  • 2017-12-10
  • 1970-01-01
  • 2015-07-08
相关资源
最近更新 更多