Why Doesn't Sfinae (Enable_If) Work for Member Functions of a Class Template

Why doesn't SFINAE (enable_if) work for member functions of a class template?

SFINAE only works for deduced template arguments, i.e. for function templates. In your case, both templates are unconditionally instantiated, and the instantiation fails.

The following variant works:

struct Foo
{
template <typename T>
typename std::enable_if<std::is_same<T, A>::value>::type bar(T) {}

// ... (further similar overloads) ...
};

Now Foo()(x) causes at most one of the overloads to be instantiated, since argument substitution fails in all the other ones.

If you want to stick with your original structure, use explicit class template specialization:

template <typename> struct Foo;

template <> struct Foo<A> { void bar() {} };
template <> struct Foo<B> { void bar() {} };

SFINAE not working with member function of template class

You're missing a dependent name for the compiler to use for SFINAE. Try something like this instead:

#include <type_traits>

template<class T>
class A
{
public:
template<typename Tp = T>
typename std::enable_if<std::is_floating_point<Tp>::value, Tp>::type
foo () {
return 1.23;
}
};

int main() {
A<double> a;
a.foo();
}

If the type T is not floating point, the declaration would be malformed (no return type) and the function would not be considered for the overload set.

See it on godbolt.

SFINAE with Templated class Member functions

In out of line definition you remove the default arguments, they are taken from declarations:

template <class T>
template <typename U, typename std::enable_if_t<!std::is_same<U, int>::value && !std::is_same<U, float>::value,int>>
void Foo<T>::sfinae() { // Foo<anything else>
std::cout << "sfinae default" << std::endl;
}

the same for the others

The catch-all default case(which prints sfinae-default) has to currently be written as not(type1, type2,...) which can potentially be huge. Is there a shorter/cleaner solution possible?

For this you need to add an extra parameter to rank the overloads giving the lowest rank to the catch-all one, this is usually done using the fact that the ellipsis parameter is not better than any other parameter type:

template <typename U = T>
void sfinae(...);

template <typename U = T, typename std::enable_if_t<std::is_same<U, int>::value,int> = 0>
void sfinae(int);

template <typename U = T, typename std::enable_if_t<std::is_same<U, float>::value,int> = 0>
void sfinae(int);

Foo<char>{}.sfinae(0); // select the catch-all

What this means is that since ... is worse than any other parameter (int is better than ...), the overload sfinae(...) is only considered if the other two can't be called, i.e if they SFINAE.

Clarification on member function template specialization using enable_if


When I try to simplify the code, however, by trying to remove what I perceive as an unnecessary

Unfortunately (if I understand correctly) you have removed something that is necessary

If I understand correctly, you have simplified the following methods

template<typename U=T, 
typename std::enable_if_t<std::is_same_v<U,T>>* = nullptr,
typename std::enable_if_t<!std::is_pointer_v<U>>* = nullptr>
inline T& at(i32 i)
{
return e[i];
}

template<typename U = T,
typename std::enable_if_t<std::is_same_v<U, T>>* = nullptr,
typename std::enable_if_t<std::is_pointer_v<U>>* = nullptr>
inline typename std::remove_pointer_t<T>& at(i32 i)
{
return *e[i];
}

as follows

template<typename std::enable_if_t<!std::is_pointer_v<T>>* = nullptr>
inline T& at(i32 i)
{
return e[i];
}

template<typename std::enable_if_t<std::is_pointer_v<T>>* = nullptr>
inline typename std::remove_pointer<T>::type& at(i32 i)
{
return *e[i];
}

Unfortunately SFINAE, over a template method, works only when uses tests (std::enable_if_t) based on template parameters of the method itself.

I mean: SFINAE doesn't works when the std::enable_if_t tests involve T (and only T) because T is a template parameter of the struct, not a template parameter of the method.

So you need the trick

typename U = T

that "transform" the T type in a template parameter of the method.

You can simplify a little removing the typename before std::enable_if_t because the "_t" is exactly typename (see the definition of std::enable_if_t)

Off topic: I'm not a language layer but, as far I know,

 std::enable_if_t<std::is_same_v<U,T>>* = nullptr

isn't perfectly legal; I suggest to rewrite using int instead of void *

 std::enable_if_t<std::is_same_v<U,T>, int> = 0

or maybe bool

 std::enable_if_t<std::is_same_v<U,T>, bool> = true

or other integer types

Concluding, I suggest to rewrite your at() method as follows

template <typename U = T, 
std::enable_if_t<std::is_same_v<U, T>, int> = 0,
std::enable_if_t<!std::is_pointer_v<U>, int> = 0>
inline T& at(i32 i)
{
return e[i];
}

template<typename U = T,
std::enable_if_t<std::is_same_v<U, T>, int> = 0,
std::enable_if_t<std::is_pointer_v<U>, int> = 0>
inline typename std::remove_pointer_t<T>& at(i32 i)
{
return *e[i];
}

Member function template selection and SFINAE


  • With default value removed, for test1, you have:

    template <typename T, typename std::enable_if<std::is_const<T>::value, int>::type>
    void test1();

    template <typename T, typename std::enable_if<!std::is_const<T>::value, int>::type>
    void test1();

    Which have clearly different signatures.

  • For test2:

    template <typename T, typename> void test2();

    template <typename T, typename> void test2();

    Which are clearly identical signatures.

  • For test3, SFINAE doesn't apply as you have hard error as R is fixed in the class and your enable_if doesn't depend of template parameter of the function.

  • For test4, there is an exception about signature for template function as overload may differ only by return type so

    int foo();
    char foo(); // Illegal.

    but

    template <typename T> int foo();
    template <typename T> char foo(); // legal, even it is not trivial to call

    In addition, std::enable_if<!std::is_const<R>::value, T>::type depends on template parameter T so it is ok.

  • For test5, second template parameter depends on first template parameter T, so it is ok too.

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

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.

Is out-of-line sfinae on template member functions possible?

The return type in the declaration must match the definition.

struct A {
template <typename T>
typename std::enable_if<(sizeof(T) > 4), void>::type
foo(T a);
};

SFINAE cannot be encapsulated as an implementation detail.

(demo)

enable_if with return type for class member function

There is no SFINAE involved here because bool_check_return are not templates themselves. They are just regular overloaded functions that differ only in return type. Making them templates would solve the problem by allowing only one of them:

template<bool enabled = boolean>
constexpr typename std::enable_if<enabled, int>::type bool_check_return(
int s) const noexcept {
return s + 1;
}

template<bool enabled = boolean>
constexpr typename std::enable_if<not enabled, int>::type bool_check_return(
int s) const noexcept {
return s;
}


Related Topics



Leave a reply



Submit