Перегрузка операторов в C++
Доброго времени суток!
Желание написать данную статью появилось после прочтения поста Перегрузка C++ операторов, потому что в нём не были раскрыты многие важные темы.
Самое главное, что необходимо помнить — перегрузка операторов, это всего лишь более удобный способ вызова функций, поэтому не стоит увлекаться перегрузкой операторов. Использовать её следует только тогда, когда это упростит написание кода. Но, не настолько, чтобы это затрудняло чтение. Ведь, как известно, код читается намного чаще, чем пишется. И не забывайте, что вам никогда не дадут перегрузить операторы в тандеме со встроенными типами, возможность перегрузки есть только для пользовательских типов/классов.
Синтаксис перегрузки
В данном случае, оператор оформлен как член класса, аргумент определяет значение, находящееся в правой части оператора. Вообще, существует два основных способа перегрузки операторов: глобальные функции, дружественные для класса, или подставляемые функции самого класса. Какой способ, для какого оператора лучше, рассмотрим в конце топика.
В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, то вы сами решаете как интерпретировать результат вычисления оператора).
Перегрузка унарных операторов
Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.
Бинарные операторы
Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.
Аргументы и возвращаемые значения
Оптимизация возвращаемого значения
При создании новых объектов и возвращении их из функции следует использовать запись как для вышеописанного примера оператора бинарного плюса.
Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:
Но на самом деле, в этом случае произойдет вызов конструктора в первой строке, далее вызов конструктора копирования, который скопирует объект, а далее, при раскрутке стека вызовется деструктор. При использовании первой записи компилятор изначально создаёт объект в памяти, в которую нужно его скопировать, таким образом экономится вызов конструктора копирования и деструктора.
Особые операторы
В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования []. Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.
Оператор запятая
В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер AxisPod в комментариях к предыдущей статье о перегрузке рассказал об одном.
Оператор разыменования указателя
Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.
Оператор присваивания
Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от «=». Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора «=». Пример:
Как можно заметить, в начале функции производится проверка на самоприсваивание. Вообще, в данном случае самоприсваивание безвредно, но ситуация не всегда такая простая. Например, если объект большой, можно потратить много времени на ненужное копирование, или при работе с указателями.
Неперегружаемые операторы
Некоторые операторы в C++ не перегружаются в принципе. По всей видимости, это сделано из соображений безопасности.
Что такое перегрузка операторов
Для тех, кто пытался, но не понял.
В статье про С++ мы упоминали перегрузку операторов. Это мощный и гибкий инструмент, который может оказаться опасным и непредсказуемым в неумелых руках. Настало время разобраться.
👉 Опытным программистам: мы намеренно упростим детали для понимания сути. Ну сорян.
На примере сложения
Во всех языках есть оператор «плюс» — обычно он умеет складывать числа и соединять строки:
Допустим, мы пишем софт для интернет-магазина, и у нас есть там класс объектов «заказ». Напомним, что класс — это как бы чертёж, по которому создаются объекты. А объект — это такая коробка с данными и функциями, которыми мы можем управлять как единым целым. Подробнее об этом — в статьях про объекты и классы.
В объекте типа «заказ» лежит куча всего:
Допустим, наша система устроена так, что у любого заказа может быть два идентификатора пользователя: постоянный или временный.
В какой-то момент пользователь с временным идентификатором логинится в систему, и нам хочется сделать следующую операцию:
Обе части выражения — это объекты класса «Заказ». А наш язык программирования не знает, что значит «сложить два объекта класса „Заказ“». Он не знает:
Вроде бы простая операция — а столько вопросов. Вот этому всему мы можем обучить оператор «+», и это будет перегрузка оператора.
👉 Короче
Перегрузка оператора — это когда мы обучаем язык программирования, как оператору типа плюс, минус, умножить и т. д. вести себя с определённым типом вводных — например, с объектами, матрицами или картинками.
В случае с нашим примером мы можем сказать, что если складываются два заказа, делай следующее:
Довольно много действий для одного плюса, не находите?
Что хорошего в перегрузке
Перегруженные операторы позволяют совершать привычные операции над необычными объектами. Если на интуитивном уровне логично, что можно складывать некоторые вещи между собой, то первое, что приходит в голову — использовать для этого стандартный плюс. Единственное, что нужно сделать — перегрузить его новыми обязанностями, а потом можно дальше им пользоваться как привычным сложением, даже с новыми объектами.
В результате программист экономит много кода, не пишет специальный отдельный обработчик такого сложения и не держит в голове параметры его вызова.
Чем опасна перегрузка операторов
Когда вы используете в коде перегруженный оператор, он выглядит как самый обычный оператор. Ну складывает и складывает. Ну умножает и умножает, чего такого-то?
Это, с одной стороны, элегантно. А с другой, создаёт проблемы в отладке.
А ещё, в особо экзотических случаях и больших проектах, программист шутки ради может перегрузить оператор сложения так, что он будет не складывать, а вычитать. И заметить, в чём тут ошибка, в таких случаях бывает очень сложно.
И что?
Перегрузка операторов — это полезно, но сложно.
Если программист не понимает полностью механизма работы перегрузок, лучше не перегружать.
Если понимает — он молодец и может учить стандартные инструменты нестандартному поведению.
ООП. Часть 4. Полиморфизм, перегрузка методов и операторов
C# позволяет использовать один метод для разных типов данных и даже переопределить логику операторов. Разбираемся в перегрузках.
Полиморфизм (от греч. poly — много и morphe — форма) — один из главных столпов объектно-ориентированного программирования. Его суть заключается в том, что один фрагмент кода может работать с разными типами данных.
В C# это реализуется с помощью перегрузок (overloading).
Все статьи про ООП
Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Перегрузка методов
C# — строго типизированный язык. Это значит, что вы не можете поместить строку в переменную типа int — сначала нужно провести преобразование. Так же и в метод нельзя передать параметр типа float, если при объявлении метода был указан тип double.
Однако если вы экспериментировали с методом WriteLine() класса Console, то могли заметить, что в него можно передавать аргументы разных типов:
Кажется, что нарушена типизация, но компилятор не выдаёт ошибку. Вместо этого всё успешно выводится на экран:
Так происходит потому, что у метода WriteLine() есть перегрузки — методы с таким же названием, но принимающие другие аргументы:
Когда вы вызовете метод Sum(), компилятор по переданным аргументам узнает, какую из его перегрузок вы имели в виду — так же, как это происходит с методом WriteLine().
При этом стоит учитывать, что значение имеют только типы и количество передаваемых аргументов. Например, можно написать такие перегрузки:
У этих методов одинаковые параметры, но разный возвращаемый тип. Попытка скомпилировать такой код приведёт к ошибке — так же, как и создание перегрузки с такими же аргументами, но с другими названиями:
Перегрузка конструкторов
То же самое можно сделать и с конструкторами классов:
Альтернатива этому решению — указать значения для аргументов по умолчанию:
Несмотря на, то что здесь меньше кода, на мой взгляд, это может запутать. Потому что придётся каждый раз заполнять все значения, даже если нужен только один аргумент из конца списка. Перегрузка же позволяет определить и порядок параметров (если они разных типов).
Перегрузка операторов
Перегрузить можно даже операторы, то есть:
Так как использоваться этот оператор должен без объявления экземпляра класса (item1 + item2, а не item1 item1.+ item2), то указываются модификаторы public static.
Например, мы хотим улучшать предметы в играх. Во многих MMO 1 популярна механика, когда один предмет улучшается за счёт другого. Мы можем сделать это с помощью перегрузки оператора сложения:
Теперь при сложении двух объектов класса Item мы будем получать третий объект с улучшенными параметрами. Вот пример использования такого оператора:
В результате в консоль будет выведено следующее:
1) MMO (англ. Massively Multiplayer Online Game, MMO, MMOG)
Массовая многопользовательская онлайн-игра
Перегрузка операторов преобразования типов
Хотя типизация в C# строгая, типы можно преобразовывать. Например, мы можем конвертировать число типа float в число типа int:
С помощью перегрузки операторов преобразования типов мы можем прописать любую логику для конвертации объектов. Для наглядности создадим класс Hero:
В этом классе хранятся данные о персонаже. В MMO часто можно увидеть такой параметр, как мощь — это сумма всех характеристик героя или предмета. Например, её можно посчитать по следующей формуле:
Мощь = (сила + ловкость + интеллект) * уровень.
Мы можем использовать преобразование типов, чтобы автоматически переводить объект в его мощь. Для этого нужно использовать такую конструкцию.
Модификатор implicit говорит компилятору, что преобразование может быть неявным. То есть оно сработает, если написать так:
Explicit, наоборот, означает, что преобразование должно быть явным:
Вот как будет выглядеть перегрузка преобразования объекта класса Hero в int:
Вот как она будет использоваться:
Вывод в консоль будет следующим:
Проблемы читаемости
Несмотря на то, что перегрузки помогают быстро реализовать какой-нибудь функционал, они могут навредить читаемости. Например, не всегда можно сразу понять, зачем в коде складываются два объекта.
Или же непонятно, зачем конвертировать Hero в int. Ясность вносит название переменной (power), но этого недостаточно.
В большинстве случаев лучше использовать более простые решения. Например, можно создать для объекта свойство Power, которое возвращает сумму характеристик.
Вместо сложения объектов можно написать метод Enhance(), который будет принимать другой предмет и прибавлять его характеристики к текущему.
Такие перегрузки стоит использовать либо если вы работаете над кодом один, либо если есть подробная документация.
Домашнее задание
Создайте игру, в которой можно улучшать одни предметы с помощью других. При улучшении предмету добавляется опыт. Когда его станет достаточно, необходимо повысить уровень. Количество опыта должно зависеть от мощи.
Заключение
Полиморфизм — очень удобный инструмент. Однако в этой статье была затронута лишь его часть; чтобы начать работать со второй, нужно ознакомиться с принципами наследования и абстракции.
Вы можете изучить ООП гораздо глубже, записавшись на курс «Профессия C#-разработчик». Он раскрывает лучшие практики работы с C# в объектно-ориентированной парадигме программирования.
Перегрузка функций
C++ позволяет определять несколько функций с одинаковым именем в одной области. Эти функции называются перегруженными функциями. Перегруженные функции позволяют указать другую семантику для функции в зависимости от типов и числа аргументов.
Можно перегружать как функции-члены, так и функции, не являющиеся членами. В следующей таблице указаны компоненты объявления функций, используемые языком C++ для различения групп функций с одинаковым именем в одной области.
Заметки по перегрузке
| Элемент объявления функции | Использование для перегрузки |
|---|---|
| Тип возвращаемого функцией значения | Нет |
| Число аргументов | Да |
| Тип аргументов | Да |
| Наличие или отсутствие многоточия | Да |
| Использование typedef имен | Нет |
| Незаданные границы массива | Нет |
| const или volatile | Да, при применении ко всей функции |
| Квалификаторы ref | Да |
Пример
В следующем примере показано использование перегрузки.
В приведенном выше коде отображается перегрузка функции print в области видимости файла.
Аргумент по умолчанию не считается частью типа функции. Поэтому он не используется при выборе перегруженных функций. Две функции, которые различаются только в своих аргументах, считаются множественными определениями, а не перегруженными функциями.
Аргументы по умолчанию не могут быть указаны для перегруженных операторов.
Сопоставление аргументов
Перегруженные функции выбираются для оптимального соответствия объявлений функций в текущей области аргументам, предоставленным в вызове функции. Если подходящая функция найдена, эта функция вызывается. Подходящее значение в этом контексте означает:
Точное соответствие найдено.
Тривиальное преобразование выполнено.
Восходящее приведение целого типа выполнено.
Стандартное преобразование в требуемый тип аргумента существует.
Пользовательское преобразование (оператор преобразования или конструктор) в требуемый тип аргумента существует.
Аргументы, представленные многоточием, найдены.
Компилятор создает набор функций-кандидатов для каждого аргумента. Функции-кандидаты — это функции, в которых фактический аргумент в данной позиции можно преобразовать в тип формального аргумента.
Для каждого аргумента создается набор наиболее подходящих функций, и выбранная функция представляет собой пересечение всех наборов. Если на пересечении находится несколько функций, перегрузка является неоднозначной и выдает ошибку. Функция, которая выбирается в конечном итоге, всегда является самой подходящей по сравнению с остальными функциями в группе по крайней мере для одного аргумента. Если нет ничего ясного, вызов функции приведет к ошибке.
Рассмотрим следующий оператор.
Представленный выше оператор создает два набора.
| Набор 1. Функции-кандидаты, имеющие первый аргумент дробного типа | Set 2: функции-кандидаты, второй аргумент которого можно преобразовать в тип int |
|---|---|
| Variant 1 | Вариант 1 ( int можно преобразовать в long использование стандартного преобразования) |
| Variant 3 |
Функции в наборе 2 — это функции, для которых существуют неявные преобразования фактического типа параметра в формальный тип параметра, а среди таких функций есть функция, для которой «стоимость» преобразования фактического типа параметра в формальный тип параметра является наименьшей.
Пересечением этих двух наборов является функция Variant 1. Ниже представлен пример неоднозначного вызова функции.
В предыдущем вызове функции создаются следующие наборы.
| Set 1: потенциальные функции, имеющие первый аргумент типа int | Set 2: потенциальные функции с вторым аргументом типа int |
|---|---|
| Вариант 2 ( int можно преобразовать в long использование стандартного преобразования) | Вариант 1 ( int можно преобразовать в long использование стандартного преобразования) |
Поскольку пересечение этих двух наборов пусто, компилятор выдает сообщение об ошибке.
Для сопоставления аргументов функция с n аргументами по умолчанию обрабатывается как n+ 1 отдельных функций, каждая из которых имеет разное число аргументов.
Многоточие (. ) выступает в качестве подстановочного знака; оно соответствует любому фактическому аргументу. Это может привести к созданию множества неоднозначных наборов, если вы не разрабатываете перегруженные наборы функций с крайней осторожностью.
Неоднозначность перегруженных функций не может быть определена до тех пор, пока не будет обнаружен вызов функции. На этом этапе наборы создаются для каждого аргумента в вызове функции, и можно определить, существует ли неоднозначная перегрузка. Это означает, что неоднозначности могут оставаться в коде до тех пор, пока они не будут вызваны конкретным вызовом функции.
Различия типов аргументов
По той же причине аргументы функции типа, измененные или, const volatile не обрабатываются иначе, чем базовый тип для перегрузки.
Однако механизм перегрузки функций может различать ссылки, уточняющие их на const volatile базовый тип и. Он делает код следующим:
Выходные данные
Указатели const на volatile объекты и также считаются отличными от указателей на базовый тип в целях перегрузки.
Сопоставление аргументов и преобразования
Когда компилятор пытается сопоставить фактические аргументы с аргументами в объявлениях функций и точное соответствие найти не удается, для получения правильного типа он может выполнять стандартные или пользовательские преобразования. Для преобразований действуют следующие правила:
последовательности преобразований, содержащие несколько пользовательских преобразований, не учитываются;
последовательности преобразований, которые могут быть сокращены путем удаления промежуточных преобразований, не учитываются.
Получающаяся последовательность преобразований (если таковые имеются), называется наилучшей последовательностью сопоставления. Существует несколько способов преобразования объекта типа int в тип unsigned long с помощью стандартных преобразований (см. описание в разделе int ).
Первая последовательность, хотя она достигает требуемой цели, не является наилучшей совпадающей последовательностью — существует более короткая последовательность.
В представленной ниже таблице показана группа преобразований, называемых тривиальными. Они оказывают ограниченное влияние на определение наилучшей последовательности сопоставления. В списке, приведенном после таблицы, рассматриваются экземпляры, в которых тривиальные преобразования влияют на выбор последовательности.
Тривиальные преобразования
| Тип, из которого выполняется преобразование | Тип, в который выполняется преобразование |
|---|---|
| имя типа | имя типа |
| имя типа | имя типа |
| Type-Name[] | имя типа |
| Type-Name(Argument-List) | ( Type-Name) (Argument-List) |
| имя типа | const const |
| имя типа | volatile volatile |
| имя типа | const const * |
| имя типа | volatile volatile * |
Ниже приведена последовательность, в которой делаются попытки выполнения преобразований.
Точное соответствие. Точное соответствие между типами, с которыми функция вызывается, и типами, объявленными в прототипе функции, всегда является наилучшим соответствием. Последовательности тривиальных преобразований классифицируются как точные соответствия. Однако последовательности, которые не делают ни одно из этих преобразований, рассматриваются лучше, чем последовательности, которые преобразуют:
От указателя к указателю на const ( type * to const type * ).
От указателя к указателю на volatile ( type * to volatile type * ).
Ссылка на ссылку на const ( type & to const type & ).
Ссылка на ссылку на volatile ( type & to volatile type & ).
Сопоставление с использованием стандартных преобразований. Любая последовательность, не классифицированная как точное соответствие или сопоставление с использованием повышений и содержащая только стандартные и тривиальные преобразования, классифицируется как сопоставление с использованием стандартных преобразований. В этой категории применяются следующие правила:
преобразование из указателя на производный класс в указатель на базовый класс создает тем более хорошее соответствие, чем ближе базовый класс к прямому базовому классу. Предположим, что иерархия классов имеет вид, показанный на следующем рисунке.

Graph отображения предпочтительных преобразований
Это же правило применяется для преобразований ссылок. Преобразование из типа D& в тип C& предпочтительнее преобразования из типа D& в тип B& и т. д.
Это же правило применяется для преобразований указателей на член. Преобразование из типа T D::* в тип T C::* предпочтительнее преобразования из типа T D::* в тип T B::* и т. д. ( T — тип члена.)
Предыдущее правило применяется только в определенном пути наследования. Рассмотрим граф, показанный на следующем рисунке.

Граф множественного наследования, демонстрирующий предпочтительные преобразования
Сопоставление с пользовательскими преобразованиями. Эта последовательность не может классифицироваться как точное совпадение, сопоставление с помощью рекламных акций или соответствие с помощью стандартных преобразований. Чтобы последовательность можно было классифицировать как сопоставление с пользовательскими преобразованиями, она должна содержать только пользовательские, стандартные или тривиальные преобразования. Сопоставление с пользовательскими преобразованиями лучше сопоставления с многоточием, но хуже сопоставления со стандартными преобразованиями.
Сопоставление с многоточием. Любая последовательность, соответствующая многоточию в объявлении, классифицируется как сопоставление с многоточием. Это считается самым слабым совпадением.
Пользовательские преобразования применяются при отсутствии встроенного повышения или преобразования. Эти преобразования выбираются на основе типа сопоставляемого аргумента. Рассмотрим следующий код.
В процессе сопоставления аргументов стандартные преобразования можно применять как к аргументу, так и к результату пользовательского преобразования. Поэтому следующий код работает.
Если какие-либо определенные пользователем преобразования должны соответствовать аргументу, стандартные преобразования не используются при вычислении наилучшего соответствия. Даже если для нескольких потенциальных функций требуется определенное пользователем преобразование, эти функции считаются равными. Пример:
Обеим версиям Func требуется определенное пользователем преобразование для преобразования типа int в аргумент типа класса. Возможные преобразования:
Преобразование из типа int в тип UDC1 (определяемое пользователем преобразование).
Преобразование из типа int в тип long ; затем преобразование в тип UDC2 (преобразование из двух шагов).
Несмотря на то, что второй требуется как стандартное преобразование, так и определяемое пользователем преобразование, два преобразования по-прежнему считаются равными.
Пользовательские преобразования считаются преобразованиями посредством создания или инициализации (функции преобразования). При рассмотрении наилучшего соответствия оба метода считаются одинаковыми.
Сопоставление аргументов и указатель this
Для этих нестатических функций-членов требуется, чтобы подразумеваемый this указатель совпадал с типом объекта, через который вызывается функция, или для перегруженных операторов требуется, чтобы первый аргумент соответствовал объекту, к которому применяется оператор. (Дополнительные сведения о перегруженных операторах см. в разделе перегруженные операторы.)
Квалификаторы ref для функций-членов
Ограничения на перегрузку
К допустимому набору перегруженных функций применяется несколько ограничений.
Любые две функции в наборе перегруженных функций должны иметь разные списки аргументов.
Перегрузка функций со списками аргументов одного типа лишь на основании возвращаемого типа недопустима.
Блок, относящийся только к системам Microsoft
Оператор New можно перегружать только на основе возвращаемого типа, а именно на основе указанного модификатора модели памяти.
Завершение блока, относящегося только к системам Майкрософт
Функции элементов не могут быть перегружены только на основе одного статического, а другого нестатического.
typedef Объявления не определяют новые типы; они представляют синонимы для существующих типов. Они не влияют на механизм перегрузки. Рассмотрим следующий код.
Перечисляемые типы являются отдельными типами и могут использоваться для различения перегруженных функций.
Типы «массив» и «указатель на» считаются идентичными в целях различения перегруженных функций, но только для одноэлементных измерений массивов. Вот почему эти перегруженные функции конфликтуют и создают сообщение об ошибке:
В случае многомерных массивов второе и все последующие измерения являются частью типа. Поэтому они используются для различения перегруженных функций.
Перегрузка, переопределение и скрытие
Любые два объявления функции с одинаковым именем в одной области видимости могут ссылаться на одну функцию или на две разные перегруженные функции. Если списки аргументов в объявлениях содержат аргументы эквивалентных типов (как описано в предыдущем разделе), эти объявления относятся к одной и той же функции. В противном случае они ссылаются на две различные функции, которые выбираются с использованием перегрузки.
Область класса строго наблюдалась; Поэтому функция, объявленная в базовом классе, находится не в той же области, что и функция, объявленная в производном классе. Если функция в производном классе объявлена с тем же именем, что и виртуальная функция в базовом классе, функция производного класса переопределяет функцию базового класса. Дополнительные сведения см. в разделе виртуальные функции.
Если функция базового класса не объявлена как Virtual, то говорят, что функция производного класса скрывает ее. Переопределение и скрытие отличаются от перегрузки.
Область видимости блока строго наблюдалась; Поэтому функция, объявленная в области файла, не находится в той же области, что и функция, объявленная локально. Если локально объявленная функция имеет то же имя, что и функция, объявленная в области файла, локально объявленная функция скрывает функцию области файла, не вызывая перегрузки. Пример:
В случае перегруженных функций-членов различным версиям функции могут предоставляться разные права доступа. Они по-прежнему считаются находящимися в области видимости включающего класса и, таким образом, являются перегруженными функциями. Рассмотрим следующий код, в котором функция-член Deposit перегружена; одна версия является открытой, вторая — закрытой.
Вызов Deposit в Account::Deposit вызывает закрытую функцию члена. Этот вызов является правильным, так как Account::Deposit является функцией-членом и имеет доступ к закрытым членам класса.






