SFINAE - 如果更复杂的函数失败,则返回默认函数
SFINAE - Falling back on default function if more sophisticated one fails
假设我编写了一个名为 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
注意:本题不是关于这些插值方法的实现,而是如何使用模板来切换函数的行为。
这听起来像是 标签调度的主要用例:
我们创建了两个不同的标签类来区分这两个用例
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
return 另一个 T
.1[=57= 时,第一个重载才存在] 第二个重载始终存在,但由于 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
的正确重载的 return 类型)。
您可以以很少的开销添加额外的标记类型,并在 enable_if_t
中测试任意复杂的条件。此特定版本仅适用于 C++17(因为 is_same_v
),但您可以通过使用 typename std::enable_if<...>::type
和 std::is_same<...>::value
轻松使其与 C++11 兼容 - 它只是有点冗长。
1 这就是您在问题中指定的内容 - 但它很危险!例如,如果您使用整数,您将使用最近邻插值,因为 *
returns float
,而不是 int
。您应该使用诸如 std::is_constructible_v<T, decltype((*t + *t) * 0.5f)>
作为奖励,这里有一个 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>
。
假设我编写了一个名为 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
注意:本题不是关于这些插值方法的实现,而是如何使用模板来切换函数的行为。
这听起来像是 标签调度的主要用例:
我们创建了两个不同的标签类来区分这两个用例
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
return 另一个 T
.1[=57= 时,第一个重载才存在] 第二个重载始终存在,但由于 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
的正确重载的 return 类型)。
您可以以很少的开销添加额外的标记类型,并在 enable_if_t
中测试任意复杂的条件。此特定版本仅适用于 C++17(因为 is_same_v
),但您可以通过使用 typename std::enable_if<...>::type
和 std::is_same<...>::value
轻松使其与 C++11 兼容 - 它只是有点冗长。
1 这就是您在问题中指定的内容 - 但它很危险!例如,如果您使用整数,您将使用最近邻插值,因为 *
returns float
,而不是 int
。您应该使用诸如 std::is_constructible_v<T, decltype((*t + *t) * 0.5f)>
作为奖励,这里有一个 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>
。