какие операторы можно перегружать
Перегрузка операторов в C++
Доброго времени суток!
Желание написать данную статью появилось после прочтения поста Перегрузка C++ операторов, потому что в нём не были раскрыты многие важные темы.
Самое главное, что необходимо помнить — перегрузка операторов, это всего лишь более удобный способ вызова функций, поэтому не стоит увлекаться перегрузкой операторов. Использовать её следует только тогда, когда это упростит написание кода. Но, не настолько, чтобы это затрудняло чтение. Ведь, как известно, код читается намного чаще, чем пишется. И не забывайте, что вам никогда не дадут перегрузить операторы в тандеме со встроенными типами, возможность перегрузки есть только для пользовательских типов/классов.
Синтаксис перегрузки
В данном случае, оператор оформлен как член класса, аргумент определяет значение, находящееся в правой части оператора. Вообще, существует два основных способа перегрузки операторов: глобальные функции, дружественные для класса, или подставляемые функции самого класса. Какой способ, для какого оператора лучше, рассмотрим в конце топика.
В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, то вы сами решаете как интерпретировать результат вычисления оператора).
Перегрузка унарных операторов
Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.
Бинарные операторы
Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.
Аргументы и возвращаемые значения
Оптимизация возвращаемого значения
При создании новых объектов и возвращении их из функции следует использовать запись как для вышеописанного примера оператора бинарного плюса.
Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:
Но на самом деле, в этом случае произойдет вызов конструктора в первой строке, далее вызов конструктора копирования, который скопирует объект, а далее, при раскрутке стека вызовется деструктор. При использовании первой записи компилятор изначально создаёт объект в памяти, в которую нужно его скопировать, таким образом экономится вызов конструктора копирования и деструктора.
Особые операторы
В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования []. Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.
Оператор запятая
В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер AxisPod в комментариях к предыдущей статье о перегрузке рассказал об одном.
Оператор разыменования указателя
Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.
Оператор присваивания
Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от «=». Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора «=». Пример:
Как можно заметить, в начале функции производится проверка на самоприсваивание. Вообще, в данном случае самоприсваивание безвредно, но ситуация не всегда такая простая. Например, если объект большой, можно потратить много времени на ненужное копирование, или при работе с указателями.
Неперегружаемые операторы
Некоторые операторы в C++ не перегружаются в принципе. По всей видимости, это сделано из соображений безопасности.
Перегрузка операторов
operator Ключевое слово объявляет функцию, указывающую, какой оператор-Symbol означает при применении к экземплярам класса. Это дает оператору более одного значения — «перегружает» его. Компилятор различает разные значения оператора, проверяя типы его операндов.
Синтаксис
тип operator operator-символ ( parameter-list )
Remarks
Функцию большинства встроенных операторов можно переопределить глобально или для отдельных классов. Перегруженные операторы реализуются в виде функции.
Имя перегруженного оператора — operator x, где x — это оператор, как показано в следующей таблице. Например, для перегрузки оператора сложения необходимо определить функцию с именем operator +. Аналогично, чтобы перегрузить оператор сложения и присваивания, += Определите функцию с именем operator + =.
Переопределяемые операторы
| Оператор | Имя | Тип |
|---|---|---|
| , | Запятая | Двоичные данные |
| ! | Логическое НЕ | Унарный |
| != | Неравенство | Двоичные данные |
| % | Modulus | Двоичные данные |
| %= | Назначение модуля | Двоичные данные |
| & | Побитовое И | Двоичные данные |
| & | Взятие адреса | Унарный |
| && | Логическое И | Двоичные данные |
| &= | Назначение побитового И | Двоичные данные |
| ( ) | Вызов функции | — |
| ( ) | Оператор приведения | Унарный |
| * | Умножение | Двоичные данные |
| * | Разыменование указателя | Унарный |
| *= | Присваивание умножения | Двоичные данные |
| + | Сложение | Двоичные данные |
| + | Унарный плюс | Унарный |
| ++ | Шаг 1 | Унарный |
| += | Присваивание сложения | Двоичные данные |
| — | Вычитание | Двоичные данные |
| — | Унарное отрицание | Унарный |
| — | Уменьшить 1 | Унарный |
| -= | Присваивание вычитания | Двоичные данные |
| -> | Выбор члена | Двоичные данные |
| — >* | Выбор указателя на член | Двоичные данные |
| / | Отдел | Двоичные данные |
| /= | Присваивание деления | Двоичные данные |
| Больше | Двоичные данные | |
| >= | Больше или равно | Двоичные данные |
| >> | Сдвиг вправо | Двоичные данные |
| >>= | Сдвиг вправо и присваивание | Двоичные данные |
| [ ] | Индекс массива | — |
| ^ | Исключающее ИЛИ | Двоичные данные |
| ^= | Исключающее ИЛИ/присваивание | Двоичные данные |
| | | Побитовое ИЛИ | Двоичные данные |
| |= | Назначение побитового включающего ИЛИ | Двоичные данные |
| || | Логическое ИЛИ | Двоичные данные |
| Дополнение до единицы | Унарный | |
| delete | Удаление | — |
| new | Создать | — |
| операторы преобразования | операторы преобразования | Унарный |
Существует 1 две версии унарных операторов инкремента и декремента: добавочное и инкрементное.
Перегрузка операторов в C++. Основы
В C++ этого ограничения нет — мы можем перегрузить практически любой известный оператор. Возможностей не счесть: можно выбрать любую комбинацию типов операндов, единственным ограничением является необходимость того, чтобы присутствовал как минимум один операнд пользовательского типа. То есть определить новый оператор над встроенными типами или переписать существующий нельзя.
Когда стоит перегружать операторы?
Приведём хороший и плохой примеры перегрузки операторов. Вышеупомянутое сложение матриц — наглядный случай. Здесь перегрузка оператора сложения интуитивно понятна и, при корректной реализации, не требует пояснений:
Примером плохой перегрузки оператора сложения будет сложение двух объектов типа «игрок» в игре. Что имел в виду создатель класса? Каким будет результат? Мы не знаем, что делает операция, и поэтому пользоваться этим оператором опасно.
Как перегружать операторы?
Большую часть операторов можно перегрузить как методами класса, так и простыми функциями, но есть несколько исключений. Когда перегруженный оператор является методом класса, тип первого операнда должен быть этим классом (всегда *this ), а второй должен быть объявлен в списке параметров. Кроме того, операторы-методы не статичны, за исключением операторов управления памятью.
При перегрузке оператора в методе класса он получает доступ к приватным полям класса, но скрытая конверсия первого аргумента недоступна. Поэтому бинарные функции обычно перегружают в виде свободных функций. Пример:
Когда унарные операторы перегружаются в виде свободных функций, им доступна скрытая конверсия аргумента, но этим обычно не пользуются. С другой стороны, это свойство необходимо бинарным операторам. Поэтому основным советом будет следующее:
Реализуйте унарные операторы и бинарные операторы типа “X=” в виде методов класса, а прочие бинарные операторы — в виде свободных функций.
Какие операторы можно перегружать?
Мы можем перегрузить почти любой оператор C++, учитывая следующие исключения и ограничения:
В следующей части вашему вниманию будут представлены перегружаемые операторы C++, в группах и по отдельности. Для каждого раздела характерна семантика, т.е. ожидаемое поведение. Кроме того, будут показаны типичные способы объявления и реализации операторов.
Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.
Перейти к регистрации
Общие правила перегрузки операторов
Нельзя определить новые операторы, например .
Не допускается переопределение операторов применительно ко встроенным типам данных.
Перегруженные операторы должны быть нестатической функцией-членом класса или глобальной функцией. Глобальная функция, которой требуется доступ к частным или защищенным членам класса, должна быть объявлена в качестве дружественной функции этого класса. Глобальная функция должна принимать хотя бы один аргумент, имеющий тип класса или перечисляемый тип либо являющийся ссылкой на тип класса или перечисляемый тип. Пример:
В предыдущем примере кода оператор «меньше чем» объявляется как функция-член; однако операторы сложения объявляются как глобальные функции, имеющие дружественный доступ. Обратите внимание, что для каждого оператора можно предоставить несколько реализаций. Выше для оператора сложения предоставлены две реализации, обеспечивающие его коммутативность. Это так же вероятно, что могут быть реализованы операторы, добавляющие в, в Point Point int Point и т. д.
Унарные операторы, объявленные как функции-члены, не принимают аргументов; при объявлении как глобальные функции они принимают один аргумент.
Бинарные операторы, объявленные как функции-члены, принимают один аргумент; при объявлении как глобальные функции они принимают два аргумента.
Перегруженные операторы не могут иметь аргументов по умолчанию.
Все перегруженные операторы, за исключением присваивания (operator =), наследуются производными классами.
Первым аргументов операторов, перегруженных в виде функций-членов, всегда является тип класса объекта, для которого вызывается этот оператор (класса, в котором объявлен оператор, или класса, производного от этого класса). Для первого аргумента никакие преобразования не предоставляются.
Для типов классов с перегруженными операторами на такую эквивалентность полагаться невозможно. Более того, некоторые из неявных требований, существующих при использовании этих операторов для базовых типов, для перегруженных операторов ослабляются. Например, оператор сложения/присваивания требует, += чтобы левый операнд был l-значением при применении к базовым типам. такое требование не предусмотрено, если оператор перегружен.
Для согласованности при определении перегруженных операторов рекомендуется следовать модели для встроенных типов. Если семантика перегруженного оператора существенно отличается от его значения в других контекстах, это может скорее запутывать ситуацию, чем приносить пользу.
Урок №134. Перегрузка операторов через методы класса
Обновл. 13 Сен 2021 |
Перегрузка операторов через методы класса очень похожа на перегрузку операторов через дружественные функции. Но при перегрузке оператора через метод класса левым операндом становится неявный объект, на который указывает скрытый указатель *this.
Перегрузка операторов через методы классов
Вспомним, как выглядит перегрузка оператора через дружественную функцию:
Конвертация перегрузки через дружественную функцию в перегрузку через метод класса довольно-таки проста:
Перегружаемый оператор определяется как метод класса, вместо дружественной функции ( Dollars::operator+ вместо friend operator+ ).
Левый параметр из функции перегрузки выбрасывается, вместо него — неявный объект, на который указывает указатель *this.
Обратите внимание, использование оператора + не изменяется (в обоих случаях dollars1 + 3 ), но реализация отличается. Наша дружественная функция с двумя параметрами становится методом класса с одним параметром, причем левый параметр в перегрузке через дружественную функцию ( &dollars ), в перегрузке через метод класса становится неявным объектом, на который указывает указатель *this.
Итак, если мы можем перегрузить оператор через дружественную функцию или через метод класса, то что тогда выбрать? Прежде чем мы дадим ответ на этот вопрос, вам нужно узнать еще несколько деталей.
Не всё может быть перегружено через дружественные функции
Не всё может быть перегружено через методы класса
На уроке о перегрузке операторов ввода и вывода мы перегружали оператор вывода для класса Point через дружественную функцию:
Однако через метод класса перегрузить оператор мы не сможем. Почему? Потому что при перегрузке через метод класса в качестве левого операнда используется текущий объект. В этом случае левым операндом является объект типа std::ostream. std::ostream является частью Cтандартной библиотеки C++. Мы не можем использовать std::ostream в качестве левого неявного параметра, на который бы указывал скрытый указатель *this, так как указатель *this может указывать только на текущий объект текущего класса, члены которого мы можем изменить, поэтому перегрузка оператора должна осуществляться через дружественную функцию.
Аналогично, хотя мы можем перегрузить operator+(Dollars, int) через метод класса (как мы делали выше), мы не можем перегрузить operator+(int, Dollars) через метод класса, поскольку int теперь является левым операндом, на который указатель *this указывать не может.
Перегрузка операторов через методы класса не используется, если левый операнд не является классом (например, int), или это класс, который мы не можем изменить (например, std::ostream).
Какой способ перегрузки и когда следует использовать?
В большинстве случаев язык C++ позволяет выбирать самостоятельно способ перегрузки операторов.
Но при работе с бинарными операторами, которые не изменяют левый операнд (например, operator+()), обычно используется перегрузка через обычную или дружественную функцию, поскольку такая перегрузка работает для всех типов данных параметров (даже если левый операнд не является объектом класса или является объектом класса, который изменить нельзя). Перегрузка через обычную/дружественную функцию имеет дополнительное преимущество «симметрии», так как все операнды становятся явными параметрами (а не как у перегрузки через метод класса, когда левый операнд становится неявным объектом, на который указывает указатель *this).
При работе с бинарными операторами, которые изменяют левый операнд (например, operator+=()), обычно используется перегрузка через методы класса. В этих случаях левым операндом всегда является объект класса, на который указывает скрытый указатель *this.
Унарные операторы обычно тоже перегружаются через методы класса, так как в таком случае параметры не используются вообще.
Для унарных операторов используйте перегрузку через методы класса.
Для перегрузки бинарных операторов, которые изменяют левый операнд (например, operator+=()) используйте перегрузку через методы класса, если это возможно.
Для перегрузки бинарных операторов, которые не изменяют левый операнд (например, operator+()) используйте перегрузку через обычные/дружественные функции.
