2

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.

Harry Williams
  • 310
  • 3
  • 11
  • Like the error message says, you are not specializing the function, but adding an overload. Trying to partially specialize function templates runs into some problems: [Why function template cannot be partially specialized?](https://stackoverflow.com/questions/5101516/why-function-template-cannot-be-partially-specialized) – BoP Jun 12 '23 at 20:47
  • Can you use C++20? – Paul Sanders Jun 12 '23 at 20:50
  • 1
    Hi Paul, I'm looking for a C++17 solution. Concepts would probably solve this case, but we're still 1+ years out from upgrading our compilers. – Harry Williams Jun 12 '23 at 20:52
  • Hi BoP, perhaps I can could have been clearer with my wording. When I say "specialize", I just mean "call the correct function for this type". I'm aware that partial specialization of functions is not allowed. – Harry Williams Jun 12 '23 at 20:54
  • 1
    Sorry Harry, just seen your comment. I'll leave the answer there now; sorry it doesn't do what you want. Maybe you can persuade the team to move the compiler on... – Paul Sanders Jun 12 '23 at 20:55

1 Answers1

2

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

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • 3
    Perhaps [`std::string toString(HasToString auto const& value)`](https://godbolt.org/z/6cY8Gdcv8) would be even easier? – Ted Lyngmo Jun 12 '23 at 21:05
  • 1
    Smart work, @ted – Paul Sanders Jun 12 '23 at 21:22
  • Tested this solution out with C++20 enabled and it works like a charm! If there is truly no other way to do this without concepts, I'll mark this answer as accepted. – Harry Williams Jun 12 '23 at 21:24
  • @HarryWilliams Found a way :) – Paul Sanders Jun 12 '23 at 21:54
  • Thanks for the edit, Paul. Unfortunately, I'm not able to change the 'unspecialized' toString function (it's defined in a third-party library), but this answer may be helpful to other users in the more general case. The more I think about this case, the more I'm leaning towards treating this as a bug in the third-party library. In fact, it looks like an issue has already been opened for this: https://github.com/google/googletest/pull/3695 – Harry Williams Jun 12 '23 at 22:10
  • If you're prepared to change the calls in `main` to (say) `toString_dispatch` and rename my template accordingly, this problem would go away. But I can imagine that would involve a lot of work. – Paul Sanders Jun 12 '23 at 22:49
  • 1
    @HarryWilliams The 3PP function is (should be) in a namespace. You could put your `toString` in a different namespace and only use that `toString` function. In the cases where `T` has `T::toString()`, then call that, otherwise call the 3PP-version. – Ted Lyngmo Jun 13 '23 at 11:24