Перегрузка операторов
Настраивает операторы C++ для операндов пользовательских типов.
Синтаксис
Функции-операторы — это функции со специальными именами:
operator op | (1) | |
operator newoperator new [] | (2) | |
operator deleteoperator 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) |
| В этой таблице |
|||
| Кроме того, для операторов сравнения | (начиная с C++20) |
Перегруженные операторы (но не встроенные операторы) можно вызывать с помощью функциональной записи:
std::string str = "Hello, ";
str.operator+=("world"); // same as str += "world";
operator<<(operator<<(std::cout, str), '\n'); // same as std::cout << str << '\n';
// (since C++17) except for sequencing
Статические перегруженные операторыПерегруженные операторы, являющиеся функциями-членами, можно объявлять как 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
T& operator=(const T& other)
{
// Guard self assignment
if (this == &other)
return *this;
// assume *this manages a reusable resource, such as a heap-allocated buffer mArray
if (size != other.size) // resource in *this cannot be reused
{
temp = new int[other.size]; // allocate resource, if throws, do nothing
delete[] mArray; // release resource in *this
mArray = temp;
size = other.size;
}
std::copy(other.mArray, other.mArray + other.size, mArray);
return *this;
}
| Ожидается, что канонический оператор перемещающего присваивания будет оставлять перемещённый объект в допустимом состоянии (то есть в состоянии, в котором сохраняются инварианты класса), а при самоприсваивании либо ничего не делать, либо как минимум оставлять объект в допустимом состоянии; кроме того, он должен возвращать левый операнд по неконстантной ссылке и быть 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 assignment (copy-and-swap idiom)
T& T::operator=(T other) noexcept // call copy or move constructor to construct other
{
std::swap(size, other.size); // exchange resources between *this and other
std::swap(mArray, other.mArray);
return *this;
} // destructor of other is called to release the resources formerly managed by *this
Такая форма автоматически обеспечивает строгую гарантию безопасности исключений, но не позволяет повторно использовать ресурсы.
Извлечение и вставка в поток
Перегрузки operator>> и operator<<, принимающие std::istream& или std::ostream& в качестве левого аргумента, называются операторами вставки и извлечения. Поскольку пользовательский тип передаётся в качестве правого аргумента (b в a @ b), эти операторы должны быть реализованы как функции, не являющиеся членами.
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if (/* T could not be constructed */)
is.setstate(std::ios::failbit);
return is;
}
Иногда эти операторы реализуются как функции-друзья.
Оператор вызова функции
Когда пользовательский класс перегружает оператор вызова функции operator(), он становится типом FunctionObject.
Объект такого типа можно использовать в выражении вызова функции:
// An object of this type represents a linear function of one variable a * x + b.
struct Linear
{
double a, b;
double operator()(double x) const
{
return a * x + b;
}
};
int main()
{
Linear f{2, 1}; // Represents function 2x + 1.
Linear g{-1, 0}; // Represents function -x.
// f and g are objects that can be used like a function.
double f_0 = f(0);
double f_1 = f(1);
double g_0 = g(0);
}
Многие стандартные библиотечные алгоритмы принимают объекты 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 << "The sum is " << s.sum << '\n';
}
Вывод:
The sum is 15
Инкремент и декремент
Когда в выражении встречается постфиксный оператор инкремента или декремента, соответствующая пользовательская функция (operator++ или operator--) вызывается с целочисленным аргументом 0. Обычно её объявляют как T operator++(int) или T operator--(int), игнорируя аргумент. Постфиксные операторы инкремента и декремента обычно реализуются через префиксные версии:
struct X
{
// prefix increment
X& operator++()
{
// actual increment takes place here
return *this; // return new value by reference
}
// postfix increment
X operator++(int)
{
X old = *this; // copy old value
operator++(); // prefix increment
return old; // return old value
}
// prefix decrement
X& operator--()
{
// actual decrement takes place here
return *this; // return new value by reference
}
// postfix decrement
X operator--(int)
{
X old = *this; // copy old value
operator--(); // prefix decrement
return old; // return old value
}
};
Хотя канонические реализации префиксных операторов инкремента и декремента возвращают ссылку, как и для любой перегрузки оператора, возвращаемый тип задаётся пользователем; например, перегрузки этих операторов для std::atomic возвращают значение.
Бинарные арифметические операторы
Бинарные операторы обычно реализуются как функции, не являющиеся членами, чтобы сохранить симметричность (например, при сложении комплексного числа и целого числа, если operator+ является функцией-членом комплексного типа, скомпилируется только complex + integer, но не integer + complex). Поскольку каждому бинарному арифметическому оператору соответствует оператор составного присваивания, канонические формы бинарных операторов реализуются через соответствующие операторы составного присваивания:
class X
{
public:
X& operator+=(const X& rhs) // compound assignment (does not need to be a member,
{ // but often is, to modify the private members)
/* addition of rhs to *this takes place here */
return *this; // return the result by reference
}
// friends defined inside class body are inline and are hidden from non-ADL lookup
friend X operator+(X lhs, // passing lhs by value helps optimize chained a+b+c
const X& rhs) // otherwise, both parameters may be const references
{
lhs += rhs; // reuse compound assignment
return lhs; // return the result by value (uses move constructor)
}
};
Операторы сравнения
Стандартные библиотечные алгоритмы, такие как 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); // keep the same order
}
}; Обычно после определения operator< остальные операторы отношения реализуют через operator<.
inline bool operator< (const X& lhs, const X& rhs) { /* do actual comparison */ }
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) { /* do actual comparison */ }
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[]: константный и неконстантный варианты:
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[].
| operator[] может принимать только один индекс. Чтобы обеспечить семантику доступа к многомерному массиву, например реализовать доступ к элементу трёхмерного массива | (до C++23) |
| operator[] может принимать любое количество индексов. Например, operator[] класса трёхмерного массива, объявленный как #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! часто перегружают пользовательские классы, предназначенные для использования в логических контекстах. Такие классы также предоставляют пользовательскую функцию преобразования к логическому типу (пример из стандартной библиотеки см. в | (до C++11) |
| Поскольку встроенный оператор | (начиная с C++11) |
Редко перегружаемые операторы
Следующие операторы перегружают редко:
- Оператор взятия адреса operator&. Если унарный & применяется к lvalue неполного типа, а полный тип объявляет перегруженный operator&, не определено, будет ли оператор иметь встроенное значение или будет вызвана функция-оператор. Поскольку этот оператор можно перегружать, обобщённые библиотеки используют
std::addressofдля получения адресов объектов пользовательских типов. Самый известный пример канонической перегрузки operator& — класс MicrosoftCComPtrBase. Пример использования этого оператора в 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
{
// or C++17's std::gcd
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++.
| Отчёт об ошибке | Применён к | Поведение согласно опубликованной версии | Корректное поведение |
|---|---|---|---|
| CWG 1481 | C++98 | оператор префиксного инкремента, не являющийся членом, мог иметь параметр только классового или перечислимого типа либо ссылочного типа на такие типы | требований к типу нет |
| CWG 2931 | C++23 | функции-операторы-члены с явным параметром объекта могли иметь только параметр классового или перечислимого типа либо ссылочного типа на такие типы | запрещено |
См. также
| Часто используемые операторы | ||||||
|---|---|---|---|---|---|---|
| Присваивание |
Инкремент, декремент | Арифметические | Логические | Сравнения |
Доступ к членам | Другие |
a = b a += b a -= b a *= b a /= b a %= b a &= b a |= b a ^= b a <<= b a >>= b | ++a --a a++ a-- | +a -a a + b a - b a * b a / b a % b ~a a & b a | b a ^ b a << b a >> b | !a a && b a || b | a == b a != b a < b a > b a <= b a >= b a <=> b | a[...] *a &a a->b a.b a->*b a.*b | вызов функцииa(...) |
запятаяa, b |
||||||
условный операторa ? b : c |
||||||
| Специальные операторы | ||||||
|
|
||||||
Внешние ссылки
|
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/operators