Сделайте свои типы как встроенные
Вы уже знаете, что 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.