std::enable_if用于条件编译成员函数。

197
我正在尝试找一个简单的例子来理解如何使用std::enable_if。在阅读了this answer之后,我认为应该不难想出一个简单的例子。我想使用std::enable_if来选择两个成员函数,并且只允许使用其中一个。
不幸的是,下面的代码在gcc 4.7上无法编译,经过数个小时的尝试之后,我想请教你们我犯了什么错误。
#include <utility>
#include <iostream>

template< class T >
class Y {

    public:
        template < typename = typename std::enable_if< true >::type >
        T foo() {
            return 10;
        }
        template < typename = typename std::enable_if< false >::type >
        T foo() {
            return 10;
        }

};


int main() {
    Y< double > y;

    std::cout << y.foo() << std::endl;
}

gcc报告了以下问题:
% LANG=C make CXXFLAGS="-std=c++0x" enable_if
g++ -std=c++0x    enable_if.cpp   -o enable_if
enable_if.cpp:12:65: error: `type' in `struct std::enable_if<false>' does not name a type
enable_if.cpp:13:15: error: `template<class T> template<class> T Y::foo()' cannot be overloaded
enable_if.cpp:9:15: error: with `template<class T> template<class> T Y::foo()'

为什么g++不删除第二个成员函数的错误实例化? 根据标准,std::enable_if< bool, T = void >::type只有在布尔模板参数为true时才存在。 但是为什么g++不将此视为SFINAE? 我认为重载错误消息来自于g++不删除第二个成员函数并认为这应该是一个重载的问题。

1
我不确定,但我认为它是基于SFINAE(替换失败不是错误)的enable_if。然而,在这里你没有任何替换,因为没有参数可以用来确定使用哪个重载。你应该让“true”和“false”取决于T。(我知道你不想在简单的例子中这样做,但现在可能太简单了...) - Philipp
3
我也考虑过并尝试使用 std::is_same< T, int >::value! std::is_same< T, int >::value,它们会给出相同的结果。 - evnu
9个回答

137
SFINAE仅在模板参数的参数推断中进行替换使构造函数无效时才起作用。但是没有这样的替换。
引用块中提到了尝试使用`std::is_same ::value`和`!std::is_same ::value`来检查类型是否相同,这两种方法得到相同的结果。
这是因为当类模板被实例化(例如创建`Y `类型的对象)时,它会实例化其所有成员声明(不一定是定义/主体!)。其中包括其成员模板。请注意,此时已知`T`,并且`!std::is_same ::value`为false。因此,它将创建一个包含`int`的类`Y`。
class Y<int> {
    public:
        /* instantiated from
        template < typename = typename std::enable_if< 
          std::is_same< T, int >::value >::type >
        T foo() {
            return 10;
        }
        */

        template < typename = typename std::enable_if< true >::type >
        int foo();

        /* instantiated from

        template < typename = typename std::enable_if< 
          ! std::is_same< T, int >::value >::type >
        T foo() {
            return 10;
        }
        */

        template < typename = typename std::enable_if< false >::type >
        int foo();
};
std::enable_if<false>::type是访问不存在类型的,因此声明是不合法的。这样你的程序就无效了。
您需要使成员模板的enable_if依赖于成员模板本身的参数。然后,声明将是有效的,因为整个类型仍然是相关的。当您尝试调用它们之一时,它们的模板参数进行参数推导并按预期发生SFINAE。请参见这个问题以及相应的答案,了解如何做到这一点。

20
需要翻译的内容:...只是想澄清一下,以防有用的话:当实例化 Y 模板类的一个实例时,编译器实际上不会编译模板成员函数;然而,编译器将对成员模板声明中的 T 进行替换,以便稍后可以实例化这些成员模板。这个失败点不是 SFINAE,因为 SFINAE 仅适用于确定 重载解析 的可能函数集时,并且实例化类不属于确定重载解析函数集的情况。(或者我是这样认为的!) - Dan Nissenbaum

117

我制作了这个短的例子,它也能正常工作。

#include <iostream>
#include <type_traits>

class foo;
class bar;

template<class T>
struct is_bar
{
    template<class Q = T>
    typename std::enable_if<std::is_same<Q, bar>::value, bool>::type check()
    {
        return true;
    }

    template<class Q = T>
    typename std::enable_if<!std::is_same<Q, bar>::value, bool>::type check()
    {
        return false;
    }
};

int main()
{
    is_bar<foo> foo_is_bar;
    is_bar<bar> bar_is_bar;
    if (!foo_is_bar.check() && bar_is_bar.check())
        std::cout << "It works!" << std::endl;

    return 0;
}

如果需要我进行详细说明,请留言评论。我认为代码基本上是不言自明的,但我做出来的,可能会有错 :)

你可以在这里看到它的实际效果


2
这在VS2012上无法编译。错误C4519:默认模板参数仅允许在类模板上使用 - PythonNut
2
很不幸,我只在gcc上测试过它。也许这可以帮助你:https://dev59.com/VHE95IYBdhLWcg3wHqQV#17543296 - jpihl
8
为什么需要创建另一个模板类Q,即使它等同于T? - ilya1725
3
因为你需要为test成员函数提供模板,两者不能同时存在。Q只是将类模板类型T转发。你可以像这样删除类模板T:http://cpp.sh/4nxw ,但这有点违背了初衷。 - jpihl
1
如果你卡在 C++ < 11(如我的情况,Eigen 核心库开发),你也无法使用默认模板参数(如在 VS2012 中)。但有一个解决方法。省略默认模板参数,相反地向函数特化添加一个 Q* 参数。然后创建一个新函数来调用这些适配的函数,向它们传递一个额外的 Q* 类型参数,例如 (Q*)NULL。在此处查看示例:http://cpp.sh/3d6uj(不要忘记检查 C++98 编译器选项)。 - Martin Pecka
显示剩余10条评论

18
对于那些寻求“只管好用”的解决方案的后来者:
#include <utility>
#include <iostream>

template< typename T >
class Y {

    template< bool cond, typename U >
    using resolvedType  = typename std::enable_if< cond, U >::type; 

    public:
        template< typename U = T > 
        resolvedType< true, U > foo() {
            return 11;
        }
        template< typename U = T >
        resolvedType< false, U > foo() {
            return 12;
        }

};


int main() {
    Y< double > y;

    std::cout << y.foo() << std::endl;
}

编译方式:

g++ -std=gnu++14 test.cpp 

运行会产生以下结果:

./a.out 
11

8
为什么要将 std::enable_if_t 重命名为 resolvedType - Qwertie
5
由于各种原因,并非每个人都能使用C++17。 - James Yang
这个答案难道不和上面的答案一模一样吗?(还请看下面的评论,可能存在标准违规情况) - user202729

12

来自这篇文章:

默认模板参数并非模板的签名的一部分。

但是可以这样做:

#include <iostream>

struct Foo {
    template < class T,
               class std::enable_if < !std::is_integral<T>::value, int >::type = 0 >
    void f(const T& value)
    {
        std::cout << "Not int" << std::endl;
    }

    template<class T,
             class std::enable_if<std::is_integral<T>::value, int>::type = 0>
    void f(const T& value)
    {
        std::cout << "Int" << std::endl;
    }
};

int main()
{
    Foo foo;
    foo.f(1);
    foo.f(1.1);

    // Output:
    // Int
    // Not int
}

它可以工作,但基本上是模板函数,而不是类本身...... 它也不允许放弃其中一个具有相同原型的函数(当您需要传递重载时)。 然而,这个想法很好。 请问您能否以可行的形式重新编写OP示例吗? - user1284631
这对我来说失败了;无论如何,在某些地方使用typename而不是class(根据链接的答案)可以解决问题。(除非您使用一些不寻常的编译器版本?) - user202729

6

解决这个问题的一种方法是,将成员函数的特化放入另一个类中,然后从该类继承。您可能需要更改继承的顺序以访问所有其他基础数据,但这种技术确实有效。

template< class T, bool condition> struct FooImpl;
template<class T> struct FooImpl<T, true> {
T foo() { return 10; }
};

template<class T> struct FoolImpl<T,false> {
T foo() { return 5; }
};

template< class T >
class Y : public FooImpl<T, boost::is_integer<T> > // whatever your test is goes here.
{
public:
    typedef FooImpl<T, boost::is_integer<T> > inherited;

    // you will need to use "inherited::" if you want to name any of the 
    // members of those inherited classes.
};

这种技术的缺点在于,如果您需要为不同的成员函数测试许多不同的内容,则必须为每个内容创建一个类,并将其链接到继承树中。这对于访问公共数据成员也是如此。
例如:
template<class T, bool condition> class Goo;
// repeat pattern above.

template<class T, bool condition>
class Foo<T, true> : public Goo<T, boost::test<T> > {
public:
    typedef Goo<T, boost::test<T> > inherited:
    // etc. etc.
};

5
布尔值需要依赖于推断出的模板参数。因此,修复的简单方法是使用默认布尔参数:
template< class T >
class Y {

    public:
        template < bool EnableBool = true, typename = typename std::enable_if<( std::is_same<T, double>::value && EnableBool )>::type >
        T foo() {
            return 10;
        }

};

然而,如果你想重载成员函数,这种方法行不通。相反,最好使用TICK_MEMBER_REQUIRES来自于Tick库:

template< class T >
class Y {

    public:
        TICK_MEMBER_REQUIRES(std::is_same<T, double>::value)
        T foo() {
            return 10;
        }

        TICK_MEMBER_REQUIRES(!std::is_same<T, double>::value)
        T foo() {
            return 10;
        }

};

您还可以像这样实现自己的成员需求宏(以防您不想使用另一个库):
template<long N>
struct requires_enum
{
    enum class type
    {
        none,
        all       
    };
};


#define MEMBER_REQUIRES(...) \
typename requires_enum<__LINE__>::type PrivateRequiresEnum ## __LINE__ = requires_enum<__LINE__>::type::none, \
class=typename std::enable_if<((PrivateRequiresEnum ## __LINE__ == requires_enum<__LINE__>::type::none) && (__VA_ARGS__))>::type

这种方式对我不起作用。也许缺少了什么?你能否将原始帖子的示例重写为可工作的形式? - user1284631
原始的例子不能使用重载。我更新了我的答案,告诉你如何使用重载来实现它。 - Paul Fultz II

4
问题在于两个foo()的声明除了默认模板参数外没有任何区别,这并不能使它们成为独立的声明。 定义第二个foo()只是重新定义了第一个foo()
以下是选项摘要,从最现代到最不现代:

使用requires子句有条件地编译成员函数

T foo() requires some_condition<T> {
    return 10;
}

// note: Trailing requires clause could also be omitted here
//       because the first function is more constrained, and overload resolution
//       will select the better one.
T foo() requires (!some_condition<T>) {
    return 5;
}

解决方法:使用 if constexpr 选择实现
T foo() {
    if constexpr (some_condition<T>) {
        // if this is more complex, you can put it in a private member function
        return 10;
    }
    else {
        return 5;
    }
}

正确使用 std::enable_if

// note: Unfortunately, we pollute the function signature with additional template
//       parameters.
// note: std::enable_if_t can be used in C++14
// note: The additional U parameter is necessary to make the condition dependent
//       on a template parameter; otherwise the compiler will simply emit an error
//       for some_condition<T>.
template <typename U = T, typename std::enable_if<some_condition<U>, int>::type = 0>
T foo() {
    return 10;
}

template <typename U = T, typename std::enable_if<!some_condition<U>, int>::type = 0>
T foo() {
    return 5;
}

混合
template <typename T, bool B>
struct mixin;

template <typename T>
struct mixin<T, true> {
    T foo() { return 10; }
}

template <typename T>
struct mixin<T, false> {
    T foo() { return 5; }
}

template <typename T>
struct Y : mixin<T, some_condition<T>> {};

-1
// Try this one:

#include <iostream>
#include <type_traits>

// suppose you want to disable certain member functions based on the tag
struct FooTag;
struct BarTag;

// macro to save some typings in the following
// note that a dummy typename is involved in both the 
// first and second parameters. 
// this should be different than the template parameter of the class (typename T for Widget below)

#define EnableIfFoo(T) \
template <typename Dummy = void, typename = \
          typename std::enable_if<std::is_same<FooTag, T>::value, Dummy>::type>

#define EnableIfBar(T) \
template <typename Dummy = void, typename = \
          typename std::enable_if<std::is_same<BarTag, T>::value, Dummy>::type>

template <typename T>
class Widget {
public:
    // enable this function only if the tag is Bar
    EnableIfFoo(T)
    void print() const { std::cout << "I am a Foo!" << std::endl; }
    
    // enable this function only if the tag is Foo
    EnableIfBar(T)
    void display() const { std::cout << "I am a Bar!" << std::endl; }
};


int main() {
    
    // instantiate a widget with tag Foo
    // only print is enabled; display is not
    Widget<FooTag> fw;
    fw.print();
    //fw.display(); // compile error !!
    
    // instantiate a Widget using tag Bar
    // only display is enabled; print is not
    Widget<BarTag> bw;
    bw.display();
    //bw.print(); // compile error !!
    
    return 0;
}

目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

-1

这是我的极简示例,使用宏。 在使用更复杂的表达式时,请使用双括号enable_if((...))

template<bool b, std::enable_if_t<b, int> = 0>
using helper_enable_if = int;

#define enable_if(value) typename = helper_enable_if<value>

struct Test
{
     template<enable_if(false)>
     void run();
}

我理解你想要的是什么,但这不是惯用法。我们应该鼓励人们避免使用宏,而是直接编写 template <typename = std::enable_if_t<b, int> = 0> - Ben
@Ben 你的意思是 template <bool b, std::enable_if_t<b, int> = 0> 吗? 我认为现在使用 "requires" 更好,可以避免像这个宏那样的愚蠢宏。 - Aedoro
很可能,C++20的概念还来不及到来。 :-/ - Ben

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