SFINAE - 如果更复杂的失败,则回退到默认功能

Fuz*_*lla 6 c++ templates sfinae

说我写了一个名为的泛型函数interpolate.它的签名是这样的:

template<typename T>
T interpolate(T a, T b, float c);
Run Code Online (Sandbox Code Playgroud)

其中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
Run Code Online (Sandbox Code Playgroud)

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

hlt*_*hlt 5

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

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

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
}
Run Code Online (Sandbox Code Playgroud)

现在,我们需要找出以下标签类型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
Run Code Online (Sandbox Code Playgroud)

如果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))){});
}
Run Code Online (Sandbox Code Playgroud)

在这里,decltype(tag_for(static_cast<T*>(nullptr)))给我们正确的标签类型(作为正确重载的返回类型tag_for).

您可以添加额外的标记类型,只需很少的开销,并测试中的任意复杂条件enable_if_t.这个特定的版本只是C++ 17(因为is_same_v),但你可以通过使用typename std::enable_if<...>::typestd::is_same<...>::value不是简单地使它与C++ 11兼容- 它只是更加冗长.

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


作为奖励,这里是一个基于概念的实现,不再需要标记(如评论中简要提到的).不幸的是,目前还没有支持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
}
Run Code Online (Sandbox Code Playgroud)