Перегрузка операторов в C++. Способы применения

В прошлой части мы рассмотрели основные аспекты использования перегрузки операторов. В этом материалы вашему вниманию будут представлены перегружаемые операторы C++. Для каждого раздела характерна семантика, т.е. ожидаемое поведение. Кроме того, будут показаны типичные способы объявления и реализации операторов. 

В примерах кода X означает пользовательский тип, для которого реализован оператор. T — это необязательный тип, пользовательский либо встроенный. Параметры бинарного оператора будут называться lhs и rhs. Если оператор будет объявлен как метод класса, у его объявления будет префикс X::.


operator=

  • Определение справа налево: в отличие от большинства операторов, operator= правоассоциативен, т.е. a = b = c означает a = (b = c).

Копирование

  • Семантика: присваивание a = b. Значение или состояние b передаётся a. Кроме того, возвращается ссылка на a. Это позволяет создавать цепочки вида c = a = b.
  • Типичное объявление: X& X::operator= (X const& rhs). Возможны другие типы аргументов, но используется это нечасто.
  • Типичная реализация:
    X& X::operator= (X const& rhs) {
      if (this != &rhs) {
        //perform element wise copy, or:
        X tmp(rhs); //copy constructor
        swap(tmp);
      }
      return *this;
    }

Перемещение (начиная с C++11)

  • Семантика: присваивание a = temporary(). Значение или состояние правой величины присваивается a путём перемещения содержимого. Возвращается ссылка на a.
  • Типичные объявление и реализация:
    X& X::operator= (X&& rhs) {
      //take the guts from rhs
      return *this;
    }
  • Сгенерированный компилятором operator=: компилятор может создать только два вида этого оператора. Если же оператор не объявлен в классе, компилятор пытается создать публичные операторы копирования и перемещения. Начиная с  C++11 компилятор может создавать оператор по умолчанию:
    X& X::operator= (X const& rhs) = default;

    Сгенерированный оператор просто копирует/перемещает указанный элемент, если такая операция разрешена.


operator+, -, *, /, %

  • Семантика: операции сложения, вычитания, умножения, деления, деления с остатком. Возвращается новый объект с результирующим значением.
  • Типичные объявление и реализация:
    X operator+ (X const lhs, X const rhs) {
      X tmp(lhs);
      tmp += rhs;
      return tmp;
    }

    Обычно, если существует operator+, имеет смысл также перегрузить и operator+= для того, чтобы использовать запись a += b вместо a = a + b. Если же operator+= не перегружен, реализация будет выглядеть примерно так:

    X operator+ (X const& lhs, X const& rhs) {
      // create a new object that represents the sum of lhs and rhs:
      return lhs.plus(rhs);
    }

Унарные operator+, –

  • Семантика: положительный или отрицательный знак. operator+ обычно ничего не делает и поэтому почти не используется. operator- возвращает аргумент с противоположным знаком.
  • Типичные объявление и реализация:
    X X::operator- () const {
      return /* a negative copy of *this */;  
    }
     
    X X::operator+ () const {
      return *this;
    }

operator<<, >>

  • Семантика: во встроенных типах операторы используются для битового сдвига левого аргумента. Перегрузка этих операторов с именно такой семантикой встречается редко, на ум приходит лишь std::bitset. Однако, для работы с потоками была введена новая семантика, и перегрузка операторов ввода/вывода весьма распространена.
  • Типичные объявление и реализация: поскольку в стандартные классы iostream добавлять методы нельзя, операторы сдвига для определённых вами классов нужно перегружать в виде свободных функций:
    ostream& operator<< (ostream& os, X const& x) {
      os << /* the formatted data of rhs you want to print */;
      return os;
    }
     
    istream& operator>> (istream& is, X& x) {
      SomeData sd;
      SomeMoreData smd;
      if (is >> sd >> smd) {
        rhs.setSomeData(sd);
        rhs.setSomeMoreData(smd);
      }
      return lhs;
    }

    Кроме того, тип левого операнда может быть любым классом, которые должен вести себя как объект ввода/вывода, то есть правый операнд может быть и встроенного типа.

    MyIO& MyIO::operator<< (int rhs) {
      doYourThingWith(rhs);
      return *this;
    }

Бинарные operator&, |, ^

  • Семантика: Битовые операции “и”, “или”, “исключающее или”. Эти операторы перегружаются очень редко. Опять же, единственным примером является std::bitset.

operator+=, -=, *=, /=, %=

  • Семантика: a += b обычно означает то же, что и a = a + b. Поведение остальных операторов аналогично.
  • Типичные определение и реализация: поскольку операция изменяет левый операнд, скрытое приведение типов нежелательно. Поэтому эти операторы должны быть перегружены как методы класса.
    X& X::operator+= (X const& rhs) {
      //apply changes to *this
      return *this;
    }

operator&=, |=, ^=, <<=, >>=

  • Семантика: аналогична operator+=, но для логических операций. Эти операторы перегружаются так же редко, как и operator| и т.д. operator<<= и operator>>= не используются для операций ввода/вывода, поскольку operator<< и operator>> уже изменяют левый аргумент.

operator==, !=

  • Семантика: проверка на равенство/неравенство. Смысл равенства очень сильно зависит от класса. В любом случае, учитывайте следующие свойства равенств:
    1. Рефлексивность, т.е. a == a.
    2. Симметричность, т.е. если a == b , то b == a.
    3. Транзитивность, т.е. если a == b и b == c, то a == c.
  • Типичные объявление и реализация:
    bool operator== (X const& lhs, X cosnt& rhs) {
      return /* check for whatever means equality */
    }
     
    bool operator!= (X const& lhs, X const& rhs) {
      return !(lhs == rhs);
    }

    Вторая реализация operator!= позволяет избежать повторов кода и исключает любую возможную неопределённость в отношении любых двух объектов.


operator<, <=, >, >=

  • Семантика: проверка на соотношение (больше, меньше и т.д.). Обычно используется, если порядок элементов однозначно определён, то есть сложные объекты с несколькими характеристиками сравнивать бессмысленно.
  • Типичные объявление и реализация:
    bool operator< (X const& lhs, X const& rhs) {
      return /* compare whatever defines the order */
    }
     
    bool operator> (X const& lhs, X const& rhs) {
      return rhs < lhs;
    }

    Реализация operator> с использованием operator< или наоборот обеспечивает однозначное определение. operator<= может быть реализован по-разному, в зависимости от ситуации. В частности, при отношении строго порядка operator== можно реализовать лишь через operator<:

    bool operator== (X const& lhs, X const& rhs) {
      return !(lhs < rhs) && !(rhs < lhs);
    }

operator++, –

  • Семантика: a++ (постинкремент) увеличивает значение на 1 и возвращает старое значение. ++a (преинкремент) возвращает новое значение. С декрементом operator-- все аналогично.
  • Типичные объявление и реализация:
    X& X::operator++() { //preincrement
      /* somehow increment, e.g. *this += 1*/;
      return *this;
    }
     
    X X::operator++(int) { //postincrement
      X oldValue(*this);
      ++(*this);
      return oldValue;
    }

operator()

  • Семантика: исполнение объекта-функции (функтора). Обычно используется не для изменения объекта, а для использования его в качестве функции.
  • Нет ограничений на параметры: в отличие от прошлых операторов, в этом случае нет никаких ограничений на количество и тип параметров. Оператор может быть перегружен только как метод класса.
  • Пример объявления:
    Foo X::operator() (Bar br, Baz const& bz);

operator[]

  • Семантика: доступ к элементам массива или контейнера, например, в std::vector, std::map, std::array.
  • Объявление: тип параметра может быть любым. Тип возвращаемого значения обычно является ссылкой на то, что хранится в контейнере. Часто оператор перегружается в двух версиях, константной и неконстантной:
    Element_t& X::operator[](Index_t const& index);
     
    const Element_t& X::operator[](Index_t const& index) const;

operator!

  • Семантика: отрицание в логическом смысле.
  • Типичные объявление и реализация:
    bool X::operator!() const {
      return !/*some evaluation of *this*/;
    }

explicit operator bool

  • Семантика: использования в логическом контексте. Чаще всего используется с умными указателями.
  • Реализация:
    explicit X::operator bool() const {
      return /* if this is true or false */;
    }

operator&&, ||

  • Семантика: логические “и”, “или”. Эти операторы определены только для встроенного логического типа и работают по “ленивому” принципу, то есть второй аргумент рассматривается, только если первый не определяет результат. При перегрузке это свойство теряется, поэтому перегружают эти операторы редко.

Унарный operator*

  • Семантика: разыменовывание указателя. Обычно перегружается для классов с умными указателями и итераторами. Возвращает ссылку на то, куда указывает объект.
  • Типичные объявление и реализация:
    T& X::operator*() const {
      return *_ptr;
    }

operator->

  • Семантика: доступ к полю по указателю. Как и предыдущий, этот оператор перегружается для использования с умными указателями и итераторами. Если в коде встречается оператор ->, компилятор перенаправляет вызовы на operator->, если возвращается результат пользовательского типа.
  • Usual implementation:
    T* X::operator->() const { return _ptr; }

operator->*

  • Семантика: доступ к указателю-на-поле по указателю. Оператор берёт указатель на поле и применяет его к тому, на что указывает *this, то есть objPtr->*memPtr — это то же самое, что и (*objPtr).*memPtr. Используется очень редко.
  • Возможная реализация:
    template <typename T, class V>
    T& X::operator->*(T V::* memptr)
    {
      return (operator*()).*memptr;
    }

    Здесь X — это умный указатель, V — тип, на который указывает X, а T — тип, на который указывает указатель-на-поле. Неудивительно, что этот оператор редко перегружают.


Унарный operator&

  • Семантика: адресный оператор. Этот оператор перегружают очень редко.

operator,

  • Семантика: встроенный оператор “запятая”, применённый к двум выражениям, выполняет их оба в порядке записи и возвращает значение второго из них. Перегружать его не рекомендуется.

operator~

  • Семантика: оператор побитовой инверсии. Один из наиболее редко используемых операторов.

Операторы приведения типов

  • Семантика: позволяет скрытое или явное приведение объектов класса к другим типам.
  • Объявление:
    //conversion to T, explicit or implicit
    X::operator T() const;  
     
    //explicit conversion to U const&
    explicit X::operator U const&() const;
     
    //conversion to V&
    V& X::operator V&();

    Эти объявления выглядят странно, поскольку в них отсутствует тип возвращаемого значения. Он является частью имени оператора у не указывается дважды. Стоит помнить, что большое количество скрытых приведений может повлечь за собой непредвиденные ошибки в работе программы.


operator new, new[], delete, delete[]

Эти операторы полностью отличаются от всех вышеупомянутых, поскольку они не работают с пользовательскими типами. Их перегрузка весьма сложна, и поэтому не будет здесь рассматриваться.


Заключение

Основной мыслью является следующее: не стоит перегружать операторы только потому, что вы умеете это делать. Перегружайте их лишь в тех случаях, когда это выглядит естественным и необходимым. Но помните, что если вы перегрузите один оператор, то придётся перегружать и другие.

Перевод статьи «Operator Overloading: Common Practice»