Namespaces
Variants

operator overloading

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Настраивает операторы C++ для операндов пользовательских типов.

Содержание

Синтаксис

Функции-операторы — это функции со специальными именами:

operator op (1)
operator new
operator new []
(2)
operator delete
operator delete []
(3)
operator co_await (4) (начиная с C++20)
op - любой из следующих операторов: + - * / % ^ & | ~ ! = < > + = - = * = / = % = ^ = & = | = << >> >>= <<= == ! = <= >= <=> (начиная с C++20) && || ++ -- , - > * - > ( ) [ ]
1) Перегруженный оператор пунктуации.
4) Перегруженный оператор co_await для использования в co_await выражениях .

Поведение непунктуационных операторов описано на их собственных страницах. Если не указано иное, оставшееся описание на этой странице не применяется к этим функциям.

Объяснение

Когда оператор появляется в выражении , и хотя бы один из его операндов имеет классовый тип или перечисляемый тип , тогда разрешение перегрузки используется для определения пользовательской функции, которая должна быть вызвана среди всех функций, чьи сигнатуры соответствуют следующим:

Выражение Как функция-член Как внешняя функция Пример
@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 )

В этой таблице @ представляет все соответствующие операторы: все префиксные операторы в @a, все постфиксные операторы кроме -> в a@, все инфиксные операторы кроме = в a@b.

Кроме того, для операторов сравнения == , ! = , < , > , <= , >= , <=> , разрешение перегрузки также рассматривает переписанные кандидаты 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; }
**Примечание:** Весь код C++ внутри тегов `
` и `` оставлен без изменений, как и требовалось. 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 [ ]

Ключевые слова

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 + = 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
Специальные операторы

static_cast преобразует один тип в другой связанный тип
dynamic_cast преобразует в пределах иерархий наследования
const_cast добавляет или удаляет cv -квалификаторы
reinterpret_cast преобразует тип в несвязанный тип
C-style приведение преобразует один тип в другой с помощью комбинации static_cast , const_cast и reinterpret_cast
new создает объекты с динамической продолжительностью хранения
delete уничтожает объекты, ранее созданные выражением new, и освобождает полученную область памяти
sizeof запрашивает размер типа
sizeof... запрашивает размер пакета (since C++11)
typeid запрашивает информацию о типе
noexcept проверяет, может ли выражение генерировать исключение (since C++11)
alignof запрашивает требования выравнивания типа (since C++11)

Внешние ссылки

  1. Перегрузка операторов в C++ FAQ на StackOverflow