8

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

skypjack
  • 49,335
  • 19
  • 95
  • 187
Otringal
  • 131
  • 7

2 Answers2

7

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.

  • 1
    Also note that, as shown in [skypjack's answer](http://stackoverflow.com/a/41904229/5386374): 1) `std::enable_if_t` can be used as a type alias for `typename std::enable_if::type`, 2) `T` defaults to `void`, and can be omitted unless it needs to be another type, and 3) `std::enable_if_t` can be used on the function's return type (or on a parameter, if the function has any). – Justin Time - Reinstate Monica Jan 27 '17 at 23:16
  • Thank you so much!!! I'm still digesting this chunk of information and rewiring what I knew about templates now that I'm facing this never-before-seen world of SFINAE :) btw: do you happen to know why MSVC gives me errors everywhere, saying 'type': is not a member of 'std::enable_if'??? The code I've posted was compiled with some online gcc-based IDE, but my actual project is in VC and I see it somehow warns me about the case when the check will be false -> which duuhhh, that's why I'm doing this for, to ALSO have cases when the check won't pass. Any idea why it's being an idiot? :) – Otringal Jan 27 '17 at 23:55
  • @Otringal Did you define a version of `g()` that can be called when `N != 2`? If you didn't, then you'll get an error when you try to call `g()` on an `A` whose `N` is anything other than 2, because `std::enable_if` only has a member `type` if the boolean condition is `true`. – Justin Time - Reinstate Monica Jan 28 '17 at 00:10
  • Did you use the code from the top of my answer, then try to make, for example, an `A`? If so, then you'll get an error because the condition is `false`, which means `std::enable_if` won't have a member `type`. – Justin Time - Reinstate Monica Jan 28 '17 at 00:13
  • If neither of those is the problem, you'll have to be more specific, since I can't think of what else it could be off the top of my head; MSVC has some issues with SFINAE, but I don't believe they should cause any trouble here. – Justin Time - Reinstate Monica Jan 28 '17 at 00:14
  • (Basically, `std::enable_if` is designed specifically to cause an error if the condition is false, so that the compiler will discard that overload and look for another one. If it can't find one that doesn't have an error, though, then the compiler has no choice but to emit an error.) – Justin Time - Reinstate Monica Jan 28 '17 at 00:16
  • Yes, that is the desired behaviour, as I'm actually using this SFINAE approach for 3 types of constructors. I'm basically selecting between 3 constructors with 2, 3 or 4 parameters, based on my N which can be 2, 3 or 4, so I'm calling them correctly. Weirdly enough after switching to your advice with using enable_if_t<> instead of enable_if<>::type, that problem vanished, BUT another one appeared. Now I get error C2794: 'type': is not a member of any direct or indirect base class of 'std::enable_if', which is absurd, as I no longer use enable_if anywhere in my code ... So so strange – Otringal Jan 28 '17 at 00:18
  • 1
    @Otringal `enable_if_t` is just a typedef for `typename enable_if::type`, so using it won't magically define `type` when `B` is `false`. The issue is that the compiler can't find any `enable_if` or `enable_if_t` versions of the function, so it emits an error about `enable_if` not having a member named `type` (which is correct, `enable_if` only has `type` if its condition evaluates to `true`). Try supplying a version of the function that can be called when `(N != 2) && (N != 3) && (N != 4)`. – Justin Time - Reinstate Monica Jan 28 '17 at 17:51
  • 1
    Consider [this](http://coliru.stacked-crooked.com/a/64b434eee977f039), for example. If you comment out #1, then `C` and `C` will cause a "`std::enable_if` doesn't have a member named `type`" error. – Justin Time - Reinstate Monica Jan 28 '17 at 18:11
4

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.

skypjack
  • 49,335
  • 19
  • 95
  • 187
  • 1
    This looks correct. Ultimately, the reason is because sfinae is only useful for including or omitting *template* definition. and in the first example `g` is not a template; it's a member function of a template class. By making `g` a template member function itself you can include or omit it at-will via sfinae. – WhozCraig Jan 27 '17 at 22:58
  • 1
    @WhozCraig I'm stealing your words to enrich the answer. Thank you. :-) – skypjack Jan 27 '17 at 22:59
  • Thanks! Also, do you happen to know why this apparently works only when using the type of enable_if as a pointer and not a base type itself? For example if instead of ::type* = nullptr, I only leave ::type = 0 it doesn't work. Why? – Otringal Jan 28 '17 at 00:22
  • 1
    @Otringal Because `void` is not a valid type for a non-type template parameter and also a `void` can not be given the value `0` (or any value). – Oktalist Jan 28 '17 at 00:40
  • you're right, I see it works with 'int'. But now I do not understand why something like template::type> does not work. It is the T from A itself (I'm using the second example), and I no longer have a default value or a named variable after it, so the whole enable_if thing should resolve like any normal type template parameter, right? I mean, it's like having template when N == 2, right? – Otringal Jan 28 '17 at 01:09
  • 1
    @Otringal No, it's not like `template`, it's like `template`. – Oktalist Jan 28 '17 at 01:23