Search code examples
c++templatessfinaeenable-ifpartial-specialization

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


Very weird problem I've been struggling with for the past few hours (after solving 5-6 other issues with SFINAE as I'm new to it). Basically in the following code I want to have f() working for all possible template instantiations, but have g() available only when N == 2:

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

When I try to compile it I get an error about having 3 template parameters instead of two. Then, after some trials, I've decided to move the definition of g() inside the definition of A itself, like this:

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

Now, magically everything works. But my question is WHY? Doesn't the compiler see that inside the class definition I'm trying to inline a member function that also depends on 3 template parameters? Or let's reverse the question: if it works inside A's definition, why doesn't it work outside? Where's the difference? Aren't there still 3 parameters, which is +1 more than what class A needs for its template parameters?

Also, why does it only work when I'm making the 3rd parameter a non-type one and not a type one? Notice I actually make a pointer of the type returned by enable_if and assign it a default value of nullptr, but I see I can't just leave it there as a type parameter like in other SO forum posts I see around here.

Appreciate it so much, thank you!!!


Solution

  • That would be because a templated function in a templated class has two sets of template parameters, not one. The "correct" form is thus:

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

    See it in action here.

    [Note that this isn't actually correct, for a reason explained at the bottom of this answer. It'll break if N != 2.]

    Continue reading for an explanation, if you so desire.


    Still with me? Nice. Let's examine each situation, shall we?

    1. Defining A<T, N>::g() outside A:

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

      In this case, A<T, N>::g()'s template declaration doesn't match A's template declaration. Therefore, the compiler emits an error. Furthermore, g() itself isn't templated, so the template can't be split into a class template and a function template without changing A's definition.

      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. Defining A<T, N>::g() inside A:

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

      In this case, since g() is defined inline, it implicitly has A's template parameters, without needing to specify them manually. Therefore, g() is actually:

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

    In both cases, for g() to have its own template parameters, while being a member of a templated class, the function template parameters have to be separated from the class template parameters. Otherwise, the function's class template wouldn't match the class'.


    Now that we've covered that, I should point out that SFINAE only concerns immediate template parameters. So, for g() to use SFINAE with N, N needs to be its template parameter; otherwise, you'd get an error if you tried to call, for example, A<float, 3>{}.g(). This can be accomplished with an intermediary, if necessary.

    Additionally, you'll need to provide a version of g() that can be called when N != 2. This is because SFINAE is only applicable if there's at least one valid version of the function; if no version of g() can be called, then an error will be emitted and no SFINAE will be performed.

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

    If doing this, we can further simplify things, by having the intermediary do the heavy lifting.

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

    See it in action here.