Menu

Перегрузка операторов в C++: собственные операторы +, == и <<

Перегрузка операторов в C++ позволяет вашим собственным типам работать со встроенными операторами вроде +, == и <<. Узнайте правила выбора между функцией-членом и свободной функцией, как перегружать операторы сравнения и потока и какие подводные камни связаны с типами возврата и оператором присваивания.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Сделайте свои типы как встроенные

Вы уже знаете, что std::string позволяет писать a + b для конкатенации и cout << s для вывода. Это не особые трюки компилятора - это обычные функции со странными именами. Перегрузка операторов - это возможность, которая позволяет вашим классам подключаться к тому же синтаксису, так что тип Vector2 или Money можно складывать, сравнивать и выводить точно так же, как int.

Механизм прост, когда вы его поймёте: выражение вроде a + b - это сокращение. Компилятор переписывает его как вызов функции с именем operator+ и ищет ту, что подходит по типам операндов. Определите эту функцию для своего класса - и a + b внезапно заработает. По сути это специализированная форма перегрузки функций - действуют те же правила разрешения имён, просто с именами в форме операторов.

Обратите внимание, что функция принимает оба операнда по const&: арифметика не должна изменять свои входные данные, а ссылки избавляют от копирования. Она возвращает новый Vector2 по значению - p + q должно давать свежий результат, не трогая p или q, точно так же, как 2 + 3 не изменяет 2.

Член класса или свободная функция

Есть два места, где можно определить оператор: как член класса или как свободную (не входящую в класс) функцию. Как у члена, левый операнд - это неявный this, поэтому бинарный оператор принимает только один явный параметр:

const после списка параметров важен: a + b не должно изменять a, поэтому член помечается как const. Используйте форму члена для операторов, которые по своей сути привязаны к левому операнду и не нуждаются в преобразованиях над ним - +=, [], (), -> и унарных операторов вроде -x или ++x.

Подвох с членами: левый операнд нельзя преобразовать. С членом operator+ выше a + 50 работает (50 преобразуется в Money для правой стороны), но 50 + a не компилируется - левый операнд 50 - это int, а к int нельзя добавить функцию-член. Свободный оператор решает это, потому что оба операнда - явные параметры, и оба могут быть преобразованы:

Эмпирическое правило: делайте симметричные бинарные операторы (+, ==, *) свободными, чтобы преобразования работали с обеих сторон; делайте операторы, которые должны изменять левый операнд или привязаны к нему (+=, [], =), членами.

Перегрузка оператора потока

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

Две детали делают это возможным. Поток передаётся и возвращается по ссылке (ostream&) - потоки нельзя копировать, и именно возврат того же потока позволяет сцеплять cout << "p = " << p << "\n". Каждый << возвращает поток, чтобы следующему << было к чему привязаться. Забудьте return os; - и цепочка ломается.

Операторы сравнения

Чтобы сравнивать свои объекты с помощью ==, < и им подобных, перегрузите операторы сравнения. До C++20 каждый из них писали вручную; главный подвох в том, что operator< должен возвращать bool и задавать согласованный порядок:

Писать все шесть сравнений (==, !=, <, <=, >, >=) вручную утомительно и чревато ошибками. В C++20 добавили оператор трёхстороннего сравнения <=> («космический корабль»). Объявление его по умолчанию вместе с == генерирует за вас все сравнения:

= default указывает компилятору сравнивать члены в порядке их объявления, что в точности соответствует лексикографическому порядку, который вы написали бы вручную. На современных компиляторах предпочитайте именно этот вариант.

Оператор присваивания и его ловушки

operator= (копирующее присваивание) особенный: компилятор создаёт его за вас, и для простых классов этот вариант по умолчанию верен. Свой собственный нужно писать, только когда ваш класс управляет ресурсом - сырой памятью, дескриптором файла, - где почленное копирование было бы неверным. Каноническая сигнатура возвращает *this по ссылке, чтобы присваивания можно было сцеплять (a = b = c):

В этой короткой функции скрыты две ловушки. Первая - проверка на самоприсваивание if (this == &other): без неё a = a выполнило бы delete[] data, а затем читало бы из только что освобождённого other.data - неопределённое поведение. Вторая - важен порядок: в написанной вручную версии нельзя удалять старый буфер, пока вы не скопировали новый безопасно (реальная реализация часто сначала выделяет память или использует идиому copy-and-swap, так что неудачное выделение оставляет объект нетронутым).

Более широкий подвох: не перегружайте операторы неожиданным образом. operator+, который тайком изменяет свой левый операнд, или operator==, который не симметричен, запутают любого читателя и сломают код стандартной библиотеки, рассчитывающий на привычные значения. Перегружайте операторы, только когда операция действительно «похожа на сложение» или «похожа на равенство» для вашего типа.

Далее: Спецификаторы доступа

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

Часто задаваемые вопросы

Что такое перегрузка операторов в C++?

Перегрузка операторов позволяет определить, что значат встроенные операторы вроде +, == или << для ваших собственных типов. Вы пишете функцию со специальным именем - operator+, operator== и т. д. - и компилятор вызывает её всякий раз, когда оператор встречается с операндами вашего класса. Именно так string + string выполняет конкатенацию, а cout << obj выводит пользовательский объект.

Чем должны быть операторы в C++ - функциями-членами или свободными (friend) функциями?

Используйте функцию-член, когда левый операнд - ваш собственный класс и ему не нужны преобразования (например, +=, [], ()). Используйте свободную функцию (часто friend), когда левый операнд может быть встроенным типом или когда нужны симметричные преобразования с обеих сторон; для operator<< это обязательно, потому что левый операнд - это std::ostream, а не ваш класс.

Какие операторы C++ нельзя перегрузить?

Нельзя перегрузить :: (разрешение области видимости), . (доступ к члену), .* (доступ через указатель на член), ?: (тернарный) и sizeof. Также нельзя придумать совершенно новые операторы или изменить арность либо приоритет оператора - + всегда бинарный с одним и тем же приоритетом, складывает ли он int или ваш Vector2.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