operator overloading
Настраивает операторы C++ для операндов пользовательских типов.
Синтаксис
Функции-операторы — это функции со специальными именами:
operator
op
|
(1) | ||||||||
operator
new
operator
new []
|
(2) | ||||||||
operator
delete
operator
delete []
|
(3) | ||||||||
operator
co_await
|
(4) | (начиная с C++20) | |||||||
| op | - | любой из следующих операторов: + - * / % ^ & | ~ ! = < > + = - = * = / = % = ^ = & = | = << >> >>= <<= == ! = <= >= <=> (начиная с C++20) && || ++ -- , - > * - > ( ) [ ] |
Поведение непунктуационных операторов описано на их собственных страницах. Если не указано иное, оставшееся описание на этой странице не применяется к этим функциям.
Объяснение
Когда оператор появляется в выражении , и хотя бы один из его операндов имеет классовый тип или перечисляемый тип , тогда разрешение перегрузки используется для определения пользовательской функции, которая должна быть вызвана среди всех функций, чьи сигнатуры соответствуют следующим:
| Выражение | Как функция-член | Как внешняя функция | Пример |
|---|---|---|---|
| @a | (a).operator@ ( ) | operator@ (a) | ! std:: cin вызывает std:: cin . operator ! ( ) |
| a@b | (a).operator@ (b) | operator@ (a, b) | std:: cout << 42 вызывает std:: cout . operator << ( 42 ) |
| a=b | (a).operator= (b) | не может быть внешней | Для std:: string s ; , s = "abc" ; вызывает s. operator = ( "abc" ) |
| a(b...) | (a).operator()(b...) | не может быть внешней | Для std:: random_device r ; , auto n = r ( ) ; вызывает r. operator ( ) ( ) |
| a[b...] | (a).operator[](b...) | не может быть внешней | Для std:: map < int , int > m ; , m [ 1 ] = 2 ; вызывает m. operator [ ] ( 1 ) |
| a-> | (a).operator->( ) | не может быть внешней | Для std:: unique_ptr < S > p ; , p - > bar ( ) вызывает p. operator - > ( ) |
| a@ | (a).operator@ (0) | operator@ (a, 0) | Для std:: vector < int > :: iterator i ; , i ++ вызывает i. operator ++ ( 0 ) |
|
В этой таблице
|
|||
|
Кроме того, для операторов сравнения == , ! = , < , > , <= , >= , <=> , разрешение перегрузки также рассматривает переписанные кандидаты operator == или operator <=> . |
(начиная с C++20) |
Перегруженные операторы (но не встроенные операторы) могут быть вызваны с использованием функциональной нотации:
std::string str = "Hello, "; str.operator+=("world"); // то же самое, что str += "world"; operator<<(operator<<(std::cout, str), '\n'); // то же самое, что std::cout << str << '\n'; // (начиная с C++17) за исключением порядка вычислений
Статические перегруженные операторыПерегруженные операторы, которые являются функциями-членами, могут быть объявлены static . Однако это разрешено только для operator ( ) и operator [ ] . Такие операторы могут быть вызваны с использованием функциональной нотации. Однако, когда эти операторы появляются в выражениях, они всё равно требуют объект классового типа. struct SwapThem { template<typename T> static void operator()(T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } template<typename T> static void operator[](T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } }; inline constexpr SwapThem swap_them{}; void foo() { int a = 1, b = 2; swap_them(a, b); // OK swap_them[a, b]; // OK SwapThem{}(a, b); // OK SwapThem{}[a, b]; // OK SwapThem::operator()(a, b); // OK SwapThem::operator[](a, b); // OK SwapThem(a, b); // error, invalid construction SwapThem[a, b]; // error } |
(начиная с C++23) |
Ограничения
- Функция-оператор должна иметь по крайней мере один параметр функции или неявный параметр объекта, тип которого является классом, ссылкой на класс, перечислением или ссылкой на перечисление.
-
Операторы
::(разрешение области видимости),.(доступ к члену),.*(доступ к члену через указатель на член) и?:(тернарный условный) не могут быть перегружены. -
Новые операторы, такие как
**,<>, или&|, не могут быть созданы. - Невозможно изменить приоритет, группировку или количество операндов операторов.
-
Перегрузка оператора
->должна либо возвращать сырой указатель, либо возвращать объект (по ссылке или по значению), для которого оператор->в свою очередь перегружен. -
Перегрузки операторов
&&и||теряют короткое замыкание вычислений.
|
(до C++17) |
Канонические реализации
Помимо указанных выше ограничений, язык не накладывает других ограничений на то, что делают перегруженные операторы или на возвращаемый тип (он не участвует в разрешении перегрузки), но в целом ожидается, что перегруженные операторы будут вести себя максимально похоже на встроенные операторы: operator + ожидается, что будет складывать, а не умножать свои аргументы, operator = ожидается, что будет присваивать и т.д. Связанные операторы должны вести себя схожим образом ( operator + и operator + = выполняют одну и ту же операцию сложения). Возвращаемые типы ограничены выражениями, в которых ожидается использование оператора: например, операторы присваивания возвращают ссылку, чтобы была возможность писать a = b = c = d , поскольку встроенные операторы это позволяют.
Обычно перегружаемые операторы имеют следующие типичные, канонические формы: [1]
Оператор присваивания
Оператор присваивания operator = обладает особыми свойствами: подробности смотрите в разделах copy assignment и move assignment .
Канонический оператор присваивания копированием должен быть безопасным при самоприсваивании и возвращать левосторонний операнд по ссылке:
// оператор присваивания копированием T& operator=(const T& other) { // Защита от самоприсваивания if (this == &other) return *this; // предполагаем, что *this управляет переиспользуемым ресурсом, таким как выделенный в куче буфер mArray if (size != other.size) // ресурс в *this не может быть переиспользован { temp = new int[other.size]; // выделить ресурс, если выбрасывает исключение - ничего не делать delete[] mArray; // освободить ресурс в *this mArray = temp; size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); return *this; }
|
Каноническое перемещающее присваивание должно оставлять перемещаемый объект в валидном состоянии (то есть в состоянии с сохранёнными инвариантами класса), и либо ничего не делать либо, по крайней мере, оставлять объект в валидном состоянии при самоприсваивании, возвращать левосторонний операнд по ссылке на не-const и быть noexcept: // move assignment T& operator=(T&& other) noexcept { // Guard self assignment if (this == &other) return *this; // delete[]/size=0 would also be ok delete[] mArray; // release resource in *this mArray = std::exchange(other.mArray, nullptr); // leave other in valid state size = std::exchange(other.size, 0); return *this; } |
(начиная с C++11) |
В тех ситуациях, когда копирующее присваивание не может воспользоваться преимуществами повторного использования ресурсов (оно не управляет массивом в динамической памяти и не имеет (возможно, транзитивного) члена, который это делает, такого как член std::vector или std::string ), существует популярное удобное сокращение: оператор присваивания через копирование и обмен (copy-and-swap assignment operator), который принимает параметр по значению (таким образом работая как копирующее и перемещающее присваивание в зависимости от категории значения аргумента), обменивается с параметром и позволяет деструктору очистить его.
// оператор присваивания копированием (идиома copy-and-swap) T& T::operator=(T other) noexcept // вызов конструктора копирования или перемещения для создания other { std::swap(size, other.size); // обмен ресурсами между *this и other std::swap(mArray, other.mArray); return *this; } // вызов деструктора other для освобождения ресурсов, ранее управляемых *this
Эта форма автоматически обеспечивает строгую гарантию безопасности исключений , но запрещает повторное использование ресурсов.
Извлечение и вставка потоков
Перегрузки операторов
operator>>
и
operator<<
, которые принимают
std::
istream
&
или
std::
ostream
&
в качестве левого аргумента, известны как операторы извлечения и вставки. Поскольку они принимают пользовательский тип в качестве правого аргумента (
b
в
a @ b
), они должны быть реализованы как нечлены класса.
std::ostream& operator<<(std::ostream& os, const T& obj) { // записать obj в поток return os; } std::istream& operator>>(std::istream& is, T& obj) { // прочитать obj из потока if (/* не удалось создать T */) is.setstate(std::ios::failbit); return is; }
Эти операторы иногда реализуются как friend functions .
Оператор вызова функции
Когда пользовательский класс перегружает оператор вызова функции operator ( ) , он становится типом FunctionObject .
Объект такого типа может быть использован в выражении вызова функции:
// Объект этого типа представляет линейную функцию одной переменной a * x + b. struct Linear { double a, b; double operator()(double x) const { return a * x + b; } }; int main() { Linear f{2, 1}; // Представляет функцию 2x + 1. Linear g{-1, 0}; // Представляет функцию -x. // f и g - объекты, которые можно использовать как функции. double f_0 = f(0); double f_1 = f(1); double g_0 = g(0); }
Многие стандартные библиотечные алгоритмы принимают FunctionObject s для настройки поведения. Не существует особо примечательных канонических форм operator ( ) , но для демонстрации использования:
#include <algorithm> #include <iostream> #include <vector> struct Sum { int sum = 0; void operator()(int n) { sum += n; } }; int main() { std::vector<int> v = {1, 2, 3, 4, 5}; Sum s = std::for_each(v.begin(), v.end(), Sum()); std::cout << "The sum is " << s.sum << '\n'; }
Вывод:
The sum is 15
Инкремент и декремент
Когда постфиксный оператор инкремента или декремента появляется в выражении, вызывается соответствующая пользовательская функция ( operator ++ или operator -- ) с целочисленным аргументом 0 . Обычно она объявляется как T operator ++ ( int ) или T operator -- ( int ) , где аргумент игнорируется. Постфиксные операторы инкремента и декремента обычно реализуются через префиксные версии:
struct X { // префиксный инкремент X& operator++() { // фактическое увеличение происходит здесь return *this; // возврат нового значения по ссылке } // постфиксный инкремент X operator++(int) { X old = *this; // копирование старого значения operator++(); // префиксный инкремент return old; // возврат старого значения } // префиксный декремент X& operator--() { // фактическое уменьшение происходит здесь return *this; // возврат нового значения по ссылке } // постфиксный декремент X operator--(int) { X old = *this; // копирование старого значения operator--(); // префиксный декремент return old; // возврат старого значения } };
Хотя канонические реализации операторов префиксного инкремента и декремента возвращают ссылку, как и при любой перегрузке операторов, возвращаемый тип определяется пользователем; например, перегрузки этих операторов для std::atomic возвращают по значению.
Бинарные арифметические операторы
Бинарные операторы обычно реализуются как не-члены для сохранения симметрии (например, при сложении комплексного числа и целого числа, если operator + является функцией-членом комплексного типа, тогда будет компилироваться только complex + integer , но не integer + complex ). Поскольку для каждого бинарного арифметического оператора существует соответствующий составной оператор присваивания, канонические формы бинарных операторов реализуются через их составные присваивания:
class X { public: X& operator+=(const X& rhs) // составное присваивание (не обязательно быть членом, { // но часто им является для изменения приватных членов) /* здесь происходит добавление rhs к *this */ return *this; // возвращаем результат по ссылке } // дружественные функции, определённые внутри класса, являются встроенными и скрыты от поиска без ADL friend X operator+(X lhs, // передача lhs по значению помогает оптимизировать цепочки a+b+c const X& rhs) // иначе оба параметра могут быть константными ссылками { lhs += rhs; // повторное использование составного присваивания return lhs; // возвращаем результат по значению (использует конструктор перемещения) } };
Операторы сравнения
Алгоритмы стандартной библиотеки, такие как std::sort и контейнеры, такие как std::set ожидают, что для пользовательских типов по умолчанию будет определен operator < и что он будет реализовывать строгое слабое упорядочение (таким образом удовлетворяя требованиям Compare ). Идиоматический способ реализации строгого слабого упорядочения для структуры — использование лексикографического сравнения, предоставляемого std::tie :
struct Record { std::string name; unsigned int floor; double weight; friend bool operator<(const Record& l, const Record& r) { return std::tie(l.name, l.floor, l.weight) < std::tie(r.name, r.floor, r.weight); // сохранить тот же порядок } };
Обычно, как только предоставлен operator < , остальные операторы отношений реализуются на основе operator < .
inline bool operator< (const X& lhs, const X& rhs) { /* выполнить фактическое сравнение */ } inline bool operator> (const X& lhs, const X& rhs) { return rhs < lhs; } inline bool operator<=(const X& lhs, const X& rhs) { return !(lhs > rhs); } inline bool operator>=(const X& lhs, const X& rhs) { return !(lhs < rhs); }
Аналогично, оператор неравенства обычно реализуется через operator == :
inline bool operator==(const X& lhs, const X& rhs) { /* выполнить фактическое сравнение */ } inline bool operator!=(const X& lhs, const X& rhs) { return !(lhs == rhs); }
Когда предоставляется трёхстороннее сравнение (такое как std::memcmp или std::string::compare ), все шесть двусторонних операторов сравнения могут быть выражены через него:
inline bool operator==(const X& lhs, const X& rhs) { return cmp(lhs,rhs) == 0; } inline bool operator!=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) != 0; } inline bool operator< (const X& lhs, const X& rhs) { return cmp(lhs,rhs) < 0; } inline bool operator> (const X& lhs, const X& rhs) { return cmp(lhs,rhs) > 0; } inline bool operator<=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) <= 0; } inline bool operator>=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) >= 0; }
` и `` оставлен без изменений, как и требовалось. HTML-теги и атрибуты также сохранены в оригинальном виде.
Оператор индексации массива
Пользовательские классы, которые предоставляют доступ к элементам по индексу с возможностью как чтения, так и записи, обычно определяют две перегрузки для operator [ ] : константную и неконстантную версии:
struct T { value_t& operator[](std::size_t idx) { return mVector[idx]; } const value_t& operator[](std::size_t idx) const { return mVector[idx]; } };
|
Альтернативно, они могут быть выражены как единая шаблонная функция-член с использованием явного параметра объекта : struct T { decltype(auto) operator[](this auto& self, std::size_t idx) { return self.mVector[idx]; } }; |
(since C++23) |
Если известно, что тип значения является скалярным типом, const вариант должен возвращать по значению.
В случаях, когда прямой доступ к элементам контейнера нежелателен или невозможен, либо требуется различать использование lvalue c [ i ] = v ; и rvalue v = c [ i ] ; , operator [ ] может возвращать прокси-объект. Смотрите, например, std::bitset::operator[] .
|
operator [ ] может принимать только один индекс. Для обеспечения семантики доступа к многомерным массивам, например, для реализации доступа к 3D-массиву a [ i ] [ j ] [ k ] = x ; , operator [ ] должен возвращать ссылку на 2D-плоскость, которая должна иметь собственный operator [ ] , возвращающий ссылку на 1D-строку, которая должна иметь operator [ ] , возвращающий ссылку на элемент. Чтобы избежать этой сложности, некоторые библиотеки предпочитают перегружать operator ( ) , чтобы выражения доступа к 3D-массивам имели синтаксис, подобный Fortran: a ( i, j, k ) = x ; . |
(до C++23) |
|
operator [ ] может принимать любое количество индексов. Например, operator [ ] для класса 3D-массива, объявленный как T & operator [ ] ( std:: size_t x, std:: size_t y, std:: size_t z ) ; , может напрямую обращаться к элементам.
Запустить этот код
#include <array> #include <cassert> #include <iostream> template<typename T, std::size_t Z, std::size_t Y, std::size_t X> struct Array3d { std::array<T, X * Y * Z> m{}; constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23 { assert(x < X and y < Y and z < Z); return m[z * Y * X + y * X + x]; } }; int main() { Array3d<int, 4, 3, 2> v; v[3, 2, 1] = 42; std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << '\n'; } Вывод: v[3, 2, 1] = 42 |
(начиная с C++23) |
Побитовые арифметические операторы
Определяемые пользователем классы и перечисления, которые реализуют требования BitmaskType , должны перегружать побитовые арифметические операторы operator & , operator | , operator ^ , operator~ , operator & = , operator | = и operator ^ = , а также могут дополнительно перегружать операторы сдвига operator << operator >> , operator >>= и operator <<= . Канонические реализации обычно следуют шаблону для бинарных арифметических операторов, описанному выше.
Оператор логического отрицания
|
Оператор operator ! обычно перегружается пользовательскими классами, которые предназначены для использования в булевых контекстах. Такие классы также предоставляют пользовательскую функцию преобразования к булевому типу (см. std::basic_ios для примера из стандартной библиотеки), и ожидаемое поведение operator ! состоит в возврате значения, противоположного operator bool . |
(до C++11) |
|
Поскольку встроенный оператор ! выполняет контекстное преобразование к bool , пользовательские классы, которые предназначены для использования в булевых контекстах, могут предоставлять только operator bool и не нуждаются в перегрузке operator ! . |
(начиная с C++11) |
Редко перегружаемые операторы
Следующие операторы перегружаются редко:
-
Оператор взятия адреса,
operator
&
. Если унарный & применяется к lvalue неполного типа, и полный тип объявляет перегруженный
operator
&
, не определено, будет ли использоваться встроенное значение оператора или будет вызвана функция оператора. Поскольку этот оператор может быть перегружен, универсальные библиотеки используют
std::addressof
для получения адресов объектов пользовательских типов. Наиболее известным примером канонической перегрузки
operator
&
является класс Microsoft
CComPtrBase. Пример использования этого оператора в EDSL можно найти в boost.spirit . - Логические операторы, operator && и operator || . В отличие от встроенных версий, перегруженные операторы не могут реализовать сокращенное вычисление. Также в отличие от встроенных версий, они не упорядочивают левый операнд перед правым. (до C++17) В стандартной библиотеке эти операторы перегружены только для std::valarray .
- Оператор запятая, operator, . В отличие от встроенной версии, перегруженные операторы не упорядочивают левый операнд перед правым. (до C++17) Поскольку этот оператор может быть перегружен, универсальные библиотеки используют выражения вида a, void ( ) , b вместо a, b для упорядочивания выполнения выражений пользовательских типов. Библиотека boost использует operator, в boost.assign , boost.spirit и других библиотеках. Библиотека доступа к базам данных SOCI также перегружает operator, .
- Оператор доступа к члену через указатель на член operator - > * . Нет специфических недостатков в перегрузке этого оператора, но на практике он используется редко. Предполагалось, что он может быть частью интерфейса умных указателей , и фактически используется в этом качестве акторами в boost.phoenix . Более распространен в EDSL, таких как cpp.react .
Примечания
| Макрос тестирования возможностей | Значение | Стандарт | Возможность |
|---|---|---|---|
__cpp_static_call_operator
|
202207L
|
(C++23) | static operator ( ) |
__cpp_multidimensional_subscript
|
202211L
|
(C++23) | static operator [ ] |
Ключевые слова
Пример
#include <iostream> class Fraction { // или std::gcd из C++17 constexpr int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: constexpr Fraction(int n, int d = 1) : n(n / gcd(n, d)), d(d / gcd(n, d)) {} constexpr int num() const { return n; } constexpr int den() const { return d; } constexpr Fraction& operator*=(const Fraction& rhs) { int new_n = n * rhs.n / gcd(n * rhs.n, d * rhs.d); d = d * rhs.d / gcd(n * rhs.n, d * rhs.d); n = new_n; return *this; } }; std::ostream& operator<<(std::ostream& out, const Fraction& f) { return out << f.num() << '/' << f.den(); } constexpr bool operator==(const Fraction& lhs, const Fraction& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } constexpr bool operator!=(const Fraction& lhs, const Fraction& rhs) { return !(lhs == rhs); } constexpr Fraction operator*(Fraction lhs, const Fraction& rhs) { return lhs *= rhs; } int main() { constexpr Fraction f1{3, 8}, f2{1, 2}, f3{10, 2}; std::cout << f1 << " * " << f2 << " = " << f1 * f2 << '\n' << f2 << " * " << f3 << " = " << f2 * f3 << '\n' << 2 << " * " << f1 << " = " << 2 * f1 << '\n'; static_assert(f3 == f2 * 10); }
Вывод:
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
Отчеты о дефектах
Следующие отчеты об изменениях поведения, влияющие на дефекты, были применены задним числом к ранее опубликованным стандартам C++.
| DR | Applied to | Behavior as published | Correct behavior |
|---|---|---|---|
| CWG 1481 | C++98 |
the non-member prefix increment operator could only have a parameter
of class type, enumeration type, or a reference type to such types |
no type requirement |
| CWG 2931 | C++23 |
explicit object member operator functions could only have no parameter
of class type, enumeration type, or a reference type to such types |
prohibited |
Смотрите также
| Основные операторы | ||||||
|---|---|---|---|---|---|---|
| присваивание |
инкремент
декремент |
арифметические | логические | сравнения |
доступа к членам
класса |
прочие |
|
a
=
b
|
++
a
|
+
a
|
!
a
|
a
==
b
|
a
[
...
]
|
вызов функции
a ( ... ) |
|
запятая
a, b |
||||||
|
условный оператор
a ? b : c |
||||||
| Специальные операторы | ||||||
|
static_cast
преобразует один тип в другой связанный тип
|
||||||
Внешние ссылки
|