Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!!!

like image 233
Otringal Avatar asked Jan 27 '17 22:01

Otringal


2 Answers

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.

like image 155
Justin Time - Reinstate Monica Avatar answered Sep 20 '22 19:09

Justin Time - Reinstate Monica


In the first snippet, template parameters are of A and you are redeclaring it with an extra parameter (that is an error).
Moreover, sfinae expressions are involved with class template or function template, that is not the case in the example.

In the second snippet template parameters are of g and that's now a member function template to which the sfinae expression correctly applies.


It follows a working version:

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

Note that the non-type parameter must be in the actual context of the sfinae expression for the latter to work.
Because of that, template<int M = N> is mandatory.

Other solutions apply as well.
As an example, you can use a base class that exports f and a derived template class with a full specialization that add g.

like image 43
skypjack Avatar answered Sep 23 '22 19:09

skypjack