Как компилятор узнает, использовать ли перегрузку оператора-члена или глобальные перегрузки оператора?

У меня есть вопрос об операторах C++, который, я надеюсь, найдет здесь ответ. Краткая версия вопроса есть в названии, но если есть какие-либо сомнения относительно того, что я действительно спрашиваю, вот длинная версия.

Операторы C++ могут быть перегружены, так что становится возможным писать такие вещи:

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

Из того, что я понимаю, типичный способ реализации, который будет выглядеть так:

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

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

Который в этом случае также включает перегрузку с типом int, что позволяет писать такие вещи:

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

Но не так:

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

потому что это было бы как звонить 2.operator+(a), а также 2 не объект Поскольку программисты хотели бы перегружать операторы таким образом, чтобы это было возможно, существует второй способ их реализации, который будет выглядеть следующим образом:

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);
};

что позволяет все три типа дополнений.

Теперь, что беспокоит меня: что, если мы реализуем оба одновременно? Как компилятор решает, использовать ли оператор члена или глобальный?

Если честно, то, какой оператор вызывается, не имеет значения в любой разумной реализации, и они не должны ни возвращать разные вещи, ни иметь разные побочные эффекты, но я попытался реализовать это, чтобы увидеть, что происходит:

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;
}

который компилирует и печатает

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

Я был бы соблазн думать, что все в этом законно и что глобальный оператор имеет приоритет над членом-участником, но единственное, что я мог найти об этом в Интернете, казалось, предположить, что эти дополнения были неоднозначными вызовами, так что это действительно так или я просто смотрю на неопределенное поведение здесь?

1 ответ

Решение

Функции-члены и не-члены участвуют в разрешении перегрузки на равных правах. Чтобы сделать их сопоставимыми, каждая функция-член расширяется компилятором с неявным параметром объекта. ( [over.match.funcs] / p2):

Набор функций-кандидатов может содержать как функции-члены, так и функции, не являющиеся членами, которые должны быть разрешены для одного и того же списка аргументов. Чтобы списки аргументов и параметров были сопоставимы в этом гетерогенном наборе, считается, что функция-член имеет дополнительный первый параметр, называемый параметром неявного объекта, который представляет объект, для которого была вызвана функция-член. В целях разрешения перегрузки как статические, так и нестатические функции-члены имеют неявный параметр объекта, а конструкторы - нет.

[over.match.funcs] / p5:

Во время разрешения перегрузки подразумеваемый объектный аргумент неотличим от других аргументов. Однако неявный параметр объекта сохраняет свою идентичность, так как никакие пользовательские преобразования не могут быть применены для достижения соответствия типа с ним.

Учитывая, что неявный параметр объекта также наследует ref- и cv-квалификации от нестатической функции-члена, это в основном означает, что с точки зрения компилятора оператор-член объявлен как:

MyClass MyClass::operator+(int) const;

в некоторой степени эквивалентно:

MyClass operator+(const MyClass&, int);

Единственное исключение, которое отличает его от обычной функции, не являющейся членом, состоит в том, что для первого (неявного объекта) параметра не учитываются определенные пользователем преобразования (именно поэтому 1 + a никогда не будет конвертировать 1 в A используя какой-то конвертирующий конструктор A(int) для того, чтобы позвонить A::operator+(const A&)) и что временный экземпляр может быть связан неконстантной ссылкой, сгенерированной для неконстантной квалифицированной функции-члена.

Член и глобальный operator+ неоднозначны в вашем коде и должны выдавать ошибку как таковую.

Другие вопросы по тегам