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 yourenable_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 callIn addition,
std::enable_if<!std::is_const<R>::value, T>::type
depends on template parameterT
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?
Defining
A<T, N>::g()
outsideA
: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 matchA
'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 changingA
'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";
}Defining
A<T, N>::g()
insideA
: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 hasA
'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
When Do We Have to Use Copy Constructors
C++ Convert from 1 Char to String
Inspecting Standard Container (Std::Map) Contents with Gdb
Reading from Ifstream Won't Read Whitespace
Using Regex Lookbehinds in C++11
How to Have an Unordered_Map Where the Value Type Is the Class It's In
How Is the Size of a C++ Class Determined
Why Does Long Long N = 2000*2000*2000*2000; Overflow
C++ Custom Compare Function for Std::Sort()
Is the Behaviour of I = I++ Really Undefined
Is the Memory Allocated for Struct Members Continguous? What If a Struct Member Is an Array