перегрузка оператора
Настраивает операторы C++ для операндов определяемых пользователем типов.
[править] Синтаксис
Перегруженные операторы это функции со специальными именами функций:
operator оп
|
(1) | ||||||||
operator тип
|
(2) | ||||||||
operator new operator new []
|
(3) | ||||||||
operator delete operator delete []
|
(4) | ||||||||
operator "" суффикс-идентификатор
|
(5) | (начиная с C++11) | |||||||
operator co_await
|
(6) | (начиная с C++20) | |||||||
оп | — | любой из следующих операторов:+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=> (начиная с 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) |
Примечание: для перегрузки co_await
, (начиная с C++20)пользовательских функций преобразования, пользовательских литералов, распределения и освобождения смотрите соответствующие статьи.
Перегруженные операторы (но не встроенные операторы) можно вызывать, используя нотацию функции:
std::string str = "Привет, "; str.operator+=("мир"); // то же, что и str += "мир"; operator<<(operator<<(std::cout, str) , '\n'); // то же, что и std::cout << str << '\n'; // (начиная C++17) за исключением для // последовательности
[править] Ограничения
- Операторы
::
(разрешение области видимости),.
(доступ к элементу),.*
(доступ к элементу через указатель на элемент) и?:
(тройное условное выражение) нельзя перегружать. - Нельзя создавать новые операторы, такие как
**
,<>
или&|
. - Невозможно изменить приоритет, группирование или количество операндов операторов.
- Перегрузка оператора
->
должна либо возвращать сырой указатель, либо возвращать объект (по ссылке или по значению), для которого оператор->
, в свою очередь, перегружен. - Перегруженные операторы
&&
и||
теряют вычисление по короткой схеме.
|
(до C++17) |
[править] Канонические реализации
Помимо указанных выше ограничений, язык не накладывает никаких других ограничений на то, что делают перегруженные операторы, или на тип возвращаемого значения (он не участвует в разрешении перегрузки), но в целом ожидается, что перегруженные операторы будут вести себя максимально похоже на встроенные операторы: ожидается, что operator+ будет складывать, а не перемножать свои аргументы, ожидается, что operator= будет присваивать и т.д. Ожидается, что связанные операторы будут вести себя одинаково (operator+ и operator+= выполняют одну и ту же операцию, подобную сложению). Типы возвращаемых значений ограничены выражениями, в которых ожидается использование оператора: например, операторы присваивания возвращают значение по ссылке, чтобы можно было написать a = b = c = d, так как встроенные операторы это позволяют.
Обычно перегруженные операторы имеют следующие типичные канонические формы:[1]
[править] Оператор присваивания
Оператор присваивания (operator=) имеет специальные свойства: подробности смотрите в разделах присваивание копированием и присваивание перемещением.
Ожидается, что канонический оператор присваивания копированием будет безопасен при присваиванием самому себе, и будет возвращать левостороннее значение по ссылке:
// присваивание копированием 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; }
Ожидается, что каноническое присваивание перемещением оставит перемещённый объект в действительном состоянии (то есть состоянии с неповреждёнными инвариантами класса), и либо ничего не сделает или, по крайней мере, оставит объект в допустимом состоянии при самоприсваивании и вернёт левостороннее значение по ссылке на неконстанту и будет noexcept: // присваивание перемещением T& operator=(T&& other) noexcept { // Защита от присваивания самому себе if (this == &other) return *this; // delete[]/size=0 тоже было бы правильно delete[] mArray; // освободить ресурс в *this mArray = std::exchange(other.mArray, nullptr); // оставить other в допустимом // состоянии size = std::exchange(other.size, 0); return *this; } |
(начиная с C++11) |
В тех ситуациях, когда присваивание копированием не может извлечь выгоду из повторного использования ресурсов (оно не управляет массивом, выделенным в куче, и не имеет элемента (возможно, транзитивного), который делает это, например, элемент std::vector или std::string), есть популярное удобное сокращение: оператор присваивания копированием и обмен, который принимает свой параметр по значению (таким образом, работая как присваивание копированием или перемещением в зависимости от категории значения аргумента), обменивается с параметром и позволяет деструктору очистить его.
// присваивание копированием (идиома копирование-и-обмен) 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; }
Эти операторы иногда реализуются как дружественные функции.
[править] Оператор вызова функции
Когда определяемый пользователем класс перегружает оператор вызова функции 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); }
Многие стандартные алгоритмы, от std::sort до std::accumulate, принимают FunctionObject для настройки поведения. Особо примечательных канонических форм 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 << "Сумма равна " << s.sum << '\n'; }
Вывод:
Сумма равна 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+
является функцией-элементом комплексного типа, то будет компилироваться только комплексное+целочисленное, но не целочисленное+комплексное). Поскольку для каждого бинарного арифметического оператора существует соответствующий составной оператор присваивания, канонические формы бинарных операторов реализуются в терминах их составных присваиваний:
class X { public: X& operator+=(const X& rhs) // составное присваивание (не обязательно должно быть { // элементом, но часто это требуется для изменения // закрытых элементов) /* добавление rhs к *this происходит здесь */ return *this; // вернуть результат по ссылке } // друзья, определённые внутри тела класса, являются встроенными и скрыты от поиска // без ADL friend X operator+(X lhs, // передача lhs по значению помогает оптимизировать const X& rhs) // цепочку a+b+c в противном случае оба параметра // могут быть константными ссылками { 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; }
Оператор неравенства автоматически генерируется компилятором, если определён operator==. Точно так же четыре оператора отношения автоматически генерируются компилятором, если определён оператор трёхстороннего сравнения operator<=>. operator== и operator!=, в свою очередь, генерируются компилятором, если operator<=> определён по умолчанию: struct Record { std::string name; unsigned int floor; double weight; auto operator<=>(const Record&) const = default; }; // записи теперь можно сравнивать с помощью ==, !=, <, <=, > и >= Дополнительные сведения смотрите в разделе сравнения по умолчанию. |
(начиная с C++20) |
[править] Оператор индексации массива
Определяемые пользователем классы, предоставляющие доступ наподобие массива, который позволяет как чтение, так и запись, обычно определяют две перегрузки для 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]; } }; |
(начиная с C++23) |
Если известно, что тип значения является скалярным, константный вариант должен делать возврат по значению.
Если прямой доступ к элементам контейнера нежелателен или невозможен, или если имеется различие между использованием lvalue c[i] = v; и rvalue v = c[i];, operator[] может вернуть прокси. Смотрите, например, std::bitset::operator[].
Поскольку оператор индексации может принимать только один индекс до C++23, чтобы обеспечить семантику доступа к многомерному массиву, например, чтобы реализ��вать доступ к трёхмерному массиву a[i][j][k] = x;, operator[] должен вернуть ссылку на двухмерную плоскость, которая должна иметь свой собственный operator[], который возвращает ссылку на одномерную строку, которая должна иметь operator[], который возвращает ссылку на элемент. Чтобы избежать такой сложности, некоторые библиотеки предпочитают вместо этого перегружать operator(), чтобы выражения трёхмерного доступа имели синтаксис, подобный Фортрану, a(i, j, k) = x;.
Начиная с C++23, operator[] может принимать любое количество индексов. Например, operator[] класса трёхмерного массива, объявленный как T& operator[](std::size_t x, std::size_t y, std::size_t z); может получить прямой доступ к элементам. Запустить этот код #include <array> #include <cassert> #include <iostream> #include <numeric> #include <tuple> template <typename T, std::size_t Z, std::size_t Y, std::size_t X> class Array3d { std::array<T, X * Y * Z> m{}; public: array3d() = default; array3d(array3d const&) = default; 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]; } constexpr auto& underlying_array() { return a; } constexpr std::tuple<std::size_t, std::size_t, std::size_t> xyz() const { return {X, Y, Z}; } }; 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) |
Поскольку встроенный оператор ! выполняет контекстное преобразование в |
(начиная с C++11) |
[править] Редко перегружаемые операторы
Следующие операторы редко перегружаются:
- Оператор взятия адреса, operator&. Если унарный оператор & применяется к lvalue неполного типа, а полный тип объявляет перегруженный operator&, поведение не определено (до C++11) не указано, имеет ли оператор встроенное значение или вызывается операторная функция (начиная с C++11). Поскольку этот оператор может быть перегружен, универсальные библиотеки используют 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 { constexpr int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } // или C++17 std::gcd 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++:
Номер | Применён | Поведение в стандарте | Корректное поведение |
---|---|---|---|
CWG 1458 | C++11 | получение адреса неполного типа, который перегружает взятие адреса, было неопределённым поведением |
поведение только не указано |
[править] Смотрите также
Общие операторы | ||||||
---|---|---|---|---|---|---|
присваивание | инкремент декремент |
арифметические | логические | сравнения | доступ к элементу | другие |
a = b |
++a |
+a |
!a |
a == b |
a[...] |
вызов функции |
a(...) | ||||||
запятая | ||||||
a, b | ||||||
условный | ||||||
a ? b : c | ||||||
Специальные операторы | ||||||
static_cast приводит один тип к другому совместимому типу |
[править] Внешние ссылки
|