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

SFINAE - Falling back on default function if more sophisticated one fails

本文关键字:功能 回退 默认 失败 SFINAE 如果 复杂      更新时间:2023-10-16

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

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

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

Iff 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

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

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

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

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) * 0.5f返回另一个T时,第一个重载才存在,对于任何T t1第二个重载始终存在,但由于 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<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。相反,您应该使用诸如std::is_constructible_v<T, decltype((*t + *t) * 0.5f)>之类的测试来测试表达式(*t + *t) * 0.5f是否返回可转换T

的内容

作为奖励,这里有一个基于 c++20 概念的实现,它不再需要标签(如评论中简要提到的(。不幸的是,目前还没有编译器支持此级别的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
}

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

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());
}

由于它不需要从int转换为int,因此前者重载是首选,但是当它不起作用时,SFINAE会退出。


如果要对任意数量的重载进行排序,则必须使用一些特殊类型,如下所示:

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

然后,rank<N>rank<N - 1>更可取。