Spec-Zone.ru › C++

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

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

Синтаксис

Перегруженные операторы являются функциями со специальными именами функций:

operator op (1)
operator type (2)
operator new
operator new []
(3)
operator delete
operator delete []
(4)
operator "" suffix-identifier (5) (с C++11)
operator co_await (6) (с C++20)
op - любой из следующих операторов:+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=>(с C++20) && || ++ -- , ->* -> ( ) [ ]
1) перегруженный оператор;
2) функция преобразования пользовательского типа;
3) функция выделения памяти;
4) функция освобождения памяти;
5) пользовательская литерал;
6) перегруженный оператор 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)

Примечание: для перегрузки 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; }

Оператор неравенства автоматически генерируется компилятором, если определен operator== . Аналогично, четыре оператора отношения автоматически генерируются компилятором, если определён оператор трёхстороннего сравнения operator<=> . operator== и operator!= , в свою очередь, генерируются компилятором, если operator<=> определён как по умолчанию:

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;.

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 для получения адресов объектов пользовательских типов. Наиболее известный пример канонически перегруженного оператора & — это класс 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
{
    // 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 = 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 cast преобразует один тип в другой, используя смесь static_cast, const_cast, и reinterpret_cast
new создаёт объекты с динамическим сроком жизни
delete уничтожает объекты, ранее созданные с помощью выражения new, и освобождает полученную область памяти
sizeof запрашивает размер типа
sizeof... запрашивает размер пакета параметров (с C++11)
typeid запрашивает информацию о типе типа
noexcept проверяет, может ли выражение выбросить исключение (с C++11)
alignof запрашивает требования к выравниванию типа (с C++11)

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

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

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

Spec-Zone.ru

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