Spec-Zone.ru › C++

Перегрузка операторов

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

Синтаксис

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

operator op (1)
operator new
operator new []
(2)
operator delete
operator delete []
(3)
operator co_await (4) (начиная с C++20)
op - любой из следующих операторов:+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=>(начиная с C++20) && || ++ -- , ->* -> () []
1) Перегруженный оператор-пунктуация.
2) Функция выделения памяти.
3) Функция освобождения памяти.
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");                      // 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[] может принимать только один индекс. Чтобы обеспечить семантику доступа к многомерному массиву, например реализовать доступ к элементу трёхмерного массива a[i][j][k] = x;, operator[] должен возвращать ссылку на двумерную плоскость, у которой должен быть собственный operator[], возвращающий ссылку на одномерную строку; у неё, в свою очередь, должен быть operator[], возвращающий ссылку на элемент. Чтобы избежать такой сложности, некоторые библиотеки предпочитают перегружать operator() вместо этого, благодаря чему выражения для доступа к трёхмерным элементам имеют синтаксис в стиле Fortran: 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>

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

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

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

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

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/operators

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API