13.5 – Перегрузка операторов, используя функции-члены

Добавлено 18 июля 2021 в 14:17
Глава 13 – Перегрузка операторов  (содержание)

В уроке «13.2 – Перегрузка арифметических операторов, используя дружественные функции» вы узнали, как перегрузить арифметические операторы с помощью дружественных функций. Вы также узнали, что можете перегружать операторы как обычные функции. Многие операторы можно перегружать еще одним способом: как функцию-член.

Перегрузка операторов, использующая функцию-член, очень похожа на перегрузку операторов, использующую дружественную функцию. При перегрузке оператора с помощью функции-члена:

  • перегруженный оператор должен быть добавлен как функция-член левого операнда;
  • левый операнд становится неявным объектом *this;
  • все остальные операнды становятся параметрами функции.

Напоминание, как мы перегрузили operator+ с помощью дружественной функции следующим образом:

#include <iostream>
 
class Cents
{
private:
    int m_cents;
 
public:
    Cents(int cents) { m_cents = cents; }
 
    // Перегрузка Cents + int
    friend Cents operator+(const Cents &cents, int value);
 
    int getCents() const { return m_cents; }
};
 
// обратите внимание: эта функция не является функцией членом!
Cents operator+(const Cents &cents, int value)
{
    return Cents(cents.m_cents + value);
}
 
int main()
{
    Cents cents1(6);
    Cents cents2 = cents1 + 2;
    std::cout << "I have " << cents2.getCents() << " cents.\n";
 
    return 0;
}

Преобразовать перегруженный оператор в виде дружественной функции в перегруженный оператор в виде функции-члена легко:

  1. перегруженный оператор определяется как функция-член вместо дружественной функции (Cents::operator+ вместо friend operator+);
  2. левый параметр удаляется, потому что теперь этот параметр становится неявным объектом *this;
  3. внутри тела функции все ссылки на левый параметр могут быть удалены (например, cents.m_cents становится m_cents, который неявно ссылается на объект *this).

Вот тот же оператор, перегруженный с помощью метода функции-члена:

#include <iostream>
 
class Cents
{
private:
    int m_cents;
 
public:
    Cents(int cents) { m_cents = cents; }
 
    // Перегрузка Cents + int
    Cents operator+(int value);
 
    int getCents() const { return m_cents; }
};
 
// обратите внимание: эта функция является функцией-членом!
// параметр cents в дружественной версии теперь неявный параметр *this
Cents Cents::operator+(int value)
{
    return Cents(m_cents + value);
}
 
int main()
{
    Cents cents1(6);
    Cents cents2 = cents1 + 2;
    std::cout << "I have " << cents2.getCents() << " cents.\n";
 
    return 0;
}

Обратите внимание, что использование оператора не меняется (в обоих случаях cents1 + 2), мы просто определили функцию по-другому. Наша дружественная функция с двумя параметрами становится функцией-членом с одним параметром, причем крайний левый параметр в версии дружественной функции (cents) становится неявным параметром *this в версии функции-члена.

Давайте подробнее рассмотрим, как вычисляется выражение cents1 + 2.

В версии дружественной функции выражение cents1 + 2 становится вызовом функции operator+(cents1, 2). Обратите внимание, что здесь два параметра функции. Всё просто.

В версии с функцией-членом выражение cents1 + 2 становится вызовом функции cents1.operator+(2). Обратите внимание, что теперь здесь только один явный параметр функции, а cents1 стал префиксом объекта. Однако в уроке «12.10 – Скрытый указатель "this"» мы упоминали, что компилятор неявно преобразует префикс объекта в скрытый крайний левый параметр с именем *this. Таким образом, на самом деле cents1.operator+(2) превращается в operator+(&cents1, 2), что почти идентично версии с дружественной функцией.

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

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

Не всё можно перегрузить как дружественную функцию

Операторы присваивания (=), индекса ([]), вызова функции (()) и выбора члена (->) должны быть перегружены как функции-члены, потому что так требует язык.

