Язык c перегрузка функций
Урок №102. Перегрузка функций
Обновл. 24 Ноя 2020 |
На этом уроке мы рассмотрим перегрузку функций в языке C++, что это такое и как её эффективно использовать.
Перегрузка функций
Перегрузка функций — это возможность определять несколько функций с одним и тем же именем, но с разными параметрами. Например:
Здесь мы выполняем операцию вычитания с целыми числами. Однако, что, если нам нужно использовать числа типа с плавающей запятой? Эта функция совсем не подходит, так как любые параметры типа double будут конвертироваться в тип int, в результате чего будет теряться дробная часть значений.
Одним из способов решения этой проблемы является определение двух функций с разными именами и параметрами:
Но есть и лучшее решение — перегрузка функции. Мы можем просто объявить еще одну функцию subtract(), которая принимает параметры типа double:
Теперь у нас есть две версии функции subtract():
Следовательно, можно определить функцию subtract() и с большим количеством параметров:
Хотя здесь subtract() имеет 3 параметра вместо 2-х, это не является ошибкой, поскольку эти параметры отличаются от параметров других версий subtract().
Типы возврата в перегрузке функций
Обратите внимание, тип возврата функции НЕ учитывается при перегрузке функции. Предположим, что вы хотите написать функцию, которая возвращает рандомное число, но вам нужна одна версия, которая возвращает значение типа int, и вторая — которая возвращает значение типа double. У вас может возникнуть соблазн сделать следующее:
Компилятор выдаст ошибку. Эти две функции имеют одинаковые параметры (точнее, они отсутствуют), и, следовательно, второй вызов функции getRandomValue() будет рассматриваться как ошибочное переопределение первого вызова. Имена функций нужно будет изменить.
Псевдонимы типов в перегрузке функций
Поскольку объявление typedef (псевдонима типа) не создает новый тип данных, то следующие два объявления функции print() считаются идентичными:
Вызовы функций
Выполнение вызова перегруженной функции приводит к одному из 3-х возможных результатов:
Совпадение найдено. Вызов разрешен для соответствующей перегруженной функции.
Совпадение не найдено. Аргументы не соответствуют любой из перегруженных функций.
Найдены несколько совпадений. Аргументы соответствуют более чем одной перегруженной функции.
При компиляции перегруженной функции, 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++ сможет найти точное совпадение.
Заключение
Перегрузка функций может значительно снизить сложность программы, в то же время создавая небольшой дополнительный риск. Хотя этот урок несколько долгий и может показаться сложным, но, на самом деле, перегрузка функций обычно работает прозрачно и без каких-либо проблем. Все неоднозначные случаи компилятор будет отмечать, и их можно будет легко исправить.
Правило: Используйте перегрузку функций для упрощения ваших программ.
Поделиться в социальных сетях:
BestProg
Перегрузка функций. Перегрузка функций в классах. Перегрузка конструкторов класса. Доступ к перегруженной функции по указателю. Примеры
Содержание
Поиск на других ресурсах:
1. Какие функции называются «перегруженными»? Что означает термин «перегрузка»?
«Перегрузка» функции – это объявление функции с тем же именем несколько раз. Таким образом, в некоторой области видимости имя «перегруженной» функции объявляется несколько раз. Чтобы компилятор мог отличать «перегруженные» функции между собой, эти функции должны отличаться списком входных параметров.
В общем случае, объявление перегруженной функции в некоторой области видимости выглядит следующим образом:
2. По каким признакам отличаются перегруженные функции? Пример
Перегруженные функции отличаются по списку параметров. Списки параметров перегруженных функций должны отличаться по следующим признакам:
Например. Функция Max() есть перегруженная и отличается количеством параметров и типами параметров
3. Примеры перегрузки функций
Программный код, который демонстрирует использование функции Equal() следующий:
Программный код, демонстрирующий применение функции следующий:
4. Пример перегрузки функции в классе
Программный код, демонстрирующий применение «перегрузки» функций в классе имеет следующий вид:
5. Могут ли считаться перегруженными функции, которые имеют одинаковые имена, одинаковое количество и типы параметров, но которые возвращают значение разных типов?
Нет. Компилятор распознает перегруженные функции только по получаемым параметрам. Если две функции имеют одинаковые имена, одинаковое количество и типы параметров но возвращают разные значения, то такие функции считаются одинаковыми. В этом случае компилятор выдаст ошибку:
Например. В нижеследующем коде объявляются две функции с именем Inc5() :
Как видно из примера, функции возвращают значение разных типов. Поскольку функции имеют одинаковые имена, одинаковое количество и типы параметров, то вышеприведенный код выдаст ошибку компилятора
6. Для чего используется перегрузка конструкторов класса? Преимущества перегрузки конструкторов класса
Перегрузка конструкторов класса дает следующие преимущества:
7. Пример перегрузки конструкторов в классе
Пример. Задан класс Cylinder реализующий цилиндр. В классе перегружается конструктор, который может вызываться одним из трех способов:
8. Каким образом осуществляется доступ к перегруженной функции с помощью указателя на функцию? Пример
При объявлении указателя на «перегруженную» функцию, компилятор определяет нужную функцию для указателя по его сигнатуре при объявлении.
Нижеследующий код демонстрирует использование указателя на перегруженную функцию
Ключевое слово overload использовалось в ранних версиях C++ для указания того, что функция есть перегруженной. Общая форма объявления «перегруженной» функции с использованием ключевого слова overload имеет вид:
извещает компилятор о том, что функция Max() есть перегруженной.
Перегрузка в C++. Часть I. Перегрузка функций и шаблонов
Введение
В широком смысле перегрузка (overloading) — это возможность одновременно использовать несколько функций с одним именем. Компилятор различает их благодаря тому, что они имеют разный набор параметров. В точки вызова компилятор анализирует типы аргументов и определяет, какая конкретно функция должна быть вызвана. В русскоязычной литературе иногда можно встретить термин «совместное использование», но, похоже, он не прижился.
Перегрузка поддерживается многими языками программирования, мы будем рассматривать только C++17.
1. Общие положения
1.1. Перегруженные функции
Функции (а также шаблоны функций) называются перегруженными (overloaded), если они объявлены в одной области видимости (scope) и имеют одно и то же имя. Перегруженные функции не могут иметь разные типы возвращаемого значения, спецификатор исключений или спецификатор удаленной функции ( =delete ) при одинаковых параметрах.
Но несколько идентичных объявлений допустимы, компилятор просто игнорирует копии.
Также надо учитывать, что компилятор выполняет некоторые стандартные преобразования типов параметров функций. Для типа массива выполняется сведение (decay) к указателю, поэтому
не перегруженные функции, это одно и то же.
Параметры типа функция сводятся к указателю на функцию.
Для параметров, передаваемых по значению, удаляется квалификатор const (и volatile ), поэтому
не перегруженные функции, это одно и то же.
1.2. Общая схема алгоритма поиска функции
В общих чертах алгоритм поиска функции можно описать следующим образом. На первом этапе компилятор осуществляет поиск (lookup) тех перегруженных функций, которые по правилам языка допустимы для данного вызова (candidate functions). В случае шаблонов выполняется еще вывод аргументов шаблона (template argument deduction). У этих функций количество параметров должно совпадать с количеством аргументов и тип аргументов должен совпадать с типом параметров (или существовать неявное преобразование типа аргументов к типу параметров). Если таких функций не найдено, поиск завершается ошибкой. Если найдена ровно одна функция, то поиск завершается успешно. Если найдено несколько функций, то начинается следующий этап, компилятор пытается выбрать ту, которая подходит «лучше всего» для данных аргументов (match the arguments most closely). Этот этап называется разрешением перегрузки (overload resolution). Если такая функция найдена, то разрешение перегрузки завершается успешно, иначе возникает ошибка (ambiguous call to overloaded function). Рассмотрим пример:
Для вызова Foo(«meow») ни одной подходящей функции не найдено, для вызова Foo(42) подходят обе функции, компилятор не может выбрать наиболее подходящую, а вот вызовы Foo(3.14f) и Foo(3.14) разрешаются успешно.
Правила выбора наиболее подходящей функции (overload resolution rules) при попытке полного и формального описания могут оказаться весьма сложными и запутанными (это из тех вещей, которые до конца знают только разработчики компилятора), но как это часто бывает, во многих практически значимых случаях они являются интуитивно понятными и особых проблем у программиста не вызывают. Часть из них будет описана ниже.
Обычно термином «разрешение перегрузки» удобно описывать обе фазы: поиск функций-кандидатов и выбор наиболее подходящей функции. В дальнейшем мы будем придерживаться этого соглашения.
Но успешное разрешение перегрузки — это еще не все. После разрешения перегрузки производится проверка на доступность выбранной функции в точке вызова (то есть не является ли она private или protected ). В случае успеха производится проверка на удаленность (то есть не объявлена ли она как =delete ). Если эти проверки не проходят, компиляция завершается с ошибкой. Обратим внимание на то, что эти проверки никак не влияют на процедуру разрешения перегрузки, они всегда выполнятся после.
1.3. Текущая область видимости и разрешение перегрузки во вложенных областях видимости
Как уже отмечалось выше, перегруженные функции по определению находятся в одной области видимости. Области видимости вложены друг в друга. Области видимости, определяемые пространствами имен могут быть вложены друг в друга. Любое пространство имен вложено в глобальное пространство имен. Область видимости производного класса вложена в области видимости базовых классов, которые, в свою очередь, вложены в область видимости пространства имен. Локальные области видимости (блоки) вложены в другие блоки и далее в область видимости класса или пространства имен.
При разрешении перегрузки компилятор прежде всего должен выбрать область видимости, в которой и будет выполнятся разрешение перегрузки. Такая область видимости называется текущей. Если в текущей области видимости нет ни одной функции с искомым именем, текущей областью видимости становится объемлющая область видимости. Но, если в текущей области видимости найдена хотя бы одна функция с искомым именем, то выполняется разрешение перегрузки в данной области видимости и объемлющая область видимости рассматриваться не будет. Функции из текущей области видимости будут скрывать (hide) одноименные функции из объемлющих областей видимости. Подчеркнем, что это не зависит от результата разрешения перегрузки, подходящая функция может быть не найдена, оказаться неоднозначной, недоступной или удаленной, все равно продолжения поиска в объемлющей области видимости не будет.
1.3.1. Выбор текущей области видимости
Первоначальная текущая область видимости и возможные объемлющие области видимости, в которые может осуществляться переход для разрешения перегрузки, определяется контекстом в точке вызова функции. Приведем примеры.
В этом случае первоначальной текущей областью видимости будет глобальное пространство имен, объемлющих областей видимости нет.
Рассмотрим теперь «голые» вызовы функций без дополнительных квалификаторов класса или пространства имен.
Если такой вызов находится в пространстве имен, то это пространство имен и будет первоначальной текущей областью видимости, объемлющими областями видимости будут объемлющие пространства имен.
Если такой вызов находится в пространстве имен класса (например при инициализации статического члена), то соответствующий класс будет первоначальной текущей областью видимости, объемлющими областями видимости будут базовые классы и далее объемлющие пространства имен.
Пусть такой вызов находится в блоке
В этом случае первоначальной текущей областью видимости будет этот блок. (Напомним, что возможны локальные объявления функций, подробнее см. далее.) Объемлющими областями видимости будут объемлющие блоки, далее класс (если блок находится в функции-члене) и далее объемлющие пространства имен.
1.3.2. Разрешение перегрузки в классах
Эти правила могут оказаться достаточно неожиданными для программиста. Наследование в C++ спроектировано так, чтобы сделать границу между производным и базовым классом максимально прозрачной, а в данном случае такой прозрачности нет. При неблагоприятных условиях это может привести к трудно обнаруживаемым ошибкам. Например, можно получить бесконечную рекурсию. (Но это еще не худший вариант, такая ошибка сразу обнаружится при выполнении.)
1.3.3. Локальное объявление функций
Рассмотрим теперь одну редко используемую особенность C++, которая называется локальные объявления функций. Функции можно объявлять локально (в блоке), например:
Функции, объявленные локально, должны быть определены в глобальном пространстве имен, локальные определения в C++ не разрешены. Если функция вызывается в блоке без дополнительных квалификаторов класса или пространства имен, то текущей областью видимости, в которой происходит разрешение перегрузки, будет этот блок. Если в блоке есть локальные объявления функций, то одноименные функции из объемлющих областей видимости будут скрыты. Если в блоках нет локальных объявлений функций (что обычно и бывает), то текущая область видимости переместится в конце концов в класс (если блок находится в функции-члене класса) и далее в объемлющие пространства имен.
1.4. Расширение области видимости для разрешения перегрузки
Следует обратить внимание, на то, что расширение области видимости может вызвать конфликты, например, если в расширенную область видимости добавляются функции с таким же набором параметров, как и в текущей.
1.4.1. Использования using-объявления в классе
Вот как это делается для предыдущего примера:
1.4.2. Использования using-объявления локально и в пространстве имен
1.4.3. Использования using-директивы
1.4.4. Поиск, зависимый от типа аргументов
Есть одна ситуация, когда компилятор самостоятельно расширяет текущую область видимости для разрешения перегрузки. Рассмотрим объявление класса и функции в некотором пространстве имен:
Рассмотрим код (вне пространства имен N ):
2. Некоторые правила разрешения перегрузки
В данном разделе рассматриваются более специальные правила разрешения перегрузки, применяемые в особых случаях.
2.1. Неявные преобразования типа и параметры «близкого» типа
В C++ довольно много неявных преобразований типа. Это в определенных ситуациях может привести к проблемам, в том числе создавать неоднозначность при разрешении перегрузки. Но тем не менее при разрешении перегрузки типы, преобразующиеся в друг друга с помощью неявных преобразований, различаются. Общее правило такое: вариант, не требующий преобразований, имеет приоритет.
Но для таких перегруженных функций
неоднозначный вызов уже сделать легче, вот пример:
Семантически и побитово совпадающие типы, например, int и long также различаются при разрешении перегрузки.
Иногда при разрешении перегрузки желательно исключить некоторые неявные преобразования. В этом случае можно воспользоваться удаленными функциями. Предположим мы хотим иметь функции, которые можно вызывать для целочисленных аргументов, но нельзя вызывать для аргументов плавающего типа. Это можно сделать так:
2.2. Нулевой указатель
В C++98 приходилось писать
Подобные перегрузки используются в интерфейсе стандартных интеллектуальных указателей.
2.3. Универсальная инициализация и списки инициализации
Подробнее про универсальную инициализацию можно почитать у Скотта Мейерса [Meyers2].
2.4. Функции с переменным числом параметров
2.5. Шаблоны функций
Напомним, что шаблоны функций могут иметь полную специализацию для некоторого шаблонного аргумента, но не могут иметь частичных специализаций. Вместо частичной специализации используется перегруженный шаблон — одноименный шаблон функции c другими параметрами.
Шаблоны функций и их полные специализации могут участвовать в перегрузке вместе с нешаблонными функциями. Полные специализации шаблонов участвуют в перегрузке довольно специфическим образом (можно даже говорить, что они в перегрузке не участвуют), детали изложены ниже.
2.5.1. Общие правила перегрузки
При разрешении перегрузки сначала рассматриваются нешаблонные функции и конкретизации шаблонов. В первую очередь рассматриваются варианты точного совпадения типов аргументов и параметров, то есть варианты не требующие неявных преобразований типов аргументов. Если таких вариантов насколько, то приоритет имеют нешаблонные функции. Если нешаблонная функция не выбрана, то среди конкретизаций шаблонов приоритет будут иметь более специализированные шаблоны.
Если выбрана конкретизация шаблона, то проверяется, нет ли полной специализации этого шаблона для выведенного типа аргумента конкретизации. Если такая специализация есть, то выбирается она. Обратим внимание на то, что полные специализации рассматриваются в последнюю очередь, после выбора шаблона. Подробнее про описанный алгоритм разрешения перегрузки можно почитать у Герба Саттера [Sutter2].
В C++11 появились шаблоны с переменным количеством параметров или вариативные шаблоны (variadic templates). Если для некоторого вызова допустимыми являются конкретизации вариативного шаблона и обычного, то последний всегда будет считаться более специализированным и, соответственно, выбран при разрешении перегрузки.
2.5.2. Принцип SFINAE
Если у нас есть шаблон функции, то может возникнуть ситуация, когда для некоторого вызова компилятор не сможет вывести тип аргумента шаблона. Вот пример:
В этом случае, если в текущей области видимости есть перегруженные шаблоны, для которых аргументы выведены успешно, или перегруженные нешаблонные функции, то ошибки не возникает, такой шаблон просто «молча» исключается из разрешения перегрузки. Это и называется принципом SFINAE, который расшифровывается как Substitution Failure is not an Error (сбой при подстановке не является ошибкой).
2.5.3. Пример разрешения перегрузки
Рассмотрим пример перегруженных функций, шаблонов и полных специализаций шаблонов.
Посмотрим, как в соответствии с описанными выще правилами разрешается перегрузка для следующих вызовов:
2.5.4. Управление перегрузкой шаблонов
Рассмотрим перегруженные функции и шаблоны:
Теперь для целочисленных аргументов этот шаблон нельзя конкретизировать и в соответствии с принципом SFINAE он будет исключен при разрешении перегрузки и, таким образом, будет выбрана нешаблонная функция и выполнены необходимые неявные преобразования аргументов.
Ну и, наконец, варианты с использованием условных инструкций и операторов, вообще без использования перегрузки:
2.6. Правила разрешения перегрузки для параметров «родственного» типа
В данном разделе мы рассмотрим правила перегрузки в случаях когда параметры функций имеют «родственные» типы: сам тип, ссылка, ссылка на константу, rvalue ссылка.
Для описания этих правил необходимо использовать так называемые категории аргументов. Для нашего уровня детализации достаточно использовать четыре категории:
Обе константные категории часто можно рассматривать как единую категорию — константы.
Рассмотрим теперь, допустимые категории аргументов для рассматриваемых типов параметров.
Пусть параметр имеет тип ссылки:
В этом случае допустимой категорией аргументов будет только lvalue.
Пусть параметр имеет тип rvalue-ссылки:
В этом случае допустимой категорией аргументов будет только rvalue.
Пусть параметр имеет тип ссылки на константу или сам тип:
В этих случаях допустимы любые категории аргументов.
2.6.1. Передача параметров по ссылке, ссылке на константу и по значению
Пусть функции перегружены следующим образом:
В этом случае для lvalue будет выбрана первая функция (хотя вторая также допустима), для остальных категорий вторая.
Пусть теперь функции перегружены следующим образом:
Здесь для констант и rvalue будет выбрана вторая функция, а вот для lvalue выбор будет неоднозначный.
Пусть функции перегружены следующим образом:
Для любых аргументов выбор будет неоднозначный.
Hеконстантные функции-члены можно вызывать для rvalue объекта, то есть тем самым можно модифицировать rvalue. Но передавать в функцию rvalue аргумент через ссылку на неконстанту нельзя.
Возможность модифицировать rvalue объект может показаться несколько странной и даже бессмысленной. Но это не совсем так, иногда ее можно с пользой использовать. В данном примере демонстрируется известная идиома полной очистки объекта с помощью rvalue объекта и функции обмена состояниями. (Ну и не надо забывать, что вся семантика перемещения базируется на модификации rvalue объекта.) Но вообще модификация rvalue объекта может создать всякого рода проблемы. Для того, чтобы предотвратить это, у функций, которые возвращают объект по значению, тип возвращаемого значения объявляют константным. Подробнее об этом можно почитать у Герба Саттера [Sutter1].
2.6.2. Rvalue ссылки
Одно из самых значительных нововведений C++11 является семантика перемещения. Для ее реализации был введен специальный тип — rvalue-ссылка. Rvalue-ссылки это разновидность обычных C++ ссылок, отличие состоит в правилах инициализации и правилах разрешения перегрузок функций, имеющих параметры типа rvalue-ссылка. Программист должен четко знать описанные ниже правила, иначе результат перегрузки может оказаться неожиданным для программиста, компилятор «молча» заменит перемещение на копирование и все преимущества перемещения будут утеряны.
Пусть функции перегружены следующим образом:
В этом случае первая функция будет выбрана для rvalue аргументов (хотя вторая также допустима), а вторая для остальных категорий.
Пусть функции перегружены следующим образом:
В этом случае вторая функция будет выбрана для lvalue и константных аргументов, а вот для rvalue аргументов выбор будет неоднозначным, то есть первая функция не будет выбрана.
Пусть функции перегружены следующим образом:
В этом случае первая функция будет выбрана для rvalue аргументов, вторая для lvalue аргументов, а для константных аргументов разрешение перегрузки завершится неудачей.
Отметим, что четвертая категория — константные rvalue, — может стать актуальной при использовании функций, которые возвращают объект по значению, который объявлен константным. (Причины обсуждаются в предыдущем разделе.) В случае, если этот тип перемещаемый, то его нельзя объявлять константным, так как это ломает всю семантику перемещения.
2.6.3. Универсальные ссылки
3. Другие темы, связанные с перегрузкой
3.1. Параметры неполного типа
В C++ в ряде случаев компилятору достаточно знать, что то или иное имя является именем какого-то пользовательского типа (класса, структуры, объединения, перечисления), а полное объявление типа не нужно. В этом случае можно использовать неполное объявление (incomplete declaration), называемое еще упреждающим (forward declaration). Типы с неполным объявлением называются неполными. Механизм перегрузки работает и для неполных типов.
В данном случае полное объявление класса X может быть недоступно, но разрешение перегрузки работает.
3.2. Инициализация указателя на функцию
При инициализации указателя на функцию также можно использовать перегруженные функции.
Разрешение перегрузки работает даже проще, для успешной инициализации нужно точное совпадение параметров и возвращаемого значения. Правила относительно областей видимости (сокрытие, расширение) такие же, но вот ADL не работает.
Преобразования типа также способны выбирать из перегруженных функций.
Здесь также требуется точное совпадение параметров и возвращаемого значения и ADL не работает.
Перегруженные функции-члены также можно использовать при инициализации указателей на функции-члены класса.
3.3. Перегрузка и параметры по умолчанию
С точки зрения программиста использование перегруженных функций и функций с параметрами по умолчанию может быть очень похожим.
можно заменить на единственную функцию с параметром по умолчанию
В случае перегруженных функций этот код был бы корректным.
Перегрузку и параметры по умолчанию можно смешивать в определенных пределах, компиляторы справляются с этой проблемой, но вряд ли это можно отнести к хорошему стилю кодирования.
3.4. Перегрузка виртуальных функций
К перегрузке виртуальных функций надо относиться с осторожностью. Дело в том, что разрешение перегрузки выполняется на этапе компиляции, и, соответственно, используется статический тип переменной, для которой вызывается виртуальная функция. Это не очень хорошо согласуется с динамической природой виртуальных функций и может привести к неприятным неожиданностям («потерей» наследуемых функций базового класса, см. раздел 1.3). Подробнее ситуация описана в [Dewhurst]. Но относиться с осторожностью — это не значит не использовать совсем. Если мы проектируем полиморфную иерархию классов, в корне которой находится интерфейсный класс (абстрактный класс, у которого почти все функции-члены чисто виртуальные), все перегрузки сделаны в этом классе и доступ к производным классам осуществляется только через этот интерфейсный класс, то никаких неприятностей не будет. Подобная модель построения полиморфной иерархии классов используется весьма широко. В Приложении А мы покажем, как перегрузка виртуальных функций используется при реализации известного паттерна проектирования Visitor.
3.5. Метапрограммирование
Разрешение перегрузки происходит на этапе компиляции, поэтому не удивительно, что этот механизм активно используется в метапрограммировании — программировании кода, который выполняется на этапе компиляции. Метапрограммирование активно используется при написании шаблонов, в том числе и шаблонов стандартной библиотеки. Без его использования практически невозможно написать универсальные, гибкие и эффективные шаблоны.
В C++ с помощью шаблонов очень легко превратить целочисленную константу, известную на этапе компиляции, в тип. В стандартной библиотеке для этого есть специальный шаблон:
Ну а там, где появляются разные типы, можно использовать перегрузку.
Вот еще один прием, используемый в метапрограммировании. Рассмотрим выражение:
Это выражение вычисляется во время компиляции. При этом expr не вычисляется, определяется только его тип. После этого выполняется разрешение перегрузки, но сама Foo не вызывается, определяется только возвращаемый тип и поэтому определение Foo не нужно. Таким образом перегрузка используется для отображения типа на числовое значение.
4. Итоги
Перегрузка — это мощный инструмент, но пользоваться им надо продуманно и аккуратно. В перегрузке немало подводных камней, надо трезво оценить свои силы и не искать лишний раз приключений на свою голову.
Не стоит использовать перегрузку только потому, что компилятор это позволяет. Увлечение перегрузкой может снизить читаемость кода, сделать его безликим. Во многих случаях лучше дать название, отражающее специфику операции.
Старайтесь избегать использования перегруженных функций и шаблонов, требующих сложных и не до конца понятных алгоритмов разрешения перегрузки.
Не надо объявлять одноименные функции во вложенных областях видимости — это не перегрузка.
Приложения
Приложение А. Двойная диспетчеризация и паттерн Visitor
во всех производных классах переопределяется одинаково, тело состоит из одной инструкции:
Voila. Двойная диспетчеризация готова.
Приложение Б. Подмена стандартных функций пользовательскими версиями
Иногда возникает необходимость замены функций из стандартной библиотеки какими-то пользовательскими вариантами. Наиболее известный пример — это функция (точнее шаблон функции) обмена состояниями двух объектов.
1. Определить в классе функцию-член Swap() (имя не принципиально), реализующую обмен состояниями.
2. В том же пространстве имен, что и класс X (обычно в том же заголовочном файле, а иногда и в теле класса), определить свободную (не-член) функцию swap() следующим образом (имя и сигнатура принципиальны):
После этого, благодаря ADL, эта функция сможет участвовать в разрешении перегрузки вместе с std::swap() и в этом случае будет выбрана как имеющая лучшее соответствие.
3. Определить полную специализацию std::swap() для X
Спрашивается, а зачем вообще нужен третий шаг? Ответ такой — он подстраховывает от некоторых ошибок. Рассмотрим случай, когда в пользовательском пространстве имен реализуется некоторый шаблон, который использует функцию обмена состояниями.
Пусть теперь обмен состояниями делается так:
В этом варианте пользовательская swap() не будет рассматриваться и вот тут и придет на помощь полная специализация std::swap() — будет выбрана она. Но это тоже не вполне правильный вариант.
А совсем правильный вариант такой:
Но полная специализация поможет, если в стандартной библиотеке (то есть в пространстве имен std ) по ошибке используется
Скотт Мейерс [Meyers1] утверждает, что в стандартной библиотеке такую ошибку полностью исключить нельзя.
Рассмотрим теперь случай, когда функцию обмена состояниями надо определить для шаблона класса.
А вот специализацию std::swap() мы уже сделать не можем, для этого надо было бы добавить в пространство имен std шаблон функции, а это стандартом запрещено, так, что подстраховки уже не будет, ошибочный код может работать неправильно.
С помощью ключевого слова friend определение swap() можно перенести внутрь шаблона:
Определение становится более лаконичным, а функция-член Swap() при этом может быть закрытой или защищенной.
Список литературы
[GoF]
Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования.: Пер. с англ. — СПб.: Питер, 2001.
[Dewhurst]
Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.
[Meyers1]
Мэйерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.
[Meyers2]
Мейерс, Скотт. Эффективный и современный C++: 42 рекомендации по использованию C++11 и C ++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2016.
[Sutter1]
Саттер, Герб. Решение сложных задач на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.
[Sutter2]
Саттер, Герб. Новые сложные задачи на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.