Search code examples
c++operator-overloadingglobalmemberambiguity

How does the compiler know whether to use a member operator overload or a global operator overloads?


I have a question about c++ operators that I hope will find its answer here. The short version of the question is in the title, but if there's any doubt as to what I'm really asking, here's the long version.

c++ operators can be overloaded, so that it becomes possible to write stuff like this:

MyClass a(1), b(2);
Myclass c = a + b;

From what I understand, a typical way to implement that would look like this:

class MyClass
{
    private:
        int val;
    public:
        explicit MyClass(int _val);

        MyClass operator+(MyClass const& other) const;
        MyClass operator+(int i) const;
};

Which in this case also includes an overload with the type int, which makes it possible to write stuff like this:

MyClass a(1);
Myclass b = a + 2;

But not like this:

MyClass a(1);
Myclass b = 2 + a;

because it would be like calling 2.operator+(a), and 2 isn't an object. Since programmers would like to overload operators in a way that makes it possible, there's a second way of implementing them, which would look like this:

class MyClass
{
    private:
        int val;
    public:
        explicit MyClass(int _val);

        friend MyClass operator+(MyClass const& lhs, MyClass const& rhs);
        friend MyClass operator+(int lhs, MyClass const& rhs);
        friend MyClass operator+(MyClass const& lhs, int rhs);
};

which allows all three types of additions.

Now, what bothers me is: what if we implement both at the same time? How does the compiler decide whether to use the member operator or the global one?

To be fair, which operator is called shouldn't matter in any sensible implementation, and they should neither return different things nor have different side effects, but I tried to implement it to see what happens:

class MyClass
{
    private:
        int val;
    public:
        explicit MyClass(int _val) : val(_val){}

        MyClass operator+(MyClass const& other) const
        {
            cout << "Call to member operator+ for MyClass+MyClass" << endl;
            return MyClass(val + other.val);
        }

        MyClass operator+(int other) const
        {
            cout << "Call to member operator+ for MyClass+int" << endl;
            return MyClass(val + other);
        }

        friend MyClass operator+(MyClass const& lhs, MyClass const& rhs) 
        {
            cout << "Call to global operator+ for MyClass+MyClass " << endl;
            return MyClass(lhs.val + rhs.val);
        }

        friend MyClass operator+(int lhs, MyClass const& rhs) 
        {
            cout << "Call to global operator+ for int+MyClass " << endl;
            return MyClass(lhs + rhs.val);
        }

        friend MyClass operator+(MyClass const& lhs, int rhs) 
        {
            cout << "Call to global operator+ for MyClass+int " << endl;
            return MyClass(lhs.val + rhs);
        }
};


int main() {
    MyClass a(1), b(2);
    int i(3);
    MyClass r_0 = a.operator+(b);
    MyClass r_1 = a.operator+(i);
    MyClass r_2 = operator+(a,b);
    MyClass r_3 = operator+(a,i);
    MyClass r_4 = operator+(i,a);
    MyClass r_5 = a + b;
    MyClass r_6 = a + i;
    MyClass r_7 = i + a;

    return 0;
}

which compiles, and prints

Call to member operator+ for MyClass+MyClass
Call to member operator+ for MyClass+int
Call to global operator+ for MyClass+MyClass 
Call to global operator+ for MyClass+int 
Call to global operator+ for int+MyClass 
Call to global operator+ for MyClass+MyClass 
Call to global operator+ for MyClass+int 
Call to global operator+ for int+MyClass

I would be tempted to think that everything in this is legal and that the global operator has priority over the member one, but the only thing I could find about it online seemed to suggest that those additions were ambiguous calls, so is it really the case or am I just looking at undefined behaviour here?


Solution

  • Member and non-member functions participate in overload resolution on equal rights. In order to make them comparable, each member function is extended by the compiler with an implicit object parameter. ([over.match.funcs]/p2):

    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 first parameter, called the implicit object parameter, which represents the object for which the member function has been called. For the purposes of overload resolution, both static and non-static member functions have an implicit object parameter, but constructors do not.

    [over.match.funcs]/p5:

    During overload resolution, the implied object argument is indistinguishable from other arguments. The implicit object parameter, however, retains its identity since no user-defined conversions can be applied to achieve a type match with it.

    Given that the implicit object parameter also inherits ref- and cv-qualification from the non-static member function, this basically means that from the compiler perspective, the member operator declared as:

    MyClass MyClass::operator+(int) const;
    

    is to some extent equivalent to:

    MyClass operator+(const MyClass&, int);
    

    The only exceptions that make it different from a regular non-member function is that no user defined conversions are considered for the first (implicit object) parameter (which is why 1 + a will never convert 1 to A using some converting constructor A(int) in order to call A::operator+(const A&)), and that a temporary instance can be bound by a non-const reference generated for a non-const qualified member function.

    The member and global operator+ are ambiguous in your code and should produce an error as such.