Search code examples
c++language-lawyerc++20ambiguous

Ambiguous Overload only for MSVC


Consider the following code:

#include <vector>
#include <iostream>

struct MySqlStream {
    template<class T>
    MySqlStream &operator<<( const std::vector<T> &values ) {
        std::cout << "vector";
        return *this;
    }
};

template<class T>
MySqlStream &operator<<( MySqlStream &m, const T &x ) {
    std::cout << "template";
    return m;
}

int main() {
    std::vector<double> temp;

    MySqlStream sql;

    sql << temp;
}

Both g++ and clang accept the code (and use the vector version).

MSVC, on the other hand, rejects the code stating:

<source>(23): error C2593: 'operator <<' is ambiguous
<source>(6): note: could be 'MySqlStream &MySqlStream::operator <<<double>(const std::vector<double,std::allocator<double>> &)'
<source>(13): note: or       'MySqlStream &operator <<<std::vector<double,std::allocator<double>>>(MySqlStream &,const T &)'
        with
        [
            T=std::vector<double,std::allocator<double>>
        ]
<source>(23): note: while trying to match the argument list '(MySqlStream, std::vector<double,std::allocator<double>>)'

Is MSVC incorrect to reject this? How would I be able to work around it? Godbolt Link

Note: I can use sql.operator<<( temp ); but that isn't great for when there's a lot of things being concatenated together.


Solution

  • g++ and clang++ are correct, and MSVC is incorrect: the member operator<< should be selected because it is the more specialized template.

    The major steps in overload resolution are:

    1. Build a set of candidate functions. For an operator expression ([over.match.oper]), this can include:

      • member functions - "vector" function template 1 template<class T> MySqlStream & MySqlStream::operator<<(const std::vector<T> &);
      • non-member functions - "template" function template 2 template<class T> MySqlStream & ::operator<<(MySqlStream &, const T &);, and some std::operator<< function templates
      • built-in candidates - representing integer left shift
      • rewritten candidates - none in this case
    2. For each non-static member function [template] in the set, treat it as though it has an extra first parameter whose type is a reference to the class type ([over.match.funcs.general]/2). The corresponding argument is based on the expression left of the . or ->, which might involve an implicit this->. In this example,

      • function template 1 template<class T> MySqlStream & MySqlStream::operator<<(MySqlStream &, const std::vector<T> &);
    3. For each function template in the set, do template argument deduction to get a concrete function type ([over.match.funcs.general]/8, [temp.deduct]). Deduction fails for all the std::operator<<, so they're thrown out of the set. And we get:

      • function 1 MySqlStream & operator<<(MySqlStream &, const std::vector<double> &);
      • function 2 MySqlStream & operator<<(MySqlStream &, const std::vector<double> &);
    4. Check if each function is "viable" ([over.match.viable]). Roughly, this means it's valid to call the function based on parameter types, argument types, and constraints, but ignoring some other rules like private or protected access and deleted functions. Both MySqlStream functions are viable. The built-in candidates are not viable, and discarded from the set. At this point, only the two MySqlStream functions remain.

    5. Determine the best viable function ([over.match.best]). Here's the key piece in this example.

    Since the function types are identical at this point, most of the text about Implicit Conversion Sequences doesn't decide a better function. The important rule is [over.match.best.general]/(2.5):

    ... a viable function F1 is defined to be a better function than another viable function F2 if ..., and then

    • ..., or if not that,
    • F1 and F2 are function template specializations, and the function template for F1 is more specialized than the function template for F2 according to the partial ordering rules described in [temp.func.order], or if not that,
    • ...

    Again a non-static member function template is considered to have an extra first parameter which is a reference to the class type ([temp.func.order]/3). Although the exact rules for this aren't identical to the earlier step in [over.match.funcs.general]/2, in this case we again get the same

    • function template 1 template<class T> MySqlStream & MySqlStream::operator<<(MySqlStream &, const std::vector<T> &);

    Nothing else in [temp.func.order] or the related [temp.deduct.partial] mentions the special first parameter or first argument for a non-static member function, so the only remaining important difference is the function parameter types const std::vector<T> & vs. const T &. To simplify a little, let's now look at a related example dealing with those:

    template<class T>
    void g(const std::vector<T> &); // g1
    
    template<class T>
    void g(const T &); // g2
    
    void test() {
        std::vector<double> temp;
        g(temp);
    }
    

    The gist of [temp.deduct.partial] is to invent template arguments as general as possible for one template F1, and see if a call which exactly matches the resulting function specialization also matches the other original template F2. If it does, F1 is "at least as specialized as" F2, meaning that (almost) any time F1 is viable, F2 is also viable. Do this both ways, and if only one way succeeds, the function which was F1 in that test is more specialized.

    So first let's specialize g1: Invent a type T1 not related to any other declarations, and substitute it for T:

    struct T1 {};
    template void g1<T1>(const std::vector<T1> &);
    

    Next see if g2 can be called with exactly matching arguments. Since the parameter here is an lvalue reference, the argument should be an lvalue expression.

    const std::vector<T1> arg1;
    g2(arg1); // arg1 as lvalue
    

    Sure, g2 deduces its T to be std::vector<T1> and is viable. g1 is at least as specialized as g2.

    The other way around, we invent a type struct T2 {}; and substitute it into g2:

    struct T2 {};
    template void g2<T>(const T2 &);
    

    And can g1 be called with exactly matching arguments?

    const T2 arg2;
    g1(arg2); // arg2 as lvalue
    

    Nope, that const T2 argument is not any specialization of std::vector, so template argument deduction for g1 fails. g2 is not at least as specialized as g1. Together, these mean g1 is more specialized than g2.

    Similarly in the original example, the "vector" member function template 1 is the more specialized function template and the better viable function, and is selected by overload resolution for the << expression. The overload resolution is not ambiguous as MSVC claims.