Перегрузка операторов в C++ — это возможность задать смысл встроенных операторов (+, ==, <<, [] и других) для своих типов: вы пишете функцию с именем operator+, и выражение a + b с объектами вашего класса вызывает ее. Новых операторов так не создать, а приоритет и число операндов остаются как у встроенных.
Содержание
- Минимальный пример: вектор на плоскости
- Синтаксис: как компилятор читает выражение
- Метод класса или свободная функция
- Что нельзя перегружать и какие правила действуют
- Инкремент, индексация и вызов как функции
- Сравнения в C++20: operator<=>
- Когда перегрузка вредит
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — полный рабочий пример, правило выбора «метод класса или свободная функция», список того, что перегружать нельзя, и сравнения через operator<=> из C++20. Примеры проверены компилятором Apple clang 21 с флагом -std=c++20 24.09.2026.
Три близких термина, которые часто путают:
| Термин | Что это | Пример |
|---|---|---|
| Перегрузка функций | несколько функций с одним именем и разными параметрами | print(int) и print(double) |
| Перегрузка операторов | частный случай перегрузки функций: имя функции — operator@ |
operator+(Vec2, Vec2) |
| Переопределение (override) | замена виртуального метода базового класса в наследнике | void draw() override |
Минимальный пример: вектор на плоскости
Сначала весь код целиком, потом разбор. Сохраните как vec.cpp, соберите g++ -std=c++20 vec.cpp или clang++ -std=c++20 vec.cpp и запустите.
#include <iostream>
struct Vec2 {
double x = 0, y = 0;
// составное присваивание - член класса, меняет *this
Vec2& operator+=(const Vec2& rhs) {
x += rhs.x;
y += rhs.y;
return *this;
}
// унарный минус - член, возвращает новый объект
Vec2 operator-() const { return {-x, -y}; }
// бинарный + через += : свободная функция (hidden friend)
friend Vec2 operator+(Vec2 lhs, const Vec2& rhs) {
lhs += rhs;
return lhs;
}
// умножение на число в обе стороны
friend Vec2 operator*(Vec2 v, double k) { return {v.x * k, v.y * k}; }
friend Vec2 operator*(double k, Vec2 v) { return v * k; }
// C++20: == по всем полям генерирует компилятор, != получаем бесплатно
bool operator==(const Vec2&) const = default;
// вывод в поток - только свободная функция
friend std::ostream& operator<<(std::ostream& os, const Vec2& v) {
return os << '(' << v.x << ", " << v.y << ')';
}
};
int main() {
Vec2 a{1, 2}, b{3, 4};
Vec2 c = a + b;
std::cout << c << '\n'; // (4, 6)
std::cout << 2 * a << '\n'; // (2, 4)
std::cout << -b << '\n'; // (-3, -4)
c += a;
std::cout << c << '\n'; // (5, 8)
std::cout << std::boolalpha << (a != b) << '\n'; // true
}
В консоли появятся пять строк:
(4, 6)
(2, 4)
(-3, -4)
(5, 8)
true
Что здесь происходит:
a + bкомпилятор превращает в вызовoperator+(a, b). Если бы+был методом, вызов выглядел бы какa.operator+(b): левый операнд становится неявнымthis.+=меняет сам объект и возвращает ссылкуVec2&, как встроенный+=. На его основе+написан в три строки — так логика сложения живет в одном месте.- Унарный минус помечен
const: он не меняет исходный вектор, а возвращает новый. operator==с= defaultсравнивает поля по порядку. Начиная с C++20 выражениеa != bкомпилятор сам переписывает в!(a == b), отдельный!=не нужен.
Синтаксис: как компилятор читает выражение
Функция-оператор — обычная функция со специальным именем. Число параметров зависит от того, где она объявлена:
| Вид оператора | Метод класса | Свободная функция |
|---|---|---|
Бинарный (a @ b) |
1 параметр (правый), левый — this |
2 параметра |
Унарный (@a) |
0 параметров | 1 параметр |
Постфиксный a++ |
1 фиктивный int |
2 параметра: объект и фиктивный int |
Hidden friend — функция, объявленная через friend прямо внутри класса. Она свободная (не метод), видит закрытые поля и находится компилятором только когда среди аргументов есть объект этого класса (поиск по аргументам, ADL). Это распространенный способ объявлять симметричные операторы.
Метод класса или свободная функция
Главный критерий — кто стоит слева. Если метод, левым операндом обязан быть объект вашего класса. Покажем, к чему это приводит.
Неверно — умножение на число только методом:
struct Vec2 {
double x = 0, y = 0;
Vec2 operator*(double k) const { return {x * k, y * k}; }
};
int main() {
Vec2 a{1, 2};
Vec2 b = a * 2; // ok: a.operator*(2)
Vec2 c = 2 * a; // ошибка
}
Результат — ошибка компиляции (текст clang 21; у GCC формулировка другая, например no match for 'operator*'):
err1.cpp:9:16: error: invalid operands to binary expression ('int' and 'Vec2')
Исправление — свободная функция для порядка «число слева», как friend Vec2 operator*(double k, Vec2 v) в первом примере. Сводное правило:
| Оператор | Как объявлять | Почему |
|---|---|---|
=, (), [], ->, операторы преобразования |
только метод | так требует язык |
+=, -=, ++, --, унарные -, ! |
обычно метод | меняют объект или относятся к одному объекту |
+, -, *, ==, <=> |
обычно свободная функция (часто hidden friend) | операнды равноправны, преобразования работают для обеих сторон |
<<, >> с потоком |
только свободная функция | левый операнд — std::ostream/std::istream, его класс вы не меняете |
Вторая и третья строки — соглашение, а не требование: == методом тоже скомпилируется, но в C++17 и раньше 2 == v при неявном преобразовании из числа работал только со свободной функцией.
Что нельзя перегружать и какие правила действуют
Нельзя перегрузить: ::, ., .*, ?:, sizeof, typeid, alignof, noexcept, а также static_cast и другие именованные приведения. Преобразование своего класса в другой тип задается отдельно — функцией-членом operator T(), лучше с explicit.
Остальные ограничения:
- новый оператор придумать нельзя:
operator**не существует; - приоритет, ассоциативность и число операндов не меняются:
a + b * cдля ваших типов по-прежнемуa + (b * c); - хотя бы один операнд должен быть классом или перечислением — поменять смысл
int + intнельзя; - перегруженные
&&и||теряют короткое замыкание: оба операнда вычисляются всегда (порядок слева направо гарантирован с C++17), поэтому их почти не перегружают.
Что говорит компилятор (clang 21), если нарушить правила:
struct A { int v; };
A& operator=(A& a, int x) { a.v = x; return a; }
int operator+(int a, int b) { return a - b; }
bad.cpp:2:4: error: overloaded 'operator=' must be a non-static member function
bad.cpp:3:5: error: overloaded 'operator+' must have at least one parameter of class or enumeration type
Инкремент, индексация и вызов как функции
У ++ и -- две формы, и различает их только фиктивный параметр int:
#include <iostream>
#include <vector>
class Counter {
int value_ = 0;
public:
Counter& operator++() { // префикс: ++c
++value_;
return *this;
}
Counter operator++(int) { // постфикс: c++ (int - фиктивный)
Counter old = *this;
++value_;
return old; // копия старого значения
}
int get() const { return value_; }
};
class Row {
std::vector<int> data_;
public:
explicit Row(std::size_t n) : data_(n) {}
int& operator[](std::size_t i) { return data_[i]; } // запись
const int& operator[](std::size_t i) const { return data_[i]; } // чтение у const
};
struct Scale {
double k;
double operator()(double x) const { return x * k; } // функциональный объект
};
int main() {
Counter c;
Counter before = c++;
++c;
std::cout << before.get() << ' ' << c.get() << '\n'; // 0 2
Row r(3);
r[1] = 42;
const Row& cr = r;
std::cout << cr[1] << '\n'; // 42
Scale twice{2.0};
std::cout << twice(21) << '\n'; // 42
}
Вывод: 0 2, затем 42 и 42. Постфиксная форма возвращает копию по значению: ссылку на локальный old вернуть нельзя, он уничтожится при выходе из функции. Поэтому в циклах по своим итераторам принято писать ++it.
У operator[] две версии: неконстантная отдает ссылку для записи, константная работает у const-объектов. Как и у std::vector, [] здесь индекс не проверяет — выход за границы дает неопределенное поведение; для проверки добавляют метод вроде at() с исключением. В C++23 operator[] может принимать несколько индексов: m[i, j].
Сравнения в C++20: operator<=>
До C++20 для полного набора сравнений писали шесть операторов. Теперь достаточно одного трехстороннего сравнения operator<=> («космический корабль»): компилятор переписывает a < b в (a <=> b) < 0, а >, <=, >= — аналогично.
#include <algorithm>
#include <compare>
#include <iostream>
#include <vector>
struct Version {
int major = 0, minor = 0, patch = 0;
// сравнение по полям в порядке объявления; == тоже генерируется
auto operator<=>(const Version&) const = default;
};
int main() {
Version v1{1, 4, 2}, v2{1, 10, 0};
std::cout << std::boolalpha
<< (v1 < v2) << ' ' // true: 4 < 10
<< (v1 == v2) << ' ' // false
<< (v1 >= v2) << '\n'; // false
std::vector<Version> vs{{2, 0, 0}, {1, 10, 0}, {1, 4, 2}};
std::sort(vs.begin(), vs.end());
for (const auto& v : vs)
std::cout << v.major << '.' << v.minor << '.' << v.patch << ' ';
std::cout << '\n';
}
Вывод: true false false, затем 1.4.2 1.10.0 2.0.0. Версии сравниваются как числа, а не как строки, где "1.10" оказалась бы меньше "1.4".
Тип результата <=> говорит о смысле сравнения:
| Тип | Когда | Пример |
|---|---|---|
std::strong_ordering |
равные значения неотличимы | целые числа, Version |
std::weak_ordering |
эквивалентные значения могут различаться | строки без учета регистра: "Cpp" и "CPP" |
std::partial_ordering |
часть значений несравнима | double с NaN |
Ловушка: == default генерируется только вместе с <=> = default. Если вы пишете <=> вручную, == сам не появится. Неверно — тег с ручным сравнением без учета регистра и без ==:
#include <algorithm>
#include <cctype>
#include <compare>
#include <iostream>
#include <string>
struct Tag {
std::string name;
// "Cpp" и "CPP" эквивалентны, но не одинаковы -> weak_ordering
std::weak_ordering operator<=>(const Tag& rhs) const {
std::size_t n = std::min(name.size(), rhs.name.size());
for (std::size_t i = 0; i < n; ++i) {
int a = std::tolower(static_cast<unsigned char>(name[i]));
int b = std::tolower(static_cast<unsigned char>(rhs.name[i]));
if (a != b) return a <=> b;
}
return name.size() <=> rhs.name.size();
}
};
int main() {
Tag a{"Cpp"}, b{"CPP"}, c{"rust"};
std::cout << std::boolalpha << (a < c) << ' ' << (c > b) << '\n';
std::cout << (a == b) << ' ' << (a != c) << '\n';
}
Результат — < и > компилируются, а == и != нет:
tag.cpp:25:21: error: invalid operands to binary expression ('Tag' and 'Tag')
tag.cpp:25:40: error: invalid operands to binary expression ('Tag' and 'Tag')
Исправление — добавить в класс ==, согласованный с <=>:
bool operator==(const Tag& rhs) const { return (*this <=> rhs) == 0; }
После этого программа печатает true true и true true. Упрощение в примере: tolower корректен только для ASCII, для кириллицы в UTF-8 нужна библиотека работы с Unicode.
Когда перегрузка вредит
Перегружайте оператор, только если его смысл очевиден без документации: сложение векторов, матриц, денежных сумм в одной валюте. Сложение двух объектов «игрок» — плохой кандидат: читатель кода не угадает результат, понятнее метод mergeTeams().
Исключение, ставшее соглашением, — << и >> у потоков: к битовому сдвигу они отношения не имеют, но так устроена стандартная библиотека.
Если не получилось — типовые симптомы:
| Симптом | Причина | Что сделать |
|---|---|---|
invalid operands to binary expression / no match for operator |
нет подходящей перегрузки для этих типов или порядка операндов | добавить свободную функцию; проверить const у параметров |
must be a non-static member function |
=, [], () или -> объявлены вне класса |
перенести внутрь класса |
must have at least one parameter of class or enumeration type |
попытка перегрузить оператор для встроенных типов | ввести свой тип-обертку |
== не компилируется при готовом <=> |
<=> написан вручную |
добавить operator== |
Выводы
- Перегрузка операторов — это функция с именем
operator@; приоритет, число операндов и ассоциативность остаются как у встроенного оператора. =,(),[],->и преобразования объявляются только методом;<<для потоков — только свободной функцией.- Симметричные операторы (
+,*,==) удобнее делать свободными функциями, часто hidden friend, а+выражать через+=. - В C++20
operator<=>с= defaultдает все шесть сравнений; ручной<=>требует отдельного==. - Перегружать стоит только там, где смысл оператора очевиден.
Где применяется / связь с практикой
Освойте тему на практике
Перегрузка операторов встречается в любом «математическом» коде — векторы, матрицы, деньги, — а также в итераторах, умных указателях и функциональных объектах для алгоритмов STL. Понимать ее нужно, даже если свои операторы вы пишете редко: так устроены std::string, std::chrono и потоки ввода-вывода. Если хотите системно пройти классы, шаблоны и стандартную библиотеку с практикой и ревью кода, посмотрите курс «C++-разработчик. Базовый уровень». Разобрать отдельные темы языка с преподавателями можно на открытых уроках Otus.
FAQ
Можно ли перегрузить оператор для enum?
Да: перечисление, включая enum class, считается пользовательским типом. Так часто делают operator| для флагов.
Что такое пользовательские литералы и при чем тут перегрузка?
С C++11 можно объявить operator""_km, и запись 5.0_km вызовет эту функцию. Это отдельный вид операторной функции; имена без подчеркивания зарезервированы за стандартной библиотекой.
Нужно ли писать operator= самому?
Обычно нет: компилятор генерирует копирующее присваивание, а перемещающее — только если в классе нет объявленных пользователем деструктора, копирующих или перемещающих операций (иначе вместо перемещения молча выполнится копирование). Свое нужно, когда класс напрямую владеет ресурсом (сырой указатель, дескриптор), — это правило трех/пяти; чаще проще хранить ресурс в std::vector или std::unique_ptr.



