Keith Ballard
Keith Ballard

Reputation: 71

Using SFINAE to detect method with GCC

I am having trouble using the SFINAE concept to detect if a class has a particular template method with some compilers, most pertinently GCC. Consider the code below. It compiles and works as expected with MSVC; however, when I compile the same code with GCC, it complains that std::vector<double> does not have a serialize method. Of course, the fact that the method does not exist is true, but I expected this to result in a substitution failure and the compiler determining that the other less specialized fallback is most suitable. Am I missing something about SFINAE or is this a bug with GCC?

#include <iostream>
#include <utility>
#include <vector>

class SerializerBase
{
  public:
    SerializerBase() {}
};

template<typename T>
struct has_serialize
{
    template<typename C>
    static constexpr auto test(...) -> std::false_type;
    /// Note: this is where the compiler complains about the .serialize method
    /// during substitution.
    template<typename C>
    static constexpr auto test(int)
      -> decltype(std::declval<T>().serialize(std::declval<SerializerBase&>()), std::true_type());

    using result_type       = decltype(test<T>(0));
    static const bool value = result_type::value;
};

class Serializer : public SerializerBase
{
  public:
    Serializer() {}

    template<typename T,
             typename std::enable_if<!has_serialize<T>::value, T>::type* = nullptr>
    void operator()(const T& v)
    {
        std::cout << "fallback called" << std::endl;
    }

    template<typename T,
             typename std::enable_if<has_serialize<T>::value, T>::type* = nullptr>
    void operator()(const T& v)
    {
        v.serialize(*this);
    }
};

struct Foo
{
    template<typename SerializerType>
    void serialize(SerializerType& s) const
    {
        std::cout << "serialize called" << std::endl;
    }
};

int main()
{
    Serializer s;
    std::vector<double> v;
    Foo f;
    s(v);
    s(f);

    return 0;
}

Upvotes: 3

Views: 101

Answers (1)

Keith Ballard
Keith Ballard

Reputation: 71

In the event someone else encounters a similar error/misunderstanding, my error (aptly pointed out by n. 'pronouns' m.) was using the wrong type in the has_serialize::test. From what I can infer (in my naivety) is that for

template<typename T>
struct has_serialize
{
    // ...
    template<typename C>
    static constexpr auto test(int)
      -> decltype(std::declval<T>().serialize(std::declval<SerializerBase&>()), std::true_type());
    // ...
};

the template C does not appear in has_serialize::test, and as a result GCC does not provide an opportunity for substitution failure. Consequently, GCC tries to evaluate std::declval<T>().serialize(...) and obviously throws an error when T = std::vector<double>. If T is replaced with C in this line, then the compiler recognizes it as a substitution failure and determines the signature to be unsuitable.

Furthermore, as Maxim Egorushkin pointed out, there is the possibility that T::serialize might return a user-defined type that overloads operator,. To ameliorate (albeit very unlikely) potential error, the code should be:

template<typename T>
struct has_serialize
{
    // ...
    template<typename C>
    static constexpr auto test(int)
      -> decltype(static_cast<void>(std::declval<C>().serialize(std::declval<SerializerBase&>())), std::true_type());
    // ...
};

Upvotes: 4

Related Questions