13.1 – Введение в перегрузку операторов
В уроке «8.9 – Введение в перегрузку функций» вы узнали о перегрузке функций, которая предоставляет механизм для создания и разрешения вызовов для нескольких функций с одним и тем же именем, если каждая из этих функций имеет уникальный прототип. Это позволяет вам создавать разные вариации одной функции для работы с разными типами данных, без необходимости придумывать уникальное имя для каждого варианта.
В C++ операторы реализованы как функции. Используя перегрузку для функций операторов, вы можете определять свои собственные версии этих операторов, которые работают с разными типами данных (включая классы, которые вы написали). Использование перегрузки функций для операторов называется перегрузкой операторов.
В данной главе мы рассмотрим темы, связанные с перегрузкой операторов.
Операторы как функции
Рассмотрим следующий пример:
int x = 2;
int y = 3;
std::cout << x + y << '\n';
Компилятор поставляется со встроенной версией оператора плюс (+
) для целочисленных операндов – эта функция складывает целые числа x
и y
и возвращает целочисленный результат. Когда вы видите выражение x + y
, вы можете мысленно перевести его в вызов функции operator+(x, y)
(где operator+
– это имя функции).
Теперь рассмотрим похожий фрагмент:
double z = 2.0;
double w = 3.0;
std::cout << w + z << '\n';
Компилятор также имеет встроенную версию оператора плюса (+
) для операндов double
. Выражение w + z
становится вызовом функции operator+(w, z)
, и перегрузка функций используется для определения того, что компилятор должен вызывать версию этой функции для double
вместо версии для int
.
Теперь посмотрим, что произойдет, если мы попытаемся сложить два объекта пользовательского класса:
Mystring string1 = "Hello, ";
Mystring string2 = "World!";
std::cout << string1 + string2 << '\n';
Что вы ожидаете в этом случае? Интуитивно ожидаемый результат – на экране будет напечатана строка "Hello, World!". Однако, поскольку Mystring
является пользовательским классом, компилятор не имеет встроенной версии оператора плюс, который он может использовать для операндов Mystring
. Поэтому в этом случае он выдаст нам ошибку. Чтобы заставить этот код работать так, как мы хотим, нам нужно написать перегруженную функцию, чтобы сообщить компилятору, как оператор +
должен работать с двумя операндами типа Mystring
. В следующем уроке мы рассмотрим, как это сделать.
Разрешение вызовов перегруженных операторов
При вычислении выражения, содержащего оператор, компилятор использует следующие правила:
- Если все операнды принадлежат базовым типам данных, компилятор вызовет встроенную подпрограмму, если она существует. Если она не существует, компилятор выдаст ошибку компиляции.
- Если любой из операндов принадлежит пользовательскому типу данных (например, одному из ваших классов или типу перечисления), компилятор проверяет, есть ли у этого типа подходящая перегруженная функция оператора, которую он может вызвать. Если он не может ее найти, он попытается преобразовать один или несколько операндов пользовательского типа в базовые типы данных, чтобы можно было использовать соответствующий встроенный оператор (через перегруженное приведение типов, которое мы рассмотрим в этой главе позже). Если это не удастся, произойдет ошибка компиляции.
Какие есть ограничения на перегрузку операторов?
Во-первых, почти любой существующий оператор в C++ может быть перегружен. Исключениями являются: условный оператор (?:
), sizeof
, оператор разрешения области видимости (::
), оператор выбора члена (.
), оператор выбора указателя на член (.*
), typeid
и операторы приведения типов.
Во-вторых, вы можете перегрузить только существующие операторы. Вы не можете создавать новые операторы или переименовывать существующие операторы. Например, вы не можете создать оператор **
для возведения в степень.
В-третьих, по крайней мере, один из операндов в перегруженном операторе должен быть пользовательского типа. Это означает, что вы не можете перегрузить оператор плюс для работы с одним числом int
и одним числом double
. Однако вы можете перегрузить оператор плюс для работы с числом int
и Mystring
.
В-четвертых, невозможно изменить количество операндов, поддерживаемых оператором.
Наконец, все операторы сохраняют свой приоритет и ассоциативность по умолчанию (независимо от того, для чего они используются), и это не может быть изменено.
Некоторые начинающие программисты пытаются перегрузить побитовый оператор исключающее ИЛИ (^
) для возведения в степень. Однако в C++ оператор ^
имеет более низкий приоритет, чем основные арифметические операторы, что приводит к неправильному вычислению выражений.
В математике возведение в степень вычисляется до базовой арифметики, поэтому 4 + 3 ^ 2 вычисляется как 4 + (3 ^ 2) => 4 + 9 => 13.
Однако в C++ арифметические операторы имеют более высокий приоритет, чем оператор ^
, поэтому 4 + 3 ^ 2 вычисляется как (4 + 3) ^ 2 => 7 ^ 2 => 49.
Чтобы всё работало правильно, вам будет нужно каждый раз явно заключать в скобки фрагмент возведения в степень (например, 4 + (3 ^ 2)), что не является интуитивно понятным и потенциально подвержено ошибкам.
Из-за этой проблемы с приоритетом, как правило, рекомендуется использовать операторы только так, как они были изначально задуманы.
Лучшая практика
При перегрузке операторов лучше всего сохранять функции операторов как можно ближе к исходному назначению операторов.
Кроме того, поскольку у операторов нет описательных имен, не всегда понятно, для чего они предназначены. Например, оператор +
может быть разумным выбором для строкового класса для объединения строк. А как насчет оператора -
? Что вы ожидаете от него? Непонятно.
Лучшая практика
Если значение оператора при применении к пользовательскому классу интуитивно непонятно, используйте вместо него именованную функцию.
При этих ограничениях вы по-прежнему найдете множество полезных функций, которые можно перегрузить для своих пользовательских классов! Вы можете перегрузить оператор +
, чтобы объединять объекты пользовательского строкового класса, или складывать два объекта класса Fraction
. Вы можете перегрузить оператор <<
, чтобы упростить вывод вашего класса на экран (или в файл). Вы можете перегрузить оператор равенства (==
), чтобы сравнить два объекта класса. Это делает перегрузку операторов одной из самых полезных функций C++ просто потому, что она позволяет вам работать с вашими классами более интуитивно понятным способом.
В следующих уроках мы более подробно рассмотрим перегрузку различных типов операторов.