Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Templated operator overload resolution, member vs non-member function

When trying out clang-3.4 (compiled from git), it failed to compile one of my projects complaining about ambiguity while resolving overloaded operators. I turned out that there were two templated operators, one of which was declared as a member function, other as a non-member one, which both seems equally good match.

Following SSCCE demonstrates the situation:

#include <iostream>

struct ostr {
        std::ostream& s;

        template<class T>
        ostr& operator<<(const T& x) { s << x; return *this; }
};

struct xy {
        double x, y;
};

template<class Stream>
Stream& operator<<(Stream& s, const xy& x) {
        s << "[" << x.x << ", " << x.y << "]";
        return s;
}

int main() {
        ostr os{std::cout};
        xy x{4, 5};
        os << "Value is: " << x <<"\n";
}

The project compiled fine before and I checked again this SSCCE with several compilers (gcc 4.5, 4.6, 4.7, 4.8 and clang 3.3) and all of them compiled it without any warning (with -Wall -Wextra -pedantic). All compilers were set to C++11/C++0x standard. After adding ctor to ostr, it compiled fine even on MSVC 2012 and 2010)

Making both operator<<s non-member exhibits the ambiguity in all compilers (as expected)

After looking through the standard drafts (N3242 and N3690) I failed to find anything making member functions/operators better match than non-member ones.

So I failed to prove clang-3.4 is wrong and I wonder who's right. Thus my question is:

  • Is this code valid? Should member operators/functions be better match than non-member ones and it's a bug in clang-3.4?
  • Or are all the other compilers wrong/too permissive?

I am aware that changing the second operator<< to non-templated function (with std::ostream instead of template parameter) would resolve the the ambiguity and work as expected, but that's not the point here.

like image 800
v154c1 Avatar asked Nov 13 '13 14:11

v154c1


People also ask

Why is the overload not a member function?

Not everything can be overloaded as a member function Because the overloaded operator must be added as a member of the left operand. In this case, the left operand is an object of type std::ostream. std::ostream is fixed as part of the standard library.

Can we overload member functions?

Member function declarations with the same parameters or the same name types cannot be overloaded if any one of them is declared as a static member function.

What is overload resolution?

The process of selecting the most appropriate overloaded function or operator is called overload resolution. Suppose that f is an overloaded function name. When you call the overloaded function f() , the compiler creates a set of candidate functions.

What is member function template?

The term member template refers to both member function templates and nested class templates. Member function templates are function templates that are members of a class or class template.


1 Answers

Overload resolution adds an additional parameter to a member function just for the purpose of overload resolution:

[over.match.funcs]/2

The set of candidate functions can contain both member and non-member functions to be resolved against the same argument list. So that argument and parameter lists are comparable within this heterogeneous set, a member function is considered to have an extra parameter, called the implicit object parameter, which represents the object for which the member function has been called.

/4

For non-static member functions, the type of the implicit object parameter is

— “lvalue reference to cv X” for functions declared without a ref-qualifier or with the & ref-qualifier

— “rvalue reference to cv X” for functions declared with the && ref-qualifier

where X is the class of which the function is a member and cv is the cv-qualification on the member function declaration.

Some special rules follow, for example to allow binding an rvalue to this implicit object parameter (for calling member functions w/o ref-qualifier on rvalues, e.g. ostr{std::cout}<<"hello").


The function signatures including the implicit object parameter we need to compare for overload resolution are:

template<class T>
ostr& ostr::operator<<(ostr&, const T&);    // F1

template<class Stream>
Stream& ::operator<<(Stream&, const xy&);    // F2

After substitution for os << x, we get the same signature:

ostr& ostr::operator<<(ostr&, const xy&);
ostr& ::    operator<<(ostr&, const xy&);

So only one of the "tie-breakers" in [over.match.best]/1 could resolve the ambiguity. Indeed, one could apply, namely the "F1 is more specialized than F2" (or vice versa): partial ordering of function templates.

N.B. The procedure of adding an implicit object parameter is specified again in the description of partial ordering [temp.func.order]/3.


For partial ordering of F1 and F2 (as defined above), we first create two unique types:

struct unique_T {};
struct unique_Stream {};

Then we transform F1 into F1' by replacing the template parameter T with the unique type unique_T (and similarly for F2):

ostr& ostr::operator<<(ostr&, const unique_T&);
ostr& ::    operator<<(unique_Stream&, const xy&);

The parameters of the transformed function F1' are now used to try to deduce the template parameters of the untransformed F2:

ostr a0;
unique_T a1; // no reference, no cv-qualifier
::operator<<(a0, a1) // does template argument deduction succeed?

// reminder: signature of ::operator<<
template<class Stream>
Stream& ::operator<<(Stream&, const xy&);

The deduction succeeds for a0 [with Stream = ostr], therefore the type ostr& from F1 is considered to be at least as specialized as type of the corresponding first parameter of F2 (Stream&, with Stream being a template parameter). I'm not sure what happens to the second argument a1, since no deduction takes place for the second parameter of ::operator<< (it is of type const xy&).

Now we repeat the process with arguments from F2' and try to deduce the template parameters of F1:

unique_Stream a0;
xy a1;
ostr::operator<<(a0, a1);

// reminder: signature of ostr::operator<<
template<class T>
ostr& ostr::operator<<(ostr&, const T&);

Here, no deduction occurs for the first argument, but it occurs and succeeds for the second argument [with T = xy].

I conclude no function template is more specialized. Therefore overload resolution should fail due to ambiguity.

like image 186
dyp Avatar answered Sep 26 '22 03:09

dyp