c++templatessfinaetemplate-specializationargument-dependent-lookup

How do I specialize a templated function for types that have a particular method name?


A third-party library provides us with a function that looks something like this (obviously the actual function is much more complicated):

template<typename T>
std::string toString(const T& value) {
    std::cout << "Called 'unspecialized' toString" << std::endl;
    return std::to_string(value);
}

Because this is from a third-party libary, I am unable to change this function signature.

Many of our internal types have a toString method, for example:

struct Foo {
    std::string toString() const {
        return "Foo";
    }
};

Ideally, calling the toString function would defer to the internal type's toString method if it has one. I have tried to accomplish this using SFINAE:

template<typename T, typename Enable = decltype(std::declval<T>().toString())>
std::string toString(const T& value) {
    std::cout << "Called 'specialized' toString" << std::endl;
    return value.toString();
}

However, calling toString for such types now results in a compilation error:

int main() {
    toString(5); // OK
    toString(Foo{}); // compilation error: call of overloaded 'toString(Foo)' is ambiguous
}

How do I resolve this error without changing the unspecialized version of toString?

Edit: for some background, this is an issue with Google Test's PrintTo function, which prints out values from unit tests (doc). That function is designed to look for an overload for a particular type. If it finds one, it uses that overload to print the type. If it doesn't find one, it uses its default ('unspecialized') implementation, which just dumps out the bytes.

There were several answers that offered solutions which required changing the default implementation of the function. These answers may be helpful to other users, but they do not help this case. I marked Paul's concept-based answer as accepted since it seems to be the only one that works without requiring modification of the default signature, even though it requires C++20.

I think this issue may be more accurately categorized as a limitation with Google Test. I found at least one issue in GitHub that addresses this, but it's still open.


Solution

  • In C++20 this is easy. Just replace your SFINAE stuff with this:

    template <typename T>
    concept HasToString = requires(T t) { t.toString(); };
    
    template<typename T> requires HasToString <T>
    std::string toString(const T& value){
        std::cout << "Called 'specialized' toString" << std::endl;
        return value.toString();
    }
    

    Live demo


    See also @TedLyngmo's (better) offering in the comments.


    Here's my attempt at a C++17 solution.

    Turn the problem on its head and replace your SFINAE stuff (never one of my favourite things) with this, in order to select the built-in to_string for arithmetic types only (tweak that test to suit). if constexpr makes this particularly neat:

    template<class T>
    std::string toString (const T& value)
    {
        if constexpr (std::is_arithmetic_v <T>)
        {
            std::cout << "Called 'unspecialized' toString" << std::endl;
            return std::to_string (value);
        }
        else
        {
            std::cout << "Called 'specialized' toString" << std::endl;
            return value.toString ();
        }
    }
    

    Live demo