13.5 – Перегрузка операторов, используя функции-члены
В уроке «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 ¢s, int value);
int getCents() const { return m_cents; }
};
// обратите внимание: эта функция не является функцией членом!
Cents operator+(const Cents ¢s, 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;
}
Преобразовать перегруженный оператор в виде дружественной функции в перегруженный оператор в виде функции-члена легко:
- перегруженный оператор определяется как функция-член вместо дружественной функции (
Cents::operator+
вместоfriend operator+
); - левый параметр удаляется, потому что теперь этот параметр становится неявным объектом
*this
; - внутри тела функции все ссылки на левый параметр могут быть удалены (например,
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+(¢s1, 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+=
), и вы можете изменить определение левого операнда, сделайте это как функцию-член.