Перегрузка операторов
Настраивает операторы C++ для операндов пользовательских типов.
Синтаксис
Перегруженные операторы являются функциями со специальными именами функций:
operator op | (1) | |
operator type | (2) | |
operator newoperator new [] | (3) | |
operator deleteoperator delete [] | (4) | |
operator "" suffix-identifier | (5) | (с C++11) |
operator co_await | (6) | (с 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) |
Примечание: для перегрузки co_await, (с 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Ограничения
- Операторы
::(разрешение области видимости),.(доступ к членам),.*(доступ к членам через указатель на член) и?:(тройное условное выражение) не могут быть перегружены. - Нельзя создавать новые операторы, такие как
**,<>, или&|. - Нельзя изменить приоритет, группировку или количество операндов операторов.
- Перегрузка оператора
->должна либо возвращать указатель на значение, либо возвращать объект (по ссылке или по значению), для которого оператор->в свою очередь перегружен. - Перегрузки операторов
&&и||теряют короткое замыкание.
| (до C++17) |
Канонические реализации
Помимо вышеуказанных ограничений, язык не накладывает других ограничений на поведение перегруженных операторов или на тип возвращаемого значения (он не участвует в разрешении перегрузки), но в целом ожидается, что перегруженные операторы будут вести себя как можно более похоже на встроенные операторы: operator+ ожидается, что он будет складывать, а не умножать свои аргументы, operator= ожидается, что он будет присваивать и т. д. Связанные операторы должны вести себя аналогично (operator+ и operator+= выполняют ту же операцию сложения). Типы возвращаемых значений ограничены выражениями, в которых ожидается использование оператора: например, операторы присваивания возвращают ссылку, чтобы можно было написать a = b = c = d, так как встроенные операторы это позволяют.
У перегруженных операторов обычно есть следующие типичные, канонические формы:[1]
Оператор присваивания
Оператор присваивания (operator=) имеет особые свойства: подробнее см. копирование присваивания и перемещение присваивания.
Канонический оператор копирования присваивания должен быть безопасным при самоприсваивании и возвращать lhs по ссылке:
// 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;
}| Каноническое перемещение присваивания должно оставлять перемещённый объект в корректном состоянии (то есть в состоянии, в котором соблюдены инварианты класса), и либо ничего не делать, либо по крайней мере оставлять объект в корректном состоянии при самоприсваивании, и возвращать lhs по неконстантной ссылке и быть 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);
}Многие стандартные алгоритмы, от std::sort до std::accumulate , принимают FunctionObjects для настройки поведения. Нет особенно заметных канонических форм 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; }| Оператор неравенства автоматически генерируется компилятором, если определен struct Record
{
std::string name;
unsigned int floor;
double weight;
auto operator<=>(const Record&) const = default;
};
// records can now be compared with ==, !=, <, <=, >, and >=Подробнее см. сравнение по умолчанию. | (с C++20) |
Оператор индексации массива
Определяемые пользователем классы, предоставляющие доступ к массивам, позволяющий как чтение, так и запись, обычно определяют две перегрузки для operator[]: const и не-const варианты:
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) |
Если тип значения известен как скалярный тип, вариант const должен возвращать значение.
Когда прямой доступ к элементам контейнера нежелателен или невозможен или есть необходимость различать использование lvalue c[i] = v; и rvalue v = c[i];, operator[] может возвращать прокси. Смотрите, например, std::bitset::operator[].
Поскольку оператор индексации может принимать только один индекс до C++23, для предоставления семантики доступа к многомерным массивам, например, для реализации доступа к трёхмерному массиву a[i][j][k] = x;, operator[] должен возвращать ссылку на двумерную плоскость, которая должна иметь свой собственный operator[] , возвращающий ссылку на одномерную строку, которая должна иметь operator[] , возвращающий ссылку на элемент. Чтобы избежать этой сложности, некоторые библиотеки выбирают перегрузку operator() вместо этого, чтобы выражения доступа к 3D массивам имели синтаксис, похожий на Fortran a(i, j, k) = x;.
|
#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<<=. Канонические реализации обычно следуют шаблону для бинарных арифметических операторов, описанному выше.
Оператор логического отрицания
| Оператор | (до C++11) |
| Поскольку встроенный оператор | (с C++11) |
Редко перегружаемые операторы
Следующие операторы редко перегружаются:
- Оператор взятия адреса,
operator&. Если унарный & применяется к lvalue неполного типа, и полный тип объявляет перегруженныйoperator&, не определено, будет ли оператор иметь встроенное значение или вызываться функция оператора. Поскольку этот оператор может быть перегружен, общие библиотеки используютstd::addressofдля получения адресов объектов пользовательских типов. Наиболее известный пример канонически перегруженного оператора & — это класс 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++.
| DR | Применено к | Поведение, как опубликовано | Правильное поведение |
|---|---|---|---|
| CWG 1481 | C++98 | Оператор префиксного инкремента без члена мог иметь только параметр типа класс или перечисление | Требование к типу отсутствует |
См. также
- Порядок следования операторов
- Альтернативный синтаксис операторов
- Поиск аргументов, зависящих от аргумента
| Общие операторы | ||||||
|---|---|---|---|---|---|---|
| присваивания |
инкремент декремент | арифметические | логические | сравнения | доступ к членам | прочие |
|
|
|
|
|
|
| вызов функции |
a(...) |
||||||
| запятая | ||||||
a, b |
||||||
| условный | ||||||
a ? b : c |
||||||
| Специальные операторы | ||||||
|
|
||||||
Внешние ссылки
|
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/w/cpp/language/operators