I was following Jean Guegant's SFINAE blog post, where he implements a type trait using sizeof() which checks if there is a serialize function in the class. Inside which he has this reallyHas struct which checks if serialize is just a member of the class or an actual member function, by doing this
template<typename T>
struct has_serialize {
typedef char yes[1];
typedef char no[2];
template<typename U, U u> struct reallyHas{};
template <typename C>
static yes& test(reallyHas<string (C::*)(), &C::serialize>*){}
// for const method
template<typename C>
static yes& test(reallyHas<string (C::*)() const, &C::serialize>*){}
template<typename> // variadic template
static no& test(...){} // sink hole
enum {
value = (sizeof(test<T>(0)) == sizeof(yes))
};
};
This works fine only when the test function has the signature of
template <typename C>
static yes& test(reallyHas<string (C::*)(), &C::serialize>*){}
But not when
template <typename C>
static yes& test(reallyHas<string (C::*)(), &C::serialize>){}
I don't really understand the significance of the * in the argument, In the end we are just checking if the substitution happens or not, so shouldn't it work in both the cases
Simplifying, the principle of SFINAE is to provide a few candidates for a symbol (overloads, template specializations etc.). Then, for a given argument the compiler will try out every candidate one at a time, until it finds the best match.
In this case, the test
function is used for SFINAE. The goal is to have it defined in such a way that for types with a serialize
member function it will resolve to a function test
that returns a yes
type and for a function returning the no
type otherwise.
The example shown above accomplishes this by using a trick where 0
gets converted to a pointer. It is possible for such a conversion to occur by the rules of the language.
When test<T>(0)
gets called, the compiler will first look at the first overload and by substituting T
will end up with a function that looks something like this:
static yes& test(reallyHas<string (T::*)(), &T::serialize>*){}
The compiler will now see if this overload can be chosen. It will see if 0
(int
) can be converted to reallyHas<string (T::*)(), &T::serialize>*
. As int
s can be converted to pointers an error will occur only if the type reallyHas<string (T::*)(), &T::serialize>*
would be ill-formed. This is the case if T
doesn't have an appropriate a member function &T::serialize
that can be converted to the function pointer string (T::*)()
. For example for using T = int
, it would fail as int
isn't even a class type.
If the substitution fails, the compiler will analogically try the second overload and finally the third one. As the third one is declared as static no& test(...){}
it can accept any arguments. In this case it could just as well be declared as static no& test(unsigned){}
as 0
is convertible to unsigned
.
Now notice what happens if you would change the first overload to this:
template <typename C>
static yes& test(reallyHas<string (C::*)(), &C::serialize>){}
Now the substitution failure will happen always (even if T
has a member serialize
function) as 0
(int
) is not convertible to a class type reallyHas<T>
.