Основы перегрузки операторов. Основы перегрузки операторов Перегрузка бинарных операторов

Минимальный оператор присваивания - это

Void Cls::operator=(Cls other) { swap(*this, other); }

Согласно стандарту, это копирующий оператор присваивания.
Однако он также может выполнять перемещение, если у Cls есть перемещающий конструктор:

Cls a, b; a = std::move(b); // Работает как // Cls other(std::move(b)); a.operator=(other); // ^^^^^^^^^^ // перемещение: вызов Cls::Cls(Cls&&)

После обмена (swap) текущие члены класса оказываются во временном объекте other и удаляются при выходе из оператора присваивания.
При копирующем присваивании самому себе будет сделана лишняя копия, но никаких ошибок не будет.

Тип результата может быть любым.
Автоматически сгенерированный оператор присваивания имеет тип возвращаемого значения Cls& и возвращает *this . Это позволяет писать код вида a = b = c или (a = b) > c .
Но многие соглашения по стилю кода такое не одобряют, в частности см. CppCoreGuidelines ES.expr "Avoid complicated expressions" .

Для работы этого оператора присваивания нужны конструкторы копирования/перемещения и функция обмена (swap).
Вместе это выглядит так:

Class Cls { public: Cls() {} // Конструктор копирования Cls(const Cls& other) : x(other.x), y(other.y) {} // Конструктор перемещения Cls(Cls&& other) noexcept { swap(*this, other); } // Оператор присваивания void operator=(Cls other) noexcept { swap(*this, other); } // Обмен friend void swap(Cls& a, Cls& b) noexcept { using std::swap; // Добавление стандартной функции в список перегрузок... swap(a.x, b.x); // ... и вызов с использованием поиска по типам аргументов (ADL). swap(a.y, b.y); } private: X x; Y y; };

Конструктор копирования копирует каждый член класса.

Конструктор перемещения конструирует пустые члены класса, и затем обменивает их со своим аргуменом. Можно перемещать каждый член по отдельности, но удобнее использовать swap .

Функция swap может быть свободной функцией-другом. Многие алгоритмы ожидают наличие свободной функции swap , и вызывают ее через поиск по типу аргументов (ADL).
Раньше рекомендовалось также писать метод swap , чтобы можно было писать f().swap(x); , но с появлением семантики перемещения это стало не нужно.

Если функции не могут бросать исключений, то они должны быть помечены как noexcept . Это нужно для std::move_if_noexcept и других функций, которые могут использовать более эффективный код, если присваивание или конструктор не бросает исключений. Для такого класса выдают

Std::is_nothrow_copy_constructible == 0 std::is_nothrow_move_constructible == 1 std::is_nothrow_copy_assignable == 0 std::is_nothrow_move_assignable == 1

Хотя оператор присваивания и помечен как noexcept , при его вызове с аргументом const Cls& произойдет копирование, которое может бросить исключение. По этому is_nothrow_copy_assignable возвращает false .

Во многих языках программирования используются операторы: как минимум, присваивания (= , := или похожие) и арифметические операторы (+ , - , * и /). В большинстве языков со статической типизацией эти операторы привязаны к типам. Например, в Java сложение с оператором + возможно лишь для целых чисел, чисел с плавающей запятой и строк. Если мы определим свои классы для математических объектов, например, для матриц, мы можем реализовать метод их сложения, но вызвать его можно лишь чем-то вроде этого: a = b.add(c) .

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

Когда стоит перегружать операторы?

Запомните главное: перегружайте операторы тогда и только тогда, когда это имеет смысл. То есть если смысл перегрузки очевиден и не несёт в себе скрытых сюрпризов. Перегруженные операторы должны действовать так же, как и их базовые версии. Естественно, допустимы исключения, но лишь в тех случаях, когда они сопровождаются понятными объяснениями. Наглядным примером являются операторы << и >> стандартной библиотеки iostream , которые явно ведут себя не как обычные операторы .

Приведём хороший и плохой примеры перегрузки операторов. Вышеупомянутое сложение матриц - наглядный случай. Здесь перегрузка оператора сложения интуитивно понятна и, при корректной реализации, не требует пояснений:

Matrix a, b; Matrix c = a + b;

Примером плохой перегрузки оператора сложения будет сложение двух объектов типа “игрок” в игре. Что имел в виду создатель класса? Каким будет результат? Мы не знаем, что делает операция, и поэтому пользоваться этим оператором опасно.

Как перегружать операторы?

Перегрузка операторов похожа на перегрузку функций с особенными именами. На самом деле, когда компилятор видит выражение, в котором присутствует оператор и пользовательский тип, он заменяет это выражение вызовом соответствующей функции перегруженного оператора. Большая часть их названий начинается с ключевого слова operator , за которым следует обозначение соответствующего оператора. Когда обозначение не состоит из особых символов, например, в случае оператора приведения типа или управления памятью (new , delete и т.д.), слово operator и обозначение оператора должны разделяться пробелом (operator new), в прочих случаях пробелом можно пренебречь (operator+).

Большую часть операторов можно перегрузить как методами класса, так и простыми функциями, но есть несколько исключений. Когда перегруженный оператор является методом класса, тип первого операнда должен быть этим классом (всегда *this), а второй должен быть объявлен в списке параметров. Кроме того, операторы-методы не статичны, за исключением операторов управления памятью.

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

Class Rational { public: //Constructor can be used for implicit conversion from int: Rational(int numerator, int denominator = 1); Rational operator+(Rational const& rhs) const; }; int main() { Rational a, b, c; int i; a = b + c; //ok, no conversion necessary a = b + i; //ok, implicit conversion of the second argument a = i + c; //ERROR: first argument can not be implicitly converted }

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

Реализуйте унарные операторы и бинарные операторы типа “X =” в виде методов класса, а прочие бинарные операторы - в виде свободных функций.

Какие операторы можно перегружать?

Мы можем перегрузить почти любой оператор C++, учитывая следующие исключения и ограничения:

  • Нельзя определить новый оператор, например, operator** .
  • Следующие операторы перегружать нельзя:
    1. ?: (тернарный оператор);
    2. :: (доступ к вложенным именам);
    3. . (доступ к полям);
    4. .* (доступ к полям по указателю);
    5. sizeof , typeid и операторы каста.
  • Следующие операторы можно перегрузить только в качестве методов:
    1. = (присваивание);
    2. -> (доступ к полям по указателю);
    3. () (вызов функции);
    4. (доступ по индексу);
    5. ->* (доступ к указателю-на-поле по указателю);
    6. операторы конверсии и управления памятью.
  • Количество операндов, порядок выполнения и ассоциативность операторов определяется стандартной версией.
  • Как минимум один операнд должен быть пользовательского типа. Typedef не считается.

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

Перегрузка операторов в 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 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

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

Заключение

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

Последнее обновление: 20.10.2017

Перегрузка операторов позволяет определить действия, которые будет выполнять оператор. Перегрузка подразумевает создание функции, название которой содержит слово operator и символ перегружаемого оператора. Функция оператора может быть определена как член класса, либо вне класса.

Перегрузить можно только те операторы, которые уже определены в C++. Создать новые операторы нельзя.

Если функция оператора определена как отдельная функция и не является членом класса, то количество параметров такой функции совпадает с количеством операндов оператора. Например, у функции, которая представляет унарный оператор, будет один параметр, а у функции, которая представляет бинарный оператор, - два параметра. Если оператор принимает два операнда, то первый операнд передается первому параметру функции, а второй операнд - второму параметру. При этом как минимум один из параметров должен представлять тип класса

Рассмотрим пример с классом Counter, который представляет секундомер и хранит количество секунд:

#include << seconds << " seconds" << std::endl; } int seconds; }; Counter operator + (Counter c1, Counter c2) { return Counter(c1.seconds + c2.seconds); } int main() { Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds return 0; }

Здесь функция оператора не является частью класса Counter и определена вне его. Данная функция перегружает оператор сложения для типа Counter. Она является бинарной, поэтому принимает два параметра. В данном случае мы складываем два объекта Counter. Возвращает функция также объект Counter, который хранит общее количесто секунд. То есть по сути здесь операция сложения сводится к сложению секунд обоих объектов:

Counter operator + (Counter c1, Counter c2) { return Counter(c1.seconds + c2.seconds); }

При этом необязательно возвращать объект класса. Это может быть и объект встроенного примитивного типа. И также мы можем определять дополнительные перегруженные функции операторов:

Int operator + (Counter c1, int s) { return c1.seconds + s; }

Данная версия складывает объект Counter с числом и возвращает также число. Поэтому левый операнд операции должен представлять тип Counter, а правый операнд - тип int. И, к примеру, мы можем применить данную версию оператора следующим образом:

Counter c1(20); int seconds = c1 + 25; // 45 std::cout << seconds << std::endl;

Также функции операторов могут быть определены как члены классов. Если функция оператора определена как член класса, то левый операнд доступен через указатель this и представляет текущий объект, а правый операнд передается в подобную функцию в качестве единственного параметра:

#include class Counter { public: Counter(int sec) { seconds = sec; } void display() { std::cout << seconds << " seconds" << std::endl; } Counter operator + (Counter c2) { return Counter(this->seconds + c2.seconds); } int operator + (int s) { return this->seconds + s; } int seconds; }; int main() { Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds int seconds = c1 + 25; // 45 return 0; }

В данном случае к левому операнду в функциях операторов мы обращаемся через указатель this.

Какие операторы где переопределять? Операторы присвоения, индексирования (), вызова (()), доступа к члену класса по указателю (->) следует определять в виде функций-членов класса. Операторы, которые изменяют состояние объекта или непосредственно связаны с объектом (инкремент, декремент,), обычно также определяются в виде функций-членов класса. Все остальные операторы чаще определяются как отдельные функции, а не члены класса.

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

Ряд операторов перегружаются парами. Например, если мы определяем оператор == , то необходимо также определить и оператор != . А при определении оператора < надо также определять функцию для оператора > . Например, перегрузим данные операторы:

Bool operator == (Counter c1, Counter c2) { return c1.seconds == c2.seconds; } bool operator != (Counter c1, Counter c2) { return c1.seconds != c2.seconds; } bool operator > (Counter c1, Counter c2) { return c1.seconds > c2.seconds; } bool operator < (Counter c1, Counter c2) { return c1.seconds < c2.seconds; } int main() { Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 > c2; // true std::cout << b1 << std::endl; std::cout << b2 << std::endl; return 0; }

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

#include class Counter { public: Counter(int sec) { seconds = sec; } void display() { std::cout << seconds << " seconds" << std::endl; } Counter& operator += (Counter c2) { seconds += c2.seconds; return *this; } int seconds; }; int main() { Counter c1(20); Counter c2(10); c1 += c2; c1.display(); // 30 seconds return 0; }

Операции инкремента и декремента

Особую сложность может представлять переопределение операций инкремента и декремента, поскольку нам надо определить и префиксную, и постфиксную форму для этих операторов. Определим подобные операторы для типа Counter:

#include class Counter { public: Counter(int sec) { seconds = sec; } void display() { std::cout << seconds << " seconds" << std::endl; } // префиксные операторы Counter& operator++ () { seconds += 5; return *this; } Counter& operator-- () { seconds -= 5; return *this; } // постфиксные операторы Counter operator++ (int) { Counter prev = *this; ++*this; return prev; } Counter operator-- (int) { Counter prev = *this; --*this; return prev; } int seconds; }; int main() { Counter c1(20); Counter c2 = c1++; c2.display(); // 20 seconds c1.display(); // 25 seconds --c1; c1.display(); // 20 seconds return 0; }

Counter& operator++ () { seconds += 5; return *this; }

В самой функции можно определить некоторую логику по инкременту значения. В данном случае количество секунд увеличивается на 5.

Постфиксные операторы должны возвращать значение объекта до инкремента, то есть предыдущее состояние объекта. Чтобы постфиксная форма отличалась от префиксной постфиксные версии получают дополнительный параметр типа int, который не используется. Хотя в принципе мы можем его использовать.

Counter operator++ (int) { Counter prev = *this; ++*this; return prev; }

В любой науке есть стандартные обозначения, которые облегчают понимание идей. Например, в математике это умножение, деление, сложение и прочие символьные обозначения. Выражение (x + y * z) понять куда проще, чем «умножить y, с, z и прибавить к x». Представьте, до XVI века математика не имела символьных обозначений, все выражения прописывались словесно так, будто бы это художественный текст с описанием. А привычные для нас обозначения операций появились и того позже. Значение краткой символьной записи сложно переоценить. Исходя из таких соображений, в языки программирования были добавлены перегрузки операторов. Рассмотрим на примере.

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

Практически как и любой язык, C++ поддерживает множество операторов, работающих с типами данных, встроенными в стандарт языка. Но большинство программ используют пользовательские типы для решения тех или иных задач. Например, комплексная математика или реализуются в программе за счет представления комплексных чисел или матриц в виде пользовательских типов C++. Встроенные операторы не умеют распространять свою работу и совершать необходимые процедуры над пользовательскими классами, какими бы очевидными они не казались. Поэтому для сложения матриц, например, обычно создается отдельная функция. Очевидно, что вызов функции sum_matrix (A, B) в коде будет носить менее ясный характер, чем выражение A + B.

Рассмотрим примерный класс комплексных чисел:

//представим комплексное число в виде пары чисел с плавающей точкой. class complex { double re, im; public: complex (double r, double i) :re(r), im(i) {} //конструктор complex operator+(complex); //перегрузка сложения complex operator*(complex); //перегрузка умножения }; void main() { complex a{ 1, 2 }, b{ 3, 4 }, c{0, 0}; c = a + b; c = a.operator+(b); ////операторная функция может быть вызвана как любая функция, данная запись эквивалентна a+b c = a*b + complex(1, 3); //Выполняются обычные правила приоритета операций сложения и умножения }

Аналогичным образом можно сделать, например, перегрузку операторов ввода/вывода в C++ и приспособить их для вывода таких сложных структур как матрицы.

Операторы, доступные для перегрузки

Полный список всех операторов, для которых можно использовать механизм перегрузки:

Как видно из таблицы, перегрузка допустима для большинства операторов языка. Необходимости в перегрузке оператора быть не может. Это делается исключительно для удобства. Поэтому перегрузка операторов в Java, например, отсутствует. А теперь о следующем важном моменте.

Операторы, перегрузка которых запрещена

  • Разрешение области видимости - «::»;
  • Выбор члена - «.»;
  • Выбор члена через указатель на член - «.*»;
  • Тернарный условный оператор - «?:»;
  • Оператор sizeof;
  • Оператор typeid.

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

Ограничения

Ограничения перегрузки операторов:

  • Нельзя изменить бинарный оператор на унарный и наоборот, как и нельзя добавить третий операнд.
  • Нельзя создавать новые операторы помимо тех, что имеются. Данное ограничение способствует устранению множества неоднозначностей. Если есть необходимость в новом операторе, можно использовать для этих целей функцию, которая будет выполнять требуемое действие.
  • Операторная функция может быть либо членом класса, либо иметь хотя бы один аргумент пользовательского типа. Исключением являются операторы new и delete. Такое правило запрещает изменять смысл выражений в случае, если они не содержат объектов типов, определенных пользователем. В частности, нельзя создать операторную функцию, которая работала бы исключительно с указателями или заставить оператор сложения работать как умножение. Исключением являются операторы "=", "&" и "," для объектов классов.
  • Операторная функция с первым членом, принадлежащим к одному из встроенных типов данных языка C++, не может быть членом класса.
  • Название любой операторной функции начинается с ключевого слова operator, за которым следует символьное обозначение самого оператора.
  • Встроенные операторы определены таким образом, что между ними бывает связь. Например, следующие операторы эквивалентны друг другу: ++x; x + = 1; x = x + 1. После переопределения связь между ними не сохранится. О сохранении их совместной работы подобным образом с новыми типами программисту придется заботиться отдельно.
  • Компилятор не умеет думать. Выражения z + 5 и 5 +z (где z - комплексное число) будут рассматриваться компилятором по-разному. Первое представляет собой «complex + число», а второе - «число + комплекс». Поэтому для каждого выражения нужно определить собственный оператор сложения.
  • При поиске определения оператора компилятор не отдает преимущества ни функциям-членам класса, ни вспомогательным функциям, которые определяются вне класса. Для компилятора они равны.

Интерпретации бинарных и унарных операторов.

Бинарный оператор определяется как функция-член с одной переменной или как функция с двумя переменными. Для любого бинарного оператора @ в выражение a@b, @ справедливы конструкции:

a.operator@(b) или operator@(a, b).

Рассмотрим на примере класса комплексных чисел определение операций как членов класса и вспомогательных.

Class complex { double re, im; public: complex& operator+=(complex z); complex& operator*=(complex z); }; //вспомогательные функции complex operator+(complex z1, complex z2); complex operator+(complex z, double a);

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

Выбор, описывать функцию как член класса или вне его - дело, в общем-то, вкуса. В примере выше принцип отбора был следующий: если операция изменяет левый операнд (например, a + = b), то записать ее внутри класса и использовать передачу переменной по адресу, для ее непосредственного изменения; если операция ничего не меняет и просто возвращает новое значение (например, a + b) - вынести за рамки определения класса.

Определение перегрузки унарных операторов в C++ происходит аналогичным образом, с той разницей, что они делятся на два вида:

  • префиксный оператор, расположенный до операнда, - @a, например, ++i. o определяется как a.operator@() или operator@(aa);
  • постфиксный оператор, расположенный после операнда, - b@, например, i++. o определяется как b.operator@(int) или operator@(b, int)

Точно так же, как и с бинарными операторами для случая, когда объявление оператора находится и в классе, и вне класса, выбор будет осуществляться механизмами C++.

Правила выбора оператора

Пусть бинарный оператор @ применяется к объектам x из класса X и y из класса Y. Правила для разрешения x@y будут следующие:

  1. если X представляет собой класс, искать внутри него определение оператора operator@ в качестве члена X, либо базового класса X;
  2. просмотреть контекст, в котором находится выражение x@y;
  3. если X относится к пространству имен N, искать объявление оператора в N;
  4. если Y относится к пространству имен M, искать объявление оператора в M.

В случае если в 1-4 было найдено несколько объявлений оператора operator@, выбор будет осуществляться по правилам разрешения перегруженных функций.

Поиск объявлений унарных операторов происходит точно таким же способом.

Уточненное определение класса complex

Теперь построим класс комплексных чисел более подробным образом, чтобы продемонстрировать ряд озвученных ранее правил.

Class complex { double re, im; public: complex& operator+=(complex z) { //работает с выражениями вида z1 += z2 re += z.re; im += z.im; return *this; } complex& operator+=(double a) { //работает с выражениями вида z1 += 5; re += a; return *this; } complex (): re(0), im(0) {} //конструктор для инициализации по умолчанию. Таким образом, все объявленные комплексные числа будут иметь начальные значения (0, 0) complex (double r): re(r), im(0) {} // конструктор делает возможным выражение вида complex z = 11; эквивалентная запись z = complex(11); complex (double r, double i): re(r), im(i) {} //конструктор }; complex operator+(complex z1, complex z2) { //работает с выражениями вида z1 + z2 complex res = z1; return res += z2; //использование оператора, определенного как функция-член } complex operator+(complex z, double a) { //обрабатывает выражения вида z+2 complex res = z; return res += a; } complex operator+(double a, complex z) { //обрабатывает выражения вида 7+z complex res = z; return res += a; } //…

Как видно из кода, перегрузка операторов имеет весьма сложный механизм, который может сильно разрастись. Однако такой детальный подход позволяет осуществлять перегрузку даже для очень сложных структур данных. Например, перегрузка операторов C++ в классе шаблонов.

Подобное создание функций для всех и вся может быть утомительным и приводить к ошибкам. Например, если добавить третий тип в рассмотренные функции, то нужно будет рассмотреть операции из соображений сочетания трех типов. Придется написать 3 функции с одним аргументом, 9 - с двумя и 27 - с тремя. Поэтому в ряде случаев реализация всех этих функций и значительное уменьшение их количества могут быть достигнуты за счет использования преобразования типов.

Особые операторы

Оператор индексации«» должен всегда определяться как член класса, так как сводит поведение объекта к массиву. При этом аргумент индексирования может быть любого типа, что позволяет создавать, например, ассоциативные массивы.

Оператор вызова функции «()» может рассматриваться как бинарная операция. Например, в конструкции «выражение(список выражений)» левым операндом бинарной операции () будет «выражение», а правым - список выражений. Функция operator()() должна быть членом класса.

Оператор последовательности «,» (запятая) вызывается для объектов, если рядом с ними есть запятая. Однако в перечислении аргументов функции оператор не участвует.

Оператор разыменования «->» также должен определяться в качестве члена функции. По своему смыслу его можно определить как унарный постфиксный оператор. При этом он в обязательном порядке должен возвращать либо ссылку, либо указатель, позволяющий обращаться к объекту.

Также определяется только в качестве члена класса из-за его связи с левым операндом.

Операторы присваивания «=», адреса «&» и последовательности «,» должны определяться в блоке public.

Итог

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

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

  1. Выполняйте перегрузку операторов только для имитации привычной записи. Для того чтобы сделать код более удобочитаемым. Если код становится сложнее по структуре или читабельности, следует отказаться от перегрузки операторов и использовать функции.
  2. Для больших операндов с целью экономии места используйте для передачи аргументы с типом константных ссылок.
  3. Оптимизируйте возвращаемые значения.
  4. Не трогайте операцию копирования, если она подходит для вашего класса.
  5. Если копирование по умолчанию не подходит, меняйте или явно запрещайте возможность копирования.
  6. Следует предпочитать функции-члены над функциями-нечленами в случаях, когда функции требуется доступ к представлению класса.
  7. Указывайте пространство имен и обозначайте связь функций с их классом.
  8. Используйте функции-нечлены для симметричных операторов.
  9. Используйте оператор () для индексов в многомерных массивах.
  10. С осторожностью используйте неявные преобразования.