Is there way out of this ambiguity?

121 Views Asked by At

Consider this (rather) simple example:

#include <iostream>

struct Out {
    int value;
};
template<class Sink> decltype(auto) operator<<(Sink &&s, Out const &out) {
    return out.value > 0? s << out.value : s;
}

struct In {
    std::ostream &sink;
    template<typename T> In &operator<<(T const &t) {
        return sink << t, *this;
    }
};

int main() {
    In in{std::cout};
    in << (1 << Out{3}) << '\n';    // Ok
    in << Out{42} << '\n';
        // error: use of overloaded operator '<<' is ambiguous
        // (with operand types 'In' and 'Out')
}

Can such an ambiguity be addressed? We have two classes, each one defines such an operator overload to forward it to its internal type (classes are designed independently by two different people, and another person tries to use them in the same application.) I don't see a way this could be reformulated in terms of other operators, say, it's no use here to give up on A's friend operator<< and try to convert A to int; and to use some kind of complicated SFINAE to rule out some overloads still doesn't look helpful.

2

There are 2 best solutions below

3
On

You might create additional overloads which would be the better match:

decltype(auto) operator<<(In& in, Out const &out) {
    return in.operator<<(out);
}

decltype(auto) operator<<(In&& in, Out const &out) {
    return in.operator<<(out);
}
1
On

Some semi-oldschool SFINAE seemingly does the trick, in the sense that it is now accepted by both gcc and clang (and they both print "8" and "42" equally):

#include <iostream>
#include <utility>

template<typename S, typename T> class can_shl {
    using Left = char;
    struct Right { char c[2]; };
    template<typename U> static constexpr decltype(
            std::declval<S>() << std::declval<U>(), Left{}) 
        has_it(U &&);
    static constexpr Right has_it(...);
public:
    static constexpr bool value = sizeof(has_it(std::declval<T>())) == 1;
};

struct Out {
    int value;
};
template<class Sink> auto operator<<(Sink &&s, Out const &out) 
    -> std::enable_if_t<!can_shl<Sink, Out>::value, decltype(out.value > 0? s << out.value : s)> 
{   
    return out.value > 0? s << out.value : s;
}

struct In {
    std::ostream &sink;
    template<typename T> In &operator<<(T const &t) {
        return sink << t, *this;
    }
};

int main() {
    In in{std::cout};
    in << (1 << Out{3}) << '\n';    // Ok
    in << Out{42} << '\n';
}

Personally I don't feel quite confident with that "only allow this in case it does not compile on its own" approach (is this perchance a (static) UB? what would I say if I were a C++ standard?)