为什么SFINAE(enable_if)从类定义内部工作而不是从外部工作

why SFINAE (enable_if) works from inside class definition but not from outside

本文关键字:工作 内部 定义 从外部 SFINAE enable if 为什么      更新时间:2023-10-16

在过去的几个小时里,我一直在努力解决一个非常奇怪的问题(因为我是新手,所以在解决了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论坛帖子中那样,将其作为类型参数保留在那里。

非常感谢,谢谢!!!

这可能是因为模板类中的模板函数有两组模板参数,而不是一组。因此,"正确"的形式是:

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";
}

请在此处查看它的实际操作。

[注意,这实际上并不是正确的,原因在这个答案的底部解释。如果N != 2,它会崩溃。]

如果你愿意的话,继续阅读以获得解释。


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

  1. A:之外定义A<T, N>::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<T, N>::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<T, N>::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<float, 3>{}.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 << "()gn";
}

如果这样做,我们可以通过让中介承担重任来进一步简化事情。

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);
};
// ...

请在此处查看它的实际操作。

在第一个代码段中,模板参数为A,您正在用一个额外的参数重新说明它(这是一个错误)
此外,sfinae表达式与类模板或函数模板有关,示例中并非如此。

在第二个snippet模板中,参数为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<int M = N>是强制性的。

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