Reputation: 73
I'm trying to figure out this SFINAE concept, and I must say I find it really confusing. I understand why it would be a huge advantage if the compiler could deduce and choose the correct function based on the template types/args, but I don't know if that's SFINAE, since what the acronym stands for implies otherwise IMO. Maybe you can sort that out for me, but right now that's actually not my problem.
My problem is this: I looked up and tried a SFINAE example from here: https://en.cppreference.com/w/cpp/language/sfinae
Specifically the one that tells you whether or not the template type C is an object type or an intrinsic type (int, bool, whatever). The example code I'm talking about is this:
template<typename T>
class is_class {
typedef char yes[1];
typedef char no [2];
template<typename C> static yes& test(int C::*); // selected if C is a class type
template<typename C> static no& test(...); // selected otherwise
public:
static bool const value = sizeof(test<T>(0)) == sizeof(yes);
};
I then wanted to try it out, so I modified it slightly, changing nothing that could possibly affect which function would be used, and ended up with the following code. I use Visual Studio 2017. I don't think it runs C++2017, but it can't be far behind. Needless to say, it shows "is not class" twice:
#include<cstdio>
#define say(x) printf(#x "\n")
template<typename T>
void f(int T::*) {
printf("f<T>();\n");
}
template<class T>
void f(T) {
printf("normal f();\n");
}
class Hejsa {
public:
int A;
Hejsa() { A = 2; }
};
template<typename T>
class is_class {
public:
typedef char yes[1];
typedef char no[2];
template<typename C> static yes& test(int C::*) {
say("is class"); return new yes;
}; // selected if C is a class type
template<typename C> static no& test(...) {
say("is not class"); no _n; return _n;
}; // selected otherwise
public:
static bool const value = sizeof(test<T>(0)) == sizeof(yes);
};
int main() {
int var1 = 9;
Hejsa var2;
is_class<Hejsa>::test<Hejsa>(var1);
is_class<Hejsa>::test<Hejsa>(var2);
f(var1);
f(var2);
getchar();
}
What is causing this? Can you change it as little as possible and make it "work", that is, make test<Hejsa>(var1);
say "is not class" and test<Hejsa>(var2)
say "is class"? ('Hejsa' is a Danish word that means 'Hiya' or 'Hello there', something like that ;P)
Thanks in advance,
Thomas
Upvotes: 0
Views: 182
Reputation: 6863
From your code, I would say you are confusing SFINAE and overload resolution (maybe because cppreference.com uses both in the example).
Overload resolution are rules the compiler applies for choosing a function to call. Therefore, it allows you to call a different function depending on the type you pass.
SFINAE will allow you test if an operation on a type you don't know is possible. Typically, test if a type has a given member function.
Normally, you wouldn't be able to write the code testing it because it is an error to call a member function that doesn't exists. But, when applying the rules of overload resolution, the compiler will discard a function which would cause a compilation error. The compilation succeeds because the ellipsis overload is a kind of catch all.
Now, the interest of SFINAE is that if your type has the given member function, the compiler will choose the more precise function (ellipsis is the last resort). Using, different return types for the functions, allows you to detect which overload was chosen.
Upvotes: 1
Reputation: 217293
How does SFINAE work to create that traits:
first, simple typedef to ensure different sizes
typedef char yes[1]; // sizeof == 1
typedef char no [2]; // sizeof != 1 (2 actually)
Then, 2 overloads functions (declaration):
template<typename C> static yes& test(int C::*);
template<typename C> static no& test(...);
int C::*
is a pointer on member of type int
. It is well formed only when C
is a class (even if the class doesn't have member of type int
BTW). It is ill-formed for other types (as float
).
...
is the ellipsis to accept extra arguments (like printf
family).
So when C
is a class, both function are valid for test<C>(0)
yes& test(int C::* m); // with m = nullptr
no& test(...); // with ... taking int 0
Overload resolution rules do the first one is selected. (so return type is yes&
).
But when C
is not a class (let say a float
)
For template<typename C> static yes& test(int C::*);
, with substitution
we would got yes& test(int float::*);
As the function is template and the failure depends of the template, instead of having an error, we simply ignore that function in overload set (it is not an error).
Only ellipsis function remains valid for test<C>(0)
(so return type is no&
).
Now, using sizeof(test<T>(0))
allows to ask compiler which overload it would choose (without calling the function, so definition is not required).
We store the final result in static member is_class::value
.
Once you have a traits, Possible usage include tag dispatching in overloads or directly SFINAE:
template <typename T>
std::enable_if_t<is_class<T>::value> foo() { std::cout << "a class"; }
template <typename T>
std::enable_if_t<!is_class<T>::value> foo() { std::cout << "a class"; }
std::enable_if_t<bool>
is ill formed when bool is false, and is substituted by void
otherwise.
Upvotes: 1