Function call ambiguity in templatized code

Viewed 87

Here is a (very distilled) use case and code example (sorry if it doesn't look too minimal, I couldn't figure out what else to exclude. The complete 'just compile' code can be found here: https://gcc.godbolt.org/z/5GMEGKG7T)

I want to have a "special" output stream, which, for certain user types, does special treatment during stream processing.

This special stream should be able to stream a "special" type with "special" treatment, as well as any other type which could be streamed through conventional streams - in the latter case, we simply use the conventional stream directly

struct Type { }; // Special type

struct Stream { // Special stream

    // In this distilled example, we could achieve the same result
    // by overriding << for ostream and Type, but the actual use case is wider
    // and requires usage of the special stream type
    // For illustration purposes only, we use this overload to cout "T!" for Types
    Stream& stream(Type t) { std::cout << "T!"; return *this;}

    // For any type for which cout << T{} is a valid operation, use cout for streaming       
    template<class T>
    auto stream(const T& t) -> decltype(std::cout << std::declval<T>(), std::declval<Stream&>())
    {
        std::cout << t; return *this;
    }
};

// Consumes every T for which Stream.stream is defined - those would be Type
// as well as anything else which can be sent to cout
template<class T>
auto operator<<(Stream& s, const T& t) -> decltype(s.stream(t))
{
    s.stream(t);
    return s;
}

This code works as expected:

void foo()
{
    Stream stream;
    stream << 10 << Type{};
}

Now I want to define a different struct, which has a member of Type in it, and a streaming operator for the same struct:

struct Compound {
    Type t;
    int i;
};

// We want the `<<` operator of the struct templated, because we want it to work with 
// any possible stream-like object out there 
template<class S>
S& operator<<(S& s, Compound comp)
{
    s << comp.t << " " << comp.i;

    return s;
};

Unfortunately, this doesn't work well. An attempt to use this << operator:

void bar(Compound comp)
{
    Stream s;
    s << comp;
}

Triggers an ambiguity:

error: ambiguous overload for 'operator<<' (operand types are 'Stream' and 'Compound')

note: candidate: 'decltype (s.Stream::stream(t)) operator<<(Stream&, const T&)

note: candidate: 'S& operator<<(S&, Compound) [with S = Stream]'

That makes sense, since after defining templated operator for << for Compound, it could be streamed via cout, as well as through this new operator!

I can certainly work aorund this by making << for Compound non-templated, and instead use the special Stream as a stream argument - but I'd like to avoid it.

Is there a way I can preserve the templated << for Compound and still avoid the ambiguity?

2 Answers

So you need to lower the priority of operator<<(Stream& s, const T& t) to make it the last resort.

You can do it by making it (seemingly) less specialized, by templating the first parameter:

auto operator<<(std::same_as<Stream> auto &s, const auto &t) -> decltype(s.stream(t))
{
    s.stream(t);
    return s;
}

The change I made to the second parameter is unimportant, it's just for terseness.

Some extra boilerplate maybe?

template<class T> struct IsDefault: std::true_type {};
template<class T> concept Default = IsDefault<T>::value;

struct Stream {
    template<Default T> Stream &operator<<(T &&t) {
        return std::cout << t, *this;
    }
};

struct Compound {};
template<> struct IsDefault<Compound>: std::false_type {};
Stream &operator<<(Stream &s, Compound &&c) { return s << "Compound"; }
Related