Перегрузка операторов в программировании — один из способов реализации полиморфизма, заключающийся в возможности одновременного существования в одной области видимости нескольких различных вариантов применения операторов, имеющих одно и то же имя, но различающихся типами параметров, к которым они применяются и используются.
Терминология Термин «перегрузка» — это калька английского слова . Такой перевод появился в книгах по языкам программирования в первой половине 1990-х годов. В изданиях советского периода аналогичные механизмы назывались переопределением или повторным определением, перекрытием операций.
Причины появления Иногда возникает потребность описывать и применять к созданным программистом типам данных операции, по смыслу эквивалентные уже имеющимся в языке. Классический пример — библиотека для работы с комплексными числами. Они, как и обычные числовые типы, поддерживают арифметические операции, и естественным было бы создать для данного типа операции «плюс», «минус», «умножить», «разделить», обозначив их теми же самыми знаками операций, что и для других числовых типов. Запрет на использование определённых в языке элементов вынуждает создавать множество функций с именами вида ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat и так далее.
Когда одинаковые по смыслу операции применяются к операндам различных типов, их вынужденно приходится называть по-разному. Невозможность применять для разных типов функции с одним именем приводит к необходимости выдумывать различные имена для одного и того же, что создаёт путаницу, а может и приводить к ошибкам. Например, в классическом языке Си существует два варианта стандартной библиотечной функции нахождения модуля числа: abs() и fabs() — первый предназначен для целого аргумента, второй — для вещественного. Такое положение, в сочетании со слабым контролем типов Си, может привести к труднообнаруживаемой ошибке: если программист напишет в вычислении abs(x), где x — вещественная переменная, то некоторые компиляторы без предупреждений сгенерируют код, который будет преобразовывать x к целому путём отбрасывания дробной части и вычислять модуль от полученного целого числа.
Отчасти проблема решается средствами объектного программирования — когда новые типы данных объявляются как классы, операции над ними могут быть оформлены как методы классов, в том числе и одноимённые (поскольку методы разных классов не обязаны иметь различные имена), но, во-первых, оформление подобным образом операций над значениями разных типов неудобно, а во-вторых, это не решает проблему создания новых операторов.
Средства, позволяющие расширять язык, дополнять его новыми операциями и синтаксическими конструкциями (а перегрузка операций является одним из таких средств, наряду с объектами, макрокомандами, функционалами, замыканиями) превращают его уже в метаязык — средство описания языков, ориентированных на конкретные задачи. С его помощью можно для каждой конкретной задачи построить языковое расширение, наиболее ей соответствующее, которое позволит описывать её решение в наиболее естественной, понятной и простой форме. Например, в приложении к перегрузке операций: создание библиотеки сложных математических типов (векторы, матрицы) и описание операций с ними в естественной, «математической» форме, создаёт «язык для векторных операций», в котором сложность вычислений скрыта, и возможно описывать решение задач в терминах векторных и матричных операций, концентрируясь на сути задачи, а не на технике. Именно из этих соображений подобные средства были в своё время включены в язык Алгол-68.
Механизм перегрузки
Реализация Перегрузка операций предполагает введение в язык двух взаимосвязанных особенностей: возможности объявлять в одной области видимости несколько процедур или функций с одинаковыми именами и возможности описывать собственные реализации бинарных операторов (то есть знаков операций, обычно записываемых в инфиксной нотации, между операндами). Принципиально реализация их достаточно проста: Чтобы разрешить существование нескольких одноимённых операций, достаточно ввести в язык правило, согласно которому операция (процедура, функция или оператор) опознаются компилятором не только по имени (обозначению), но и по типам их параметров. Так например в C++ abs(int i), где i объявлено как целое, и abs(float x), где x объявлено как вещественное — это две разные операции. Принципиально в обеспечении именно такой трактовки нет никаких сложностей. Чтобы дать возможность определять и переопределять операции, необходимо ввести в язык соответствующие синтаксические конструкции. Вариантов их может быть достаточно много, но по сути они ничем друг от друга не отличаются, — достаточно помнить, что запись вида «<операнд1> <знакОперации> <операнд2>» принципиально аналогична вызову функции «<знакОперации>(<операнд1>,<операнд2>)». Достаточно разрешить программисту описывать поведение операторов в виде функций, — и проблема описания решена.
Перегрузка операторов в C++ В C++ можно выделить четыре типа перегрузок операторов: Перегрузка обычных операторов + - * / % ˆ & | ~ ! = < > += -= *= /= %= ˆ= &= |= << >> >>= <<= == != <= >= && || ++ -- , ->* -> ( ) <=> [ ] Перегрузка операторов преобразования типа Перегрузка операторов размещения '''new''' и уничтожения '''delete''' объектов в памяти. Перегрузка литералов operator""
Обычные операторы Важно помнить, что перегрузка расширяет возможности языка, а не изменяет язык, поэтому перегружать операторы для встроенных типов нельзя. Нельзя менять приоритет и ассоциативность (слева направо или справа налево) операторов. Нельзя создавать собственные операторы и перегружать некоторые встроенные: :: . .* ?: sizeof typeid. Также операторы && || , теряют свои уникальные свойства при перегрузке: ленивость для первых двух и очерёдность для запятой (порядок выражений между запятыми строго определён, как лево-ассоциативный, то есть слева-направо). Оператор -> должен возвращать либо указатель, либо объект (по копии или ссылке).
Операторы могут быть перегружены и как отдельные функции, и как функции-члены класса. Во втором случае левым аргументом оператора всегда выступает объект *this. Операторы = -> [] () могут быть перегружены только как методы (функции-члены), но не как функции.
Можно сильно облегчить написание кода, если производить перегрузку операторов в определённом порядке. Это не только ускорит написание, но и избавит от дублирования одного и того же кода. Рассмотрим перегрузку на примере класса, представляющего собой геометрическую точку в двумерном векторном пространстве: class Point { int x, y; public: Point(int x, int xx): x(x), y(xx) {} // Конструктор по-умолчанию исчез. // Имена аргументов конструктора могут совпадать с именами полей класса. } Операторы присваивания копированием и перемещением operator=Стоит учитывать, что по-умолчанию C++ помимо конструктора создаёт пять базовых функций. Поэтому перегрузку операторов присваивания копированием и перемещением лучше поручить компилятору или реализовать с помощью идиомы Copy-and-swap. Комбинированные арифметические операторы += *= -= /= %= и т. д. Если мы хотим реализовать обычные бинарные арифметические операторы, удобнее будет реализовать вначале данную группу операторов. Point& Point::operator+=(const Point& rhs) { x += rhs.x; y += rhs.y; return *this; } Оператор возвращает значение по ссылке, это позволяет писать такие конструкции: (a += b) += c; Арифметические операторы + * - / %Чтобы избавиться от повторения кода, воспользуемся нашим комбинированным оператором. Оператор не модифицирует объект, поэтому возвращает новый объект. const Point Point::operator+(const Point& rhs) const { return Point(*this) += rhs; } Оператор возвращает const значение. Это защитит нас от написания конструкций подобного вида (a + b) = c;. С другой стороны, для классов, копирование которых дорого обходится, гораздо выгоднее возвращать значение по неконстантной копии, то есть : MyClass MyClass::operator+(const MyClass& rhs) const;. Тогда при такой записи x = y + z; будет вызван конструктор перемещения, а не копирования. Унарные арифметические операторы + - Унарные плюс и минус не принимают аргументов при перегрузке. Они не изменяют сам объект (в нашем случае), а возвращают новый изменённый объект. Следует перегрузить и их, если перегружены их бинарные аналоги. Point Point::operator+() { return Point(*this); } Point Point::operator-() { Point tmp(*this); tmp.x *= -1; tmp.y *= -1; return tmp; } Операторы сравнения == != < <= > >=Первыми следует перегрузить операторы равенства и неравенства. Оператор неравенства будет использовать оператор равенства. bool Point::operator==(const Point& rhs) const { return (this->x == rhs.x && this->y == rhs.y); } bool Point::operator!=(const Point& rhs) const { return !(*this == rhs); } Следом перегружаются операторы < и >, а затем их нестрогие аналоги, с помощью ранее перегруженных операторов. Для точек в геометрии такая операция не определена, поэтому в данном примере нет смысла их перегружать. Побитовые операторы <<= >>= &= |= ^= и << >> & | ^ ~На них распространяется те же принципы, что и на арифметические. В некоторых классах пригодится использование битовой маски std::bitset. Внимание: оператор & имеет унарный аналог и используется для взятия адреса; обычно не перегружается. Логические операторы && || Эти операторы потеряют свои уникальные свойства ленивости при перегрузке. Инкремент и декремент ++ -- C++ позволяет перегрузить как постфиксные, так и префиксные инкремент и декремент. Рассмотрим инкремент: Point& Point::operator++() { //префиксный x++; y++; return *this; } Point Point::operator++(int) { //постфиксный Point tmp(x,y,i); ++(*this); return tmp; } Заметим, что функция-член operator++(int) принимает значение типа int, но у этого аргумента нет имени. C++ позволяет создавать такие функции. Мы можем присвоить ему (аргументу) имя и увеличивать значения точек на этот коэффициент, однако в операторной форме этот аргумент по-умолчанию будет равен нулю и вызывать его можно будет только в функциональном стиле: A.operator++(5); Оператор () не имеет ограничений на тип возвращаемого значения и типы/количество аргументов и позволяет создавать функторы. Оператор передачи класса в поток вывода. Реализуется в виде отдельной функции, а не функции-члена. В классе эта функция помечается как дружественная.friend std::ostream& operator<<(const ostream& s, const Point& p);
На остальные операторы не распространяются какие-либо общие рекомендации к перегрузке.
Преобразования типов Преобразования типов позволяют задать правила преобразования нашего класса к другим типам и классам. Также можно указать спецификатор explicit, который позволит преобразовывать типы только, если программист явно это указал (например static_cast<Point3>(Point(2,3));). Пример: Point::operator bool() const { return this->x != 0 || this->y != 0; }
Операторы аллокации и деаллокации
Операторы , , и могут быть перегружены и могут принимать произвольное количество дополнительных аргументов.
Операторы и обязаны первым параметром принимать аргумент типа и возвращать указатель типа . Операторы и первым параметром принимают и не возвращают значения ().
Эти операторы могут быть перегружены как глобально (для всех классов), так и внутри конкретного класса для управления его собственной памятью.
Пример перегрузки оператора new #include <cstdlib> #include <new> #include <iostream>
class MyClass { public: void* operator new(std::size_t s, int factor) { void* p = std::malloc(s * factor); if (!p) throw std::bad_alloc(); std::cout << "Выделено " << (s * factor) << " байт\n"; return p;
void operator delete(void* p) noexcept { std::cout << "Освобождение памяти\n"; std::free(p);
int main() { MyClass* obj = new(12) MyClass; // перегруженный new с дополнительным параметром delete obj; // вызов перегруженного delete }
Особенности