【问题标题】:SFINAE - Falling back on default function if more sophisticated one failsSFINAE - 如果更复杂的功能失败,则回退到默认功能
【发布时间】:2018-05-19 22:14:14
【问题描述】:

假设我编写了一个名为interpolate 的通用函数。它的签名是这样的:

template<typename T>
T interpolate(T a, T b, float c);

其中 a 和 b 是要在其间进行插值的值,c 是 [0.0,1.0] 中的浮点数。

如果 T 定义了 T operator*(float)T operator+(T),我希望它以某种方式表现(线性插值)。否则,它的行为会有所不同——任何T 都是可用的(最近邻插值)。

我怎样才能实现这种行为?

例如:

interpolate<std::string>("hello","world!", 0.798); //uses nearest neighbor, as std::string does not have the necessary operators

interpolate<double>(42.0,128.0, 0.5);              //uses linear, as double has the needed operators

注意:这个问题不是关于这些插值方法的实现,而是如何使用模板来切换函数的行为。

【问题讨论】:

    标签: c++ templates sfinae


    【解决方案1】:

    这听起来像是 标签调度的主要用例

    我们创建了两个不同的标签类来区分这两个用例

    struct linear_tag {};
    struct nn_tag {};
    
    template <typename T>
    T impl(T a, T b, float c, linear_tag) {
        // linear interpolation here
    }
    
    template <typename T>
    T impl(T a, T b, float c, nn_tag) {
        // nearest neighbor interpolation here
    }
    

    现在,我们需要从T中找出标签类型:

    template <typename T>
    linear_tag tag_for(
        T* p,
        std::enable_if_t<std::is_same_v<T, decltype((*p + *p) * 0.5)>>* = nullptr
    );
    nn_tag tag_for(...); // Fallback
    

    第一个重载仅在对于任何T t,表达式(t + t) * 0.5f 返回另一个T 时才存在。1 第二个重载始终存在,但由于 C 风格的可变参数, 除非第一个重载不匹配,否则永远不会使用它。

    然后,我们可以通过创建适当的标签来分派到任一版本:

    template <typename T>
    T interpolate(T a, T b, float c) {
        return impl(a, b, c, decltype(tag_for(static_cast<T*>(nullptr))){});
    }
    

    这里,decltype(tag_for(static_cast&lt;T*&gt;(nullptr))) 为我们提供了正确的标签类型(作为 tag_for 正确重载的返回类型)。

    您可以以很少的开销添加其他标记类型,并在enable_if_t 中测试任意复杂的条件。这个特定版本仅是 C++17(因为 is_same_v),但您可以通过使用 typename std::enable_if&lt;...&gt;::typestd::is_same&lt;...&gt;::value 轻松使其与 C++11 兼容 - 它只是有点冗长。

    1 这是您在问题中指定的内容 - 但它很危险!例如,如果您使用整数,您将使用最近邻插值,因为* 返回float,而不是int。您应该使用诸如std::is_constructible_v&lt;T, decltype((*t + *t) * 0.5f)&gt; 之类的测试来测试表达式(*t + *t) * 0.5f 是否返回可可转换T 的内容。


    作为奖励,这是一个基于 概念的实现,不再需要标签(如 cmets 中简要提到的)。不幸的是,目前还没有编译器支持这个级别的requires,当然标准草案总是会发生变化:

    template <typename T>
    concept LinearInterpolatable = requires(T a, T b, float c) {
        { a + b } -> T;
        { a * c } -> T;
    };
    
    template <LinearInterpolatable T>
    T interpolate(T a, T b, float c)
    {
        // Linear interpolation
    }
    
    template <typename T>
    T interpolate(T a, T b, float c)
    {
        // Nearest-neighbor interpolation
    }
    

    【讨论】:

    • 哇!这比我预期的要复杂得多!非常感谢您写下这篇文章 - 我可以从您在这里向我展示的内容中学到很多东西 :)
    • 这是一个极其强大和灵活的特性——标准库通过iterator_category使用它来区分具有不同特性集的不同类型的迭代器
    • 如果你使用 C++17,你也可以使用 if constexpr 而不是 impl 的重载函数
    • 这是可能的,但需要为标签类型添加一个额外的模板,此时我认为让编译器进行类型相等检查会更干净。使用 C++20,我们将获得概念(也许),这有望让我们在没有整个调度部分的情况下做到这一点。
    • 你为什么在decltype(tag_for((T*) nullptr))中使用C风格的转换?
    【解决方案2】:

    可以为重载函数提供优先顺序。如果重载的数量很少,您可以使用:

    using prefer_overload_t = int;
    using backup_overload_t = long;
    
    template <typename T>
    auto interpolate_impl(T a, T b, float c, prefer_overload_t)
        -> std::enable_if_t<
               std::is_same_v<T, decltype(a * c)>
               && std::is_same_v<T, decltype(a + b)>,
               T
           >
    {
        // linear interpolation
    }
    
    template <typename T>
    T interpolate_impl(T a, T b, float c, backup_overload_t)
    {
        // nearest neighbor
    }
    
    template<typename T>
    T interpolate(T a, T b, float c)
    {
        return interpolate_impl(std::move(a), std::move(b), c, prefer_overload_t());
    }
    

    由于从intint 不需要转换,因此首选前一个重载,但当它不起作用时会被 SFINAE 淘汰。


    如果您想订购任意数量的重载,则必须使用一些特殊类型,如下所示:

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

    那么,rank&lt;N&gt; 将优先于 rank&lt;N - 1&gt;

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-03-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-25
      • 2021-10-05
      • 2012-08-18
      • 1970-01-01
      相关资源
      最近更新 更多