【问题标题】:why SFINAE (enable_if) works from inside class definition but not from outside为什么 SFINAE (enable_if) 从内部类定义而不是从外部工作
【发布时间】:2017-01-27 22:31:29
【问题描述】:

过去几个小时我一直在努力解决一个非常奇怪的问题(在解决了 SFINAE 的 5-6 个其他问题后,因为我是新手)。基本上在下面的代码中,我想让f() 为所有可能的模板实例化工作,但只有在N == 2 时才有g()

#include <type_traits>
#include <iostream>

template<typename T, int N>
class A
{
public:
    void f(void);
    void g(void);
};

template<typename T, int N>
inline void A<T, N>::f()
{
    std::cout << "f()\n";
}

template<typename T, int N, typename std::enable_if<N == 2, void>::type* = nullptr>
inline void A<T, N>::g()
{
    std::cout << "g()\n";
}

int main(int argc, char *argv[])
{
    A<float, 2> obj;
    obj.f();
    obj.g();

    return 0;
}

当我尝试编译它时,我得到一个关于有 3 个模板参数而不是两个的错误。然后,经过一些试验,我决定将g() 的定义移到A 本身的定义中,如下所示:

#include <type_traits>
#include <iostream>

template<typename T, int N>
class A
{
public:
    void f(void);

    template<typename t = T, int n = N, typename std::enable_if<N == 2, void>::type* = nullptr>
    void g()
    {
        std::cout << "g()\n";
    }
};

template<typename T, int N>
inline void A<T, N>::f()
{
    std::cout << "f()\n";
}

int main(int argc, char *argv[])
{
    A<float, 2> obj;
    obj.f();
    obj.g();

    return 0;
}

现在,神奇地一切正常。但我的问题是为什么?编译器没有看到在类定义中我试图内联一个也依赖于 3 个模板参数的成员函数吗?或者让我们颠倒一下这个问题:如果它在A 的定义中起作用,为什么它在外面不起作用?区别在哪里?不是还有3个参数,比A的模板参数需要哪个类多+1吗?

另外,为什么它只在我将第三个参数设为非类型而不是类型时才有效?请注意,我实际上创建了一个由 enable_if 返回的类型的指针,并为其分配了一个默认值 nullptr,但我看到我不能像在我在这里看到的其他 SO 论坛帖子中那样将它作为类型参数留在那里。

非常感谢,谢谢!!!

【问题讨论】:

    标签: c++ templates sfinae enable-if partial-specialization


    【解决方案1】:

    那是因为模板类中的模板函数有两组组模板参数,而不是一组。因此,“正确”的形式是:

    template<typename T, int N>
    class A
    {
    public:
        void f(void);
    
        template<typename std::enable_if<N == 2, void>::type* = nullptr>
        void g(void);
    };
    
    template<typename T, int N>                                            // Class template.
    template<typename std::enable_if<N == 2, void>::type* /* = nullptr */> // Function template.
    inline void A<T, N>::g()
    {
        std::cout << "g()\n";
    }
    

    看到它在行动here

    [请注意,这实际上不正确,原因在此答案的底部进行了解释。如果N != 2,它会中断。]

    如果您愿意,请继续阅读以获取解释。


    还和我在一起吗?好的。让我们检查每种情况,好吗?

    1. A之外定义A&lt;T, N&gt;::g()

      template<typename T, int N>
      class A
      {
      public:
          void f(void);
          void g(void);
      };
      
      template<typename T, int N, typename std::enable_if<N == 2, void>::type* = nullptr>
      inline void A<T, N>::g()
      {
          std::cout << "g()\n";
      }
      

      在这种情况下,A&lt;T, N&gt;::g() 的模板声明与 A 的模板声明不匹配。因此,编译器会发出错误。而且g()本身没有模板化,所以在不改变A的定义的情况下,不能将模板拆分为类模板和函数模板。

      template<typename T, int N>
      class A
      {
      public:
          void f(void);
      
          // Here...
          template<typename std::enable_if<N == 2, void>::type* = nullptr>
          void g(void);
      };
      
      // And here.
      template<typename T, int N>                                            // Class template.
      template<typename std::enable_if<N == 2, void>::type* /* = nullptr */> // Function template.
      inline void A<T, N>::g()
      {
          std::cout << "g()\n";
      }
      
    2. A内定义A&lt;T, N&gt;::g()

      template<typename T, int N>
      class A
      {
      public:
          void f(void);
      
          template<typename t = T, int n = N, typename std::enable_if<N == 2, void>::type* = nullptr>
          void g()
          {
              std::cout << "g()\n";
          }
      };
      

      在这种情况下,由于g() 是内联定义的,因此它隐含了A 的模板参数,无需手动指定它们。所以g()其实就是:

      // ...
          template<typename T, int N>
          template<typename t = T, int n = N, typename std::enable_if<N == 2, void>::type* = nullptr>
          void g()
          {
              std::cout << "g()\n";
          }
      // ...
      

    在这两种情况下,g() 拥有自己的模板参数,作为模板类的成员,函数模板参数必须与类模板参数分开。否则,函数的类模板将与类不匹配。


    既然我们已经介绍了这一点,我应该指出 SFINAE 只涉及 立即 模板参数。因此,对于g() 使用带有N 的SFINAE,N 需要作为它的模板参数;否则,如果您尝试调用 A&lt;float, 3&gt;{}.g(),则会收到错误消息。如有必要,这可以通过中介来完成。

    此外,您需要提供g() 的版本,当N != 2 时可以调用该版本。这是因为 SFINAE 仅适用于至少有一个有效版本的函数;如果无法调用g() 的任何版本,则会发出错误并且不会执行SFINAE。

    template<typename T, int N>
    class A
    {
    public:
        void f(void);
    
        // Note the use of "MyN".
        template<int MyN = N, typename std::enable_if<MyN == 2, void>::type* = nullptr>
        void g(void);
    
        // Note the "fail condition" overload.
        template<int MyN = N, typename std::enable_if<MyN != 2, void>::type* = nullptr>
        void g(void);
    };
    
    template<typename T, int N>
    template<int MyN /*= N*/, typename std::enable_if<MyN == 2, void>::type* /* = nullptr */>
    inline void A<T, N>::g()
    {
        std::cout << "g()\n";
    }
    
    template<typename T, int N>
    template<int MyN /*= N*/, typename std::enable_if<MyN != 2, void>::type* /* = nullptr */>
    inline void A<T, N>::g()
    {
        std::cout << "()g\n";
    }
    

    如果这样做,我们可以通过让中介完成繁重的工作来进一步简化事情。

    template<typename T, int N>
    class A
    {
    public:
        void f(void);
    
        template<bool B = (N == 2), typename std::enable_if<B, void>::type* = nullptr>
        void g(void);
    
        template<bool B = (N == 2), typename std::enable_if<!B, void>::type* = nullptr>
        void g(void);
    };
    
    // ...
    

    看到它在行动here

    【讨论】:

    • 还要注意,如skypjack's answer所示:1)std::enable_if_t&lt;bool, T&gt;可以作为typename std::enable_if&lt;bool, T&gt;::type的类型别名,2)T默认为void,可以是除非它需要是另一种类型,否则省略,并且 3) std::enable_if_t 可用于函数的返回类型(或参数,如果函数有的话)。
    • 非常感谢!!!我仍然在消化这些信息并重新整理我对模板的了解,因为我现在面临着这个前所未有的 SFINAE 世界:) 顺便说一句:你知道为什么 MSVC 到处给我错误,说'type ': 不是 'std::enable_if' 的成员???我发布的代码是用一些基于 gcc 的在线 IDE 编译的,但是我的实际项目是在 VC 中,我看到它以某种方式警告我检查是否为假的情况-> 哪个 duuhhh,这就是我这样做的原因这是因为,也有检查不通过的情况。知道为什么它是个白痴吗? :)
    • @Otringal 你是否定义了g() 的版本,可以在N != 2 时调用?如果您没有这样做,那么当您尝试在 A&lt;T, N&gt; 上调用 g() 时会出现错误,而 N 不是 2,因为 std::enable_if 只有一个成员 type 如果布尔值条件是true
    • @Otringal enable_if_t&lt;B, T&gt; 只是typename enable_if&lt;B, T&gt;::type 的类型定义,所以当Bfalse 时,使用它不会神奇地定义type。问题是编译器找不到任何enable_if&lt;true, T&gt;enable_if_t&lt;true, T&gt; 版本的函数,因此它会发出关于enable_if&lt;false, T&gt; 没有名为type 的成员的错误(这是正确的,enable_if 只有type 如果其条件评估为 true)。尝试提供可以在 (N != 2) &amp;&amp; (N != 3) &amp;&amp; (N != 4) 时调用的函数版本。
    • 例如考虑this。如果注释掉#1,那么C&lt;float, 5&gt;C&lt;float, 1&gt; 将导致“std::enable_if&lt;false, T&gt; 没有名为type 的成员”错误。
    【解决方案2】:

    在第一个 sn-p 中,模板参数是 A,而您正在使用额外的参数重新声明它(这是一个错误)。
    而且sfinae表达式涉及到类模板或函数模板,例子中没有。

    在第二个 sn-p 模板中,参数是 g,现在是 sfinae 表达式正确应用的成员函数模板。


    它遵循一个工作版本:

    #include <type_traits>
    #include <iostream>
    
    template<typename T, int N>
    class A
    {
    public:
        void f(void);
    
        template<int M = N>
        std::enable_if_t<M==2> g();
    };
    
    template<typename T, int N>
    inline void A<T, N>::f()
    {
        std::cout << "f()\n";
    }
    
    template<typename T, int N>
    template<int M>
    inline std::enable_if_t<M==2> A<T, N>::g()
    {
        std::cout << "g()\n";
    }
    
    int main(int argc, char *argv[])
    {
        A<float, 2> obj;
        obj.f(); // ok
        obj.g(); // ok (N==2)
    
        A<double,1> err;
        err.f(); // ok
        //err.g(); invalid (there is no g())
    
        return 0;
    }
    

    请注意,非类型参数必须在 sfinae 表达式的实际上下文中才能使后者起作用。
    因此,template&lt;int M = N&gt; 是强制性的。

    其他解决方案也适用。
    例如,您可以使用导出f 的基类和添加g 的具有完全特化的派生模板类。

    【讨论】:

    • 这看起来是正确的。归根结底,原因是因为 sfinae 仅对包含或省略 template 定义有用。在第一个示例中,g 不是模板;它是模板类的成员函数。通过使 g 成为模板成员函数本身,您可以通过 sfinae 随意包含或省略它。
    • @WhozCraig 我在偷你的话来丰富答案。谢谢你。 :-)
    • 谢谢!另外,您是否知道为什么这显然仅在使用 enable_if 类型作为指针而不是基类型本身时才有效?例如,如果不是 ::type* = nullptr,我只留下 ::type = 0 它不起作用。为什么?
    • @Otringal 因为void 不是非类型模板参数的有效类型,而且void 不能被赋予值0(或任何值)。
    • @Otringal 不,不像template&lt;typename T&gt;,它像template&lt;T&gt;
    猜你喜欢
    • 2015-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-13
    • 2020-11-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多