зачем нужна перегрузка функций в c
Урок №102. Перегрузка функций
Обновл. 13 Сен 2021 |
На этом уроке мы рассмотрим перегрузку функций в языке C++, что это такое и как её эффективно использовать.
Перегрузка функций
Перегрузка функций — это возможность определять несколько функций с одним и тем же именем, но с разными параметрами. Например:
Здесь мы выполняем операцию вычитания с целыми числами. Однако, что, если нам нужно использовать числа типа с плавающей запятой? Эта функция совсем не подходит, так как любые параметры типа double будут конвертироваться в тип int, в результате чего будет теряться дробная часть значений.
Одним из способов решения этой проблемы является определение двух функций с разными именами и параметрами:
Но есть и лучшее решение — перегрузка функции. Мы можем просто объявить еще одну функцию subtract(), которая принимает параметры типа double:
Теперь у нас есть две версии функции subtract():
Следовательно, можно определить функцию subtract() и с большим количеством параметров:
Хотя здесь subtract() имеет 3 параметра вместо 2, это не является ошибкой, поскольку эти параметры отличаются от параметров других версий subtract().
Типы возврата в перегрузке функций
Обратите внимание, тип возврата функции НЕ учитывается при перегрузке функции. Предположим, что вы хотите написать функцию, которая возвращает рандомное число, но вам нужна одна версия, которая возвращает значение типа int, и вторая — которая возвращает значение типа double. У вас может возникнуть соблазн сделать следующее:
Компилятор выдаст ошибку. Эти две функции имеют одинаковые параметры (точнее, они отсутствуют), и, следовательно, второй вызов функции getRandomValue() будет рассматриваться как ошибочное переопределение первого вызова. Имена функций нужно будет изменить.
Псевдонимы типов в перегрузке функций
Поскольку объявление typedef (псевдонима типа) не создает новый тип данных, то следующие два объявления функции print() считаются идентичными:
Вызовы функций
Выполнение вызова перегруженной функции приводит к одному из трех возможных результатов:
Совпадение найдено. Вызов разрешен для соответствующей перегруженной функции.
Совпадение не найдено. Аргументы не соответствуют любой из перегруженных функций.
Найдены несколько совпадений. Аргументы соответствуют более чем одной перегруженной функции.
При компиляции перегруженной функции, C++ выполняет следующие шаги для определения того, какую версию функции следует вызывать:
Шаг №1: C++ пытается найти точное совпадение. Это тот случай, когда фактический аргумент точно соответствует типу параметра одной из перегруженных функций. Например:
Шаг №2: Если точного совпадения не найдено, то C++ пытается найти совпадение путем дальнейшего неявного преобразования типов. На уроке №55 мы говорили о том, как определенные типы данных могут автоматически конвертироваться в другие типы данных. Если вкратце, то:
char, unsigned char и short конвертируются в int;
unsigned short может конвертироваться в int или unsigned int (в зависимости от размера int);
float конвертируется в double;
Шаг №3: Если неявное преобразование невозможно, то C++ пытается найти соответствие посредством стандартного преобразования. В стандартном преобразовании:
Любой числовой тип будет соответствовать любому другому числовому типу, включая unsigned (например, int равно float).
enum соответствует формальному типу числового типа данных (например, enum равно float).
Ноль соответствует типу указателя и числовому типу (например, 0 как char * или 0 как float ).
Обратите внимание, все стандартные преобразования считаются равными. Ни одно из них не считается выше остальных по приоритету.
Шаг №4: C++ пытается найти соответствие путем пользовательского преобразования. Хотя мы еще не рассматривали классы, но они могут определять преобразования в другие типы данных, которые могут быть неявно применены к объектам этих классов. Например, мы можем создать класс W и в нем определить пользовательское преобразование в тип int:
То, как делать пользовательские преобразования в классах, мы рассмотрим на соответствующих уроках.
Несколько совпадений
Если каждая из перегруженных функций должна иметь уникальные параметры, то как могут быть возможны несколько совпадений? Поскольку все стандартные и пользовательские преобразования считаются равными, то, если вызов функции соответствует нескольким кандидатам посредством стандартного или пользовательского преобразования, результатом будет неоднозначное совпадение (т.е. несколько совпадений). Например:
В случае с print(‘b’) C++ не может найти точного совпадения. Он пытается преобразовать b в тип int, но версии print(int) тоже нет. Используя стандартное преобразование, C++ может преобразовать b как в unsigned int, так и во float. Поскольку все стандартные преобразования считаются равными, то получается два совпадения.
С print(0) всё аналогично. 0 — это int, а версии print(int) нет. Путем стандартного преобразования мы опять получаем два совпадения.
Неоднозначное совпадение считается ошибкой типа compile-time. Следовательно, оно должно быть устранено до того, как ваша программа скомпилируется. Есть два решения этой проблемы:
Решение №1: Просто определить новую перегруженную функцию, которая принимает параметры именно того типа данных, который вы используете в вызове функции. Тогда C++ сможет найти точное совпадение.
Заключение
Перегрузка функций может значительно снизить сложность программы, в то же время создавая небольшой дополнительный риск. Хотя этот урок несколько долгий и может показаться сложным, но, на самом деле, перегрузка функций обычно работает прозрачно и без каких-либо проблем. Все неоднозначные случаи компилятор будет отмечать, и их можно будет легко исправить.
Правило: Используйте перегрузку функций для упрощения ваших программ.
Поделиться в социальных сетях:
Перегрузка операторов в C++
Доброго времени суток!
Желание написать данную статью появилось после прочтения поста Перегрузка C++ операторов, потому что в нём не были раскрыты многие важные темы.
Самое главное, что необходимо помнить — перегрузка операторов, это всего лишь более удобный способ вызова функций, поэтому не стоит увлекаться перегрузкой операторов. Использовать её следует только тогда, когда это упростит написание кода. Но, не настолько, чтобы это затрудняло чтение. Ведь, как известно, код читается намного чаще, чем пишется. И не забывайте, что вам никогда не дадут перегрузить операторы в тандеме со встроенными типами, возможность перегрузки есть только для пользовательских типов/классов.
Синтаксис перегрузки
В данном случае, оператор оформлен как член класса, аргумент определяет значение, находящееся в правой части оператора. Вообще, существует два основных способа перегрузки операторов: глобальные функции, дружественные для класса, или подставляемые функции самого класса. Какой способ, для какого оператора лучше, рассмотрим в конце топика.
В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, то вы сами решаете как интерпретировать результат вычисления оператора).
Перегрузка унарных операторов
Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.
Бинарные операторы
Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.
Аргументы и возвращаемые значения
Оптимизация возвращаемого значения
При создании новых объектов и возвращении их из функции следует использовать запись как для вышеописанного примера оператора бинарного плюса.
Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:
Но на самом деле, в этом случае произойдет вызов конструктора в первой строке, далее вызов конструктора копирования, который скопирует объект, а далее, при раскрутке стека вызовется деструктор. При использовании первой записи компилятор изначально создаёт объект в памяти, в которую нужно его скопировать, таким образом экономится вызов конструктора копирования и деструктора.
Особые операторы
В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования []. Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.
Оператор запятая
В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер AxisPod в комментариях к предыдущей статье о перегрузке рассказал об одном.
Оператор разыменования указателя
Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.
Оператор присваивания
Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от «=». Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора «=». Пример:
Как можно заметить, в начале функции производится проверка на самоприсваивание. Вообще, в данном случае самоприсваивание безвредно, но ситуация не всегда такая простая. Например, если объект большой, можно потратить много времени на ненужное копирование, или при работе с указателями.
Неперегружаемые операторы
Некоторые операторы в C++ не перегружаются в принципе. По всей видимости, это сделано из соображений безопасности.
Перегрузка функций в C++
В этом руководстве мы узнаем о перегрузке функций в C++ на примерах.
В С++ две функции могут иметь одинаковое имя, если количество или тип переданных аргументов различаются.
Эти функции, имеющие одинаковое имя, но разные аргументы, известны, как перегруженные функции. Например:
Здесь все 4 функции являются перегруженными.
Обратите внимание, что возвращаемые типы всех этих 4 функций не совпадают. Перегруженные функции могут иметь или не иметь разные возвращаемые типы, но они должны иметь разные аргументы. Например:
Здесь обе функции имеют одинаковое имя, один и тот же тип и одинаковое количество аргументов. Следовательно, компилятор выдаст ошибку.
Перегрузка функций с использованием различных типов параметров
В этой программе мы перегружаем функцию absolute(). В зависимости от типа параметра, переданного во время вызова функции, вызывается соответствующая функция.
Перегрузка функций с использованием разного количества параметров
Здесь функция display() вызывается трижды с разными аргументами. В зависимости от количества и типа переданных аргументов вызывается соответствующая функция display().
Тип возвращаемого значения всех этих функций одинаков, но это не обязательно в случае перегрузки функции.
Примечание. В C++ многие стандартные библиотечные функции перегружены. Например, функция sqrt() может принимать в качестве параметров double, float, int и т.д., поскольку функция перегружена.
Перегрузка в C++. Часть II. Перегрузка операторов
Продолжаем серию «C++, копаем в глубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Эта статья посвящена перегрузке операторов. Особое внимание уделено использованию перегруженных операторов в стандартной библиотеке. Это вторая статья из серии, первая, посвященная перегрузке функций и шаблонов, находится здесь. Следующая статья будет посвящена перегрузке операторов управления памятью.
Оглавление
Введение
1. Общие вопросы перегрузки операторов
1.1. Перегружаемые операторы
1.2. Общие правила при выборе перегружаемого оператора
Необходимо учитывать приоритет и ассоциативность операторов, они при перегрузке не меняются и должны соответствовать ожиданиям пользователя. Характерный пример — это использование оператора для вывода данных в поток. К сожалению, приоритет этого оператора довольно высок, поэтому скобками приходится пользоваться чаще, чем хотелось бы. Например
1.3. Операторы, не рекомендуемые для перегрузки
1.4. Интерфейс и семантика перегруженных операторов
должны возвращать модифицированное значение и не изменять операнд. Если реализация оператора возвращает объект по значению, то его часто объявляют константным. Это предотвращает модификацию возвращаемого значения, что позволяет предотвратить ряд синтаксических странностей, которых нет при использовании встроенных операторов (подробнее см. [Sutter1]). Но если возвращаемый тип является перемещаемым, то его нельзя объявлять константным, так как это ломает всю семантику перемещения. Другие примеры будут рассмотрены далее.
1.5. Реализация перегрузки операторов
1.5.1. Два варианта реализации перегрузки операторов
Среди операторов, которые можно перегружать двумя способами, унарные операторы и присваивающие версии бинарных операторов обычно перегружают как функцию-член, а оставшиеся бинарные операторы как свободные функции.
1.5.2. Две формы использования перегруженных операторов
Использовать перегруженный оператор можно в двух формах (нотациях): инфиксной и функциональной. Инфиксная форма как раз и есть привычный синтаксис использования операторов.
Вот пример для класса из предыдущего раздела (будем считать, что код находится вне пространства имен N ):
Обратим внимание на то, что при использовании перегруженных операторов работает поиск, зависимый от типа аргумента (argument depended lookup, ADL), без него это использование, особенно в инфиксной форме, было бы весьма неудобно в случае, когда класс, для которого перегружается оператор, находится в другом пространстве имен. Вполне возможно, что ADL и появился в основном для решения этой проблемы.
1.5.3. Одновременное использование двух вариантов реализации перегрузки
Оператор, для которого возможна реализация в виде свободной функции, может быть перегружен одновременно как функция-член и как свободная функция. В этом случае при использовании инфиксной формы может возникнуть неоднозначность. Конечно, если такие перегрузки различаются параметрами, то компилятор сможет сделать выбор по типу аргументов. Но при одинаковых параметрах возникнет ошибка. Понятно, что подобной ситуации лучше избегать. Но если такое случилось, то помочь сможет только функциональная форма.
2. Дополнительные подробности реализации перегрузки операторов
2.1. Множественная перегрузка
Один и тот же оператор можно перегрузить несколько раз. Для унарных операторов может быть всего два варианта — с квалификатором const и без него (для функций-членов), или варианты с параметром типа константная ссылка или обычная ссылка (для свободных функций). Для бинарных операторов и оператора () количество перегрузок не ограничено.
Бинарные операторы и оператор () могут быть шаблонами, что по существу является множественной перегрузкой.
2.2. Особенности перегрузки операторов с использованием свободных функций
Рассмотрим несколько ситуаций, когда перегрузка операторов с использованием свободных функций предпочтительней или, вообще, безальтернативна.
2.2.1. Симметрия
2.2.2. Расширение интерфейса класса
Перегрузка бинарных операторов с использованием свободных функций позволяет расширять интерфейс класса без добавления новых функций-членов. (Напомним, что интерфейс класса включает не только функции-члены, но и свободные функции с параметрами тип которых определяется этим классом.) В качестве примера можно привести перегрузку операторов вставки и извлечения из потока. Если бы мы для перегрузки этих операторов использовали функции-члены, то нам бы пришлось для каждого нового типа, вставляемого в поток или извлекаемого из потока, добавлять в потоковые классы соответствующие функции-члены, что понятное дело невозможно. Подробнее про перегрузку операторов вставки и извлечения из потока см. раздел 3.8.
2.2.3. Неявные преобразования
2.2.4. Перечисления
Для перечислений операторы можно перегружать только как свободные функции, так как у перечислений просто не может быть функций-членов, пример см. в разделе 2.6.
2.3. Определение дружественной свободной функции внутри класса
Часто свободным функциям, реализующим оператор, целесообразно иметь доступ к закрытым членам класса и поэтому их объявляют дружественными. Напомним, что синтаксис дружественных функций позволяет разместить их определение непосредственно в теле класса.
Подробнее см. [Meyers1].
2.4. Вычислительные конструкторы
Если оператор возвращает объект по значению, иногда целесообразно определить специальный закрытый конструктор, называемый вычислительным конструктором (computational constructor). В этом случае компилятор сможет применить оптимизацию возвращаемого значения (return value optimization, RVO). Подробнее см. [Dewhurst].
2.5. Виртуальные операторы
2.6. Перегрузка операторов для перечислений
Операторы, перегружаемые как свободная функция, можно перегрузить для перечислений. Вот пример:
Теперь перебрать все элементы перечисления можно так:
Перегрузим еще один оператор
Теперь перебрать все элементы перечисления можно с помощью стандартного алгоритма:
И еще один вариант. Определим класс:
После этого перебрать все элементы перечисления можно с помощью диапазонного for :
3. Особенности перегрузки некоторых операторов
В этом разделе описываются особенности перегрузки некоторых операторов, особое внимание уделяется использованию этих перегрузок в стандартной библиотеке.
3.2. Унарный оператор *
В стандартной библиотеке оператор * перегружен для интеллектуальных указателей и итераторов.
3.3. Оператор []
Индексатор часто перегружают в двух вариантах — константном и неконстантном.
Первая версия позволяет модифицировать элемент, вторая только прочитать и она будет выбрана для константных экземпляров и в константных функциях-членах.
3.3.1. Многомерные массивы
3.4. Оператор ()
3.4.1. Локальные определения и лямбда-выражения
В C++ нельзя определить функцию локально (в блоке). Но можно определить локальный класс и этот класс может быть функциональным. Столь популярные в народе лямбда-выражения как раз и представляют из себя средство для быстрого и удобного определения анонимного локального функционального класса на «на лету».
3.4.2. Мультифункциональные типы и объекты
3.4.3. Хеш-функция
В Приложении Б приводится пример решения для C-строк на основе полной специализации стандартного шаблона.
3.4.4. Сравнение элементов и ключей в контейнерах
Если для использования некоторого типа в контейнере стандартной библиотеки требуется изменить или определить сравнение элементов этого типа, то существует три способа решить эту проблему.
В Приложении Б приводится пример решения для C-строк на основе полной специализации стандартного шаблона.
3.4.5. Удалители в интеллектуальных указателях
3.4.6. Алгоритмы
Алгоритмы стандартной библиотеки активно используют функциональные объекты и, соответственно, многие из них имеют параметр функционального типа. Часто алгоритмы имеют версию без такого параметра, в этом случае для реализации необходимых операций используется оператор (встроенный или перегруженный), определенный для элементов диапазона.
Если для использования некоторого типа в алгоритме стандартной библиотеки требуется изменить или определить необходимые операции для элементов этого типа, то существует два способа решить эту проблему.
Пример для алгоритма сортировки C-строк приведен в Приложение Б.
3.4.7. Функциональный шаблон
В C++11 появился универсальный функциональный шаблон. Он конкретизируется типом функции и перегружает оператор () в соответствии с сигнатурой функции. Экземпляры конкретизации можно инициализировать указателем на функцию, функциональным объектом или лямбда-выражением с соответствующей сигнатурой. Вот пример.
3.5. Операторы сравнения
3.6. Арифметические операторы
В бинарных операторах тип операндов может не совпадать. Например для строк один из операндов может быть C-строкой, для итераторов произвольного доступа второй операнд является сдвигом. Но в таком случае надо подумать о симметрии (см. раздел 2.2).
3.7. Инкремент, декремент
Эти операторы являются частью стандартного интерфейса итератора. Префиксные формы являются унарными операторами, постфиксные бинарными с фиктивным вторым параметром целого типа. Обе они обычно реализуются как функции-члены и постфиксный вариант определяется через префиксный. Вот типичная реализация инкремента.
Итераторы являются копируемыми типами без поддержки перемещения, поэтому постфиксный инкремент должен возвращать константный объект, это предотвращает модификацию возвращаемого значения, см. раздел 1.4.
В стандартной библиотеке инкремент перегружают все итераторы, а декремент двунаправленные итераторы и итераторы произвольного доступа.
3.8. Операторы >
Перегрузка этих операторов используется в стандартной библиотеке для вставки объектов в текстовой поток и извлечения объектов из текстового потока (поэтому в этом качестве их еще называют оператором вставки в поток и оператором извлечения из потока). Перегружаются они всегда как свободные функции, их сигнатура подчиняется правилам: первый операнд является ссылкой на поток, второй операнд является ссылкой на вставляемый или извлекаемый объект, возвращаемое значение является ссылкой на поток. Вот пример.
3.9. Оператор присваивания
Оператор присваивания можно реализовать только, как функцию-член, которая должна иметь ровно один параметр. Тип этого параметра произвольный, соответственно, перегрузок может быть несколько, для разных типов параметра. Перегрузка оператора присваивания является составной частью поддержки семантики копирования/перемещения и к ней приходится прибегать достаточно часто. Оператор присваивания практически всегда идет в паре с конструктором, имеющим один параметр. Нормальная ситуация — это когда каждому конструктору с одним параметром прилагается соответствующий оператор присваивания. Если описать семантику присваивания «на пальцах», то присваивание должно полностью освободить все текущие ресурсы, которыми владеет объект (левый операнд), и на его месте создать новый объект, определяемый правым операндом.
Среди операторов присваивания выделяются два стандартных — оператор копирующего присваивания и оператор перемещающего присваивания, которые соответствуют копирующему конструктору и перемещающему конструктору.
Компилятор может сгенерировать стандартные операторы присваивания и без такой подсказки. Если это не желательно, то можно явно запретить такую генерацию, объявив эти операторы удаленными.
И тогда операторы присваивания реализуются с помощью соответствующего конструктора и функции обмена состояниями следующим образом:
Аналогично можно определить оператор присваивания, соответствующий любому другому конструктору с одним параметром.
Главное достоинства этой идиомы состоит в обеспечении строгой гарантии безопасности исключений: если в конструкторе произошло исключение, то объект останется в том же состоянии, что и до начала операции (транзакционная семантика).
Если идиома «копирование и обмен» не используется, то необходима проверка на самоприсваивание.
Также, в случае наследования, надо вызвать соответствующий оператор базового класса. Еще одно достоинство идиомы «копирование и обмен» как раз и состоит в том, что она корректно работает при самоприсваивании, хотя, конечно, и не оптимально.
Ну и, наконец, рассмотрим довольно известную антиидиому для реализации присваивания.
X() уничтожает объект производного класса, что может полностью сломать взаимодействие базового класса и производного. Никогда так не делайте.
Оператор копирующего присваивания и оператор перемещающего присваивания (вместе с соответствующим конструктором) приходится перегружать практически всегда, когда нужна нестандартная семантика копирования/перемещения. (Запрет копирующего или перемещающего присваивания также можно рассматривать как перегрузку.) Также оператор присваивания обычно перегружается, как парный для конструктора с одним параметром. Практически все классы стандартной библиотеки перегружают операторы присваивания.
4. Итоги
Тщательно продумывайте перегрузку операторов. Она должна повысить наглядность и читаемость кода, но не наоборот.
При реализации перегрузки оператора учитывайте интерфейс и семантику встроенного оператора.
Приложения
Приложение А. Пример использования мультифункциональных объектов
BinOper — это функциональный тип, совместимой с сигнатурой
Ключевое отличие BinOper от аналогичного в std::accumulate() — это то, что BinOper должен поддерживать несколько сигнатур:
Приложение Б. Хэш-функция и сравнение для C-строк
Функция hash_combine() — это хорошо известная функция из библиотеки Boost. Она может быть использована при создании других пользовательских хеш-функций.
Ну и, наконец, пример сортировки C-строк в котором используется лямбда-выражение для определения нужного функционального объекта.
Список литературы
[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.
[Dewhurst]
Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.
[Meyers1]
Мэйерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.
[Sutter1]
Саттер, Герб. Решение сложных задач на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.