Не всё можно перегрузить как функцию-член

В уроке «13.4 – Перегрузка операторов ввода/вывода» мы перегружали operator<< для нашего класса Point, используя метод дружественной функции. Вот напоминание о том, как мы это сделали:

#include <iostream>
 
class Point
{
private:
    double m_x, m_y, m_z;
 
public:
    Point(double x=0.0, double y=0.0, double z=0.0): m_x(x), m_y(y), m_z(z)
    {
    }
 
    friend std::ostream& operator<< (std::ostream &out, const Point &point);
};
 
std::ostream& operator<< (std::ostream &out, const Point &point)
{
    // Поскольку operator<< является другом класса Point,
    // мы можем напрямую обращаться к членам класса Point.
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")";
 
    return out;
}
 
int main()
{
    Point point1(2.0, 3.0, 4.0);
 
    std::cout << point1;
 
    return 0;
}

Однако мы не можем перегрузить operator<< как функцию-член. Почему? Потому что перегруженный оператор должен быть добавлен как член левого операнда. В этом случае левый операнд – это объект типа std::ostream. std::ostream закреплен как часть стандартной библиотеки. Мы не можем изменить объявление этого класса, чтобы добавить перегрузку в качестве функции-члена std::ostream.

Это требует, чтобы operator<< был перегружен как обычная функция (предпочтительно) или как дружественная функция.

Точно так же, хотя мы можем перегрузить operator+(Cents, int) как функцию-член (как мы это сделали выше), мы не можем перегрузить operator+(int, Cents) как функцию-член, поскольку int не является классом, к которому мы можем добавлять члены.

Обычно мы не можем использовать перегрузку с помощью функций-членов, если левый операнд либо не является классом (например, int), либо это класс, который мы не можем изменить (например, std::ostream).

Когда использовать перегрузку с обычной функцией, дружественной функцией или функцией-членом

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

При работе с бинарными операторами, которые не изменяют левый операнд (например, operator+), обычно предпочтительнее версия с обычной или дружественной функцией, поскольку она работает для всех типов параметров (даже если левый операнд не является объектом класса или является объектом класса, который нельзя изменить). Версия с обычной или дружественной функцией имеет дополнительное преимущество «симметрии», поскольку все операнды становятся явными параметрами (вместо того, чтобы левый операнд стал *this, а правый операнд стал явным параметром).

При работе с бинарными операторами, которые изменяют левый операнд (например, operator+=), обычно предпочтительнее использовать версию с функцией-членом. В этих случаях крайний левый операнд всегда будет принадлежать типу класса, и модифицируемый объект естественно станет тем, на который указывает *this. Поскольку крайний правый операнд становится явным параметром, не возникает путаницы в отношении того, кто изменяется, а кто вычисляется.

Унарные операторы обычно также перегружаются как функции-члены, поскольку версия с функцией-членом не имеет параметров.

Следующие практические правила помогут вам определить, какая форма лучше всего подходит для заданной ситуации:

  • Если вы перегружаете присваивание (=), индекс ([]), вызов функции (()) или выбор члена (->), сделайте это как функцию-член.
  • Если вы перегружаете унарный оператор, сделайте это как функцию-член.
  • Если вы перегружаете бинарный оператор, который не изменяет свой левый операнд (например, operator+), сделайте это как обычную функцию (предпочтительно) или как дружественную функцию.
  • Если вы перегружаете бинарный оператор, который изменяет свой левый операнд, но вы не можете изменить определение левого операнда (например, operator<<, который имеет левый операнд типа ostream), сделайте это как обычную функцию (предпочтительно) или как дружественную функцию.
  • Если вы перегружаете бинарный оператор, изменяющий свой левый операнд (например, operator+=), и вы можете изменить определение левого операнда, сделайте это как функцию-член.

Теги

C++ / CppLearnCppДля начинающихОбучениеОператор (программирование)Перегрузка (программирование)Перегрузка операторовПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.