Search code examples
c++sfinaetype-traits

Error trying to implement type checking with SFINAE


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


Solution

  • 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 ints 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>.