在C++中使用SFINAE的方法

50
我在一个项目中大量使用SFINAE函数,但不确定以下两种方法之间是否有任何区别(除了风格之外)。
#include <cstdlib>
#include <type_traits>
#include <iostream>

template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{
    std::cout << "method 1" << std::endl;
}

template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{
    std::cout << "method 2" << std::endl;
}

int main()
{
    foo<int>();
    foo<double>();

    std::cout << "Done...";
    std::getchar();

    return EXIT_SUCCESS;
}

程序的输出结果如预期所示:
method 1
method 2
Done...

我在stackoverflow上经常看到使用方法2的情况,但我更喜欢方法1。
这两种方法有什么不同的情况吗?

你怎么运行这个程序?[它对我来说无法编译。] (http://coliru.stacked-crooked.com/a/c6e0966c7f83000b) - alter_igel
@alter igel 需要 C++17 编译器。我使用 MSVC 2019 测试了此示例,但我主要使用 Clang 工作。 - keith
相关:为什么应该避免在函数签名中使用std :: enable_if,而C++20也引入了新的概念方式 :-) - Jarod42
@Jarod42 概念是我从C++20最需要的东西之一。 - val is still with Monica
2个回答

46

我在stackoverflow上看到更多人使用方法2,但我更喜欢方法1。

建议:推荐使用方法2。

这两种方法都适用于单个函数。问题出现在当你有多个具有相同签名的函数时,你只想启用其中一个函数。

假设你想在bar<T>()(假装它是一个constexpr函数)为true时启用版本1的foo(),并在bar<T>()false时启用版本2的foo()

使用

template <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1
 { }

template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2
 { }

由于存在模棱两可的情况,即有两个具有相同签名的 foo() 函数(默认模板参数不会改变函数签名),所以您会出现编译错误。

但是以下解决方案可以解决这个问题。

template <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1
 { }

template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2
 { }

这段代码之所以能够正常工作,是因为SFINAE修改了函数的签名。

另外一个观察结果是:还有第三种方法,即启用/禁用返回类型(当然,对于类/结构体的构造函数除外)。

template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1
 { }

template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2
 { }

作为第二种方法的替代,第三种方法支持选择具有相同签名的备用函数。

1
感谢您的清晰解释,从现在开始我会更倾向于使用方法2和3 :-) - keith
“默认模板参数不会改变签名” - 在您的第二个变量中,使用默认模板参数有何不同?请解释。 - Eric
1
@Eric - 不好简单说...我想其他答案解释得更好...如果SFINAE启用/禁用默认模板参数,则在使用显式第二个模板参数(foo<double, double>();调用)调用时,foo()函数仍然可用。如果仍然可用,则与其他版本存在歧义。使用方法2,SFINAE启用/禁用第二个参数,而不是默认参数。因此,您无法显式调用它,因为存在替换失败,不允许第二个参数。因此,该版本不可用,因此没有歧义。 - max66
3
方法三的另一个优点通常不会泄漏到符号名称中。 auto foo()-> std :: enable_if_t <...> 变体通常有助于避免隐藏函数签名并允许使用函数参数。 - Deduplicator
@max66:所以关键点在于,如果提供了参数并且不需要默认值,则模板参数默认情况下的替换失败不是错误吗? - Eric
@Eric - 是的...在我看来,你抓住了重点:模板参数默认值的替换失败仅禁用不表达该参数但保持函数本身可用的函数调用。方法2则禁用该函数本身。 - max66

23
除了max66的答案之外,更喜欢方法2的另一个原因是,使用方法1,你可以(无意中)将显式类型参数作为第二个模板参数传递,并完全破坏SFINAE机制。这可能会发生作为一个打字错误、复制/粘贴错误或者在一个较大的模板机制中的疏忽。
#include <cstdlib>
#include <type_traits>
#include <iostream>

// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){
    std::cout << "method 1" << std::endl;
}

int main(){

    // works fine
    foo<int>();

    // ERROR: subsitution failure, as expected
    // foo<double>();

    // Oops! also works, even though T != int :(
    foo<double, double>();

    return 0;
}

Live demo here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接