Как сделать перегрузку метода c
BestProg
Перегрузка методов в классах. Примеры. Преимущество перегрузки методов. Перегрузка конструкторов
Содержание
Поиск на других ресурсах:
1. В чем состоит суть перегрузки метода в классе?
Язык программирования C# позволяет осуществлять перегрузку методов в классе. Перегрузка означает использование одного имени для разных методов. Фактически, перегружается имя метода.
Перегрузка метода – это программная реализация нескольких методов для которых выполняются следующие условия:
Перегрузка метода в классе – это объявления другого метода с таким самым именем в классе но с отличаемыми параметрами. Параметры перегруженного метода должны отличаться типами или количеством.
Если вызывается перегруженный метод, то компилятор выбирает тот вариант метода, параметры которого совпадают (по типу и по количеству) с аргументами, которые передаются в метод.
⇑
2. Что такое сигнатура метода? Какие требования к сигнатуре метода с точки зрения перегрузки?
Сигнатура метода – это имя метода и список его параметров. С точки зрения перегрузки к сигнатуре метода относится следующее требование:
⇑
3. Считаются ли перегруженными два метода, которые имеют одинаковые параметры но возвращают разные значения?
Нет. Это ошибка. Согласно синтаксису C# перегруженные методы обязательно должны отличаться параметрами.
⇑
4. Примеры перегруженных методов
Ниже приведено использование перегруженного метода Max() класса MaxMethods
Ниже продемонстрировано использование перегруженного метода Average()
Как видно из примера, компилятор автоматически определяет метод, который нужно вызвать, по типу его параметров.
⇑
и попытке компиляции, компилятор выдаст сообщение об ошибке
Это означает, что модификаторы ref и out не изменяют общую сигнатуру метода.
⇑
6. Какие преимущества дает перегрузка методов?
Перегрузка методов дает следующие преимущества:
⇑
7. Перегрузка конструкторов в классе. Примеры
Конструкторы классов могут перегружаться точно также как и методы.
Ниже продемонстрировано использование перегруженных конструкторов класса
Ниже продемонстрировано использование перегруженных конструкторов класса
⇑
⇑
9. Пример вызова перегруженного конструктора с использованием ключевого слова this
Класс содержит 2 перегруженных конструктора. В классе продемонстрирован вызов перегруженного конструктора.
Вызов перегруженного конструктора осуществляется с помощью кода
Перегрузка методов
Перегрузка методов – создание одноименных методов в пределах одного класса, которые отличаются количеством и/или типом параметров. Перегруженные методы могут возвращать значения разных типов данных, однако отличие только в возвращаемом типе не допускается.
Сигнатуры
Сигнатура метода – это часть объявления метода, которая позволяет компилятору идентифицировать метод среди других.
В сигнатуру входят:
Названия параметров и тип возвращаемого значения не относится к сигнатуре.
Опираясь на сигнатуру, компилятор выбирает метод, который нужно использовать.
Рассмотрим несколько методов:
Метод Div имеет следующую сигнатуру – Div(int, int), а метод Sum – Sum(uint, uint, uint).
Перегрузка
Исходя из понятия сигнатуры, перегруженными называют методы, которые отличаются сигнатурами, но при этом имеют одинаковые имена.
Как можно заметить, в каждом из рассмотренных примеров использована уникальная сигнатура.
C# также поддерживает сокращенную запись перегруженных методов:
При вызове метода с использованием литералов, можно указывать их тип с помощью суффиксов:
Перегруженные методы могут отличаться только модификаторами:
Для чего использовать перегрузку методов
Перегрузка используется для создания универсальных методов, логика поведения которых одинакова, но типы данных или количество аргументов разное. Это дает возможность писать красивый код, группируя методы с одинаковым поведением по имени.
Рассмотрим пример поиска минимального значения из двух целых чисел:
Используя перегрузку можно увеличить количество аргументов, для нахождения минимального из трех чисел:
Вызов перегруженных методов:
Ограничения при перегрузке методов
Локальные функции
C# не поддерживает перегрузку локальных функций, поэтому такой код не скомпилируется:
Отличие только по возвращаемому типу
Нельзя перегружать методы, если они отличаются только по типу возвращаемого значения.
Следующий код не скомпилируется:
Методы с опциональными параметрами
Рассмотрим метод с параметрами по умолчанию:
Может показаться, что к этому методу подходят сразу две сигнатуры: ShowSum(byte, byte, byte) и ShowSum(byte, byte), но это не так, подходит только первый вариант. Поэтому если перегрузить его методом с двумя параметрами:
он будет иметь больший приоритет, и аргумент по умолчанию не используется. Выйти из ситуации можно используя именованные параметры:
Хотя при разработке программ, таких конструкций лучше избегать, потому что они вносят неоднозначность.
Классы и объекты C#: перегрузка методов
О том, что такое методы C# и как их создавать мы уже поговорили, научились создавать методы классов и использовать их для доступа к свойствам. Однако, иногда бывает необходимо создать создать более одного метода с одним и тем же именем, но различным набором параметров. В практике программирования такой подход называется перегрузкой методов (method overloading).
С перегрузкой методов как таковой мы уже сталкивались, когда создавали различные конструкторы для нашего первого класса. В языке C# мы можем объявить и создать в классе несколько методов с одним и тем же именем, но различающейся сигнатурой.
Сигнатура метода C#
В C# сигнатура метода складывается из следующих элементов:
При этом при перегрузке метода его имя не меняется. Например,вернемся к нашему классу Building и создадим метод, который линейно увеличивает все три измерения здания (длину, ширину и высоту):
Перегрузка методов в C#
Чтобы перегрузить метод мы должны оставить его имя, но изменить хотя бы одну из составляющих его сигнатуры. Например, приведенный выше метод принимает в качестве параметров только целочисленные значения. Мы можем перегрузить метод, изменив тип данных входных параметров и получить два метода с одинаковым именем в одном классе:
Также, мы можем поменять количество параметров:
Теперь у нас в классе имеется три метода изменения размеров здания, которые имеют следующие сигнатуры:
Использование перегруженных методов
Перегруженные методы используются точно также, как и любые другие методы. При выборе того или иного метода Visual Studio подскажет есть ли у метода перегрузки и укажет их количество. Например, вот так будет выглядеть редактор кода Visual Studio при выборе нашего метода:
Чтобы воспользоваться одной из перегрузок метода, мы можем полностью написать имя метода и, поставив после названия метода круглую скобку ( выбрать необходимую перегрузку из списка:
Итого
Сегодня мы познакомились с такими понятиями как сигнатура метода C# и перегрузка методов. Перегрузка методов C# позволяет создавать в классе методы с одинаковым именем, но различающейся сигнатурой.
Урок №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+()) используйте перегрузку через обычные/дружественные функции.
Перегрузка в 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.