Виммельбух, 3, перетяжка
Виммельбух, 3, перетяжка
Виммельбух, 3, перетяжка

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

Аватар Иван Бирюков
Отредактировано

66К открытий68К показов

В прошлой части мы рассмотрели основные аспекты использования перегрузки операторов. В этом материалы вашему вниманию будут представлены перегружаемые операторы 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==, !=

  • Семантика: проверка на равенство/неравенство. Смысл равенства очень сильно зависит от класса. В любом случае, учитывайте следующие свойства равенств:Рефлексивность, т.е. a == a.Симметричность, т.е. если a == b , то b == a.Транзитивность, т.е. если 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 T& X::operator->*(T V::* memptr){  return (operator*()).*memptr;}Здесь X — это умный указатель, V — тип, на который указывает X, а T — тип, на который указывает указатель-на-поле. Неудивительно, что этот оператор редко перегружают.

Унарный operator&

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

operator,

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

operator~

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

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

  • Семантика: позволяет скрытое или явное приведение объектов класса к другим типам.
  • Объявление://conversion to T, explicit or implicitX::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[]

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

Заключение

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

Следите за новыми постами
Следите за новыми постами по любимым темам
66К открытий68К показов