Что такое вообще полиморфизм?
Если дать определение настолько простое, насколько это теоретически возможно, полиморфизм - это способность рассматривать объекты разных типов, как если бы они были одного типа. Есть несколько типов полиморфизма и разные методы достижения полиморфизма.
Академическое определение
По словам Бьярна Страуструпа, отца языка C ++,
полиморфизм - предоставление единого интерфейса для сущностей разных типов. виртуальные функции обеспечивают динамический (во время выполнения) полиморфизм через интерфейс, предоставляемый базовым классом. Перегруженные функции и шаблоны обеспечивают статический (во время компиляции) полиморфизм.
Разбивая определение Страуструпа, полиморфизм можно разделить на две категории:
- Статический полиморфизм с перегруженными функциями и шаблонами, возникающий во время компиляции;
- Динамический полиморфизм с интерфейсами, возникающий во время выполнения.
Обратите внимание, что стандарт C ++ определяет полиморфные объекты как объекты, чьи
реализация генерирует информацию, связанную с каждым таким объектом, которая позволяет определить тип этого объекта во время выполнения программы.
а также краткое определение полиморфного класса:
Класс, который объявляет или наследует виртуальную функцию, называется полиморфным классом.
который, в основном, опускает статический (во время компиляции) полиморфизм. Однако термин «полиморфизм» используется для характеристики как времени компиляции, так и времени выполнения настолько часто, что трудно избежать описания того или другого.
Что такое статический полиморфизм?
Несмотря на то, что вы можете рассматривать разные типы как одно и то же, вы не можете полностью игнорировать типы - они все равно должны быть разрешены для запуска вашей программы. Такое определение происходит разными методами в разное время. Ключевое отличие статического полиморфизма заключается в том, что полиморфные - неизвестные - типы разрешаются компилятором на этапе компиляции. Это может быть трудно понять, но примеры облегчат понимание концепции.
Перегрузка
Перегруженный оператор
Вы когда-нибудь ценили возможности встроенного оператора +
? Он отлично работает с числовыми значениями и даже имеет особое поведение для нечисловых встроенных типов. Вы можете добавить int
, char
, bool
и даже все сразу, используя один и тот же знак +
. Оператор +
перегружен - он меняет свое поведение в зависимости от типов аргументов.
Как оказалось, существует несколько определений оператора +
: одно предназначено для int
, другое - для char
и т. Д., И компилятор выбирает подходящую версию определения при различных обстоятельствах. Может показаться, что вы используете один и тот же знак +
снова и снова, хотя компилятор определяет типы аргументов, решает, какое определение оператора +
подходит лучше всего, и помещает выбранную версию оператора в ваш код.
Оператор +
не уникален. Многие встроенные операторы перегружены для поддержки множества встроенных типов. Вы даже можете определить дополнительное значение для встроенных операторов, чтобы добавить поддержку вашего настраиваемого типа (класса). Это не влияет на операторы в целом - вы просто добавляете еще одно определение оператора в пул.
Важное примечание: определение статического полиморфизма Бьярна Страуструпа не включает перегруженные операторы. Однако такие операторы иногда рассматриваются как основная функция полиморфизма, и они полезны при объяснении перегруженных функций - истинной реализации полиморфного поведения.
Перегруженная функция
Перегруженные функции работают так же, как перегруженные операторы. Если несколько определений функции с одинаковым именем предоставлены в одной области (что означает, что они одинаково доступны), компилятор выбирает подходящее во время компиляции, исходя из типа и количества вводимых данных.
Теперь начинает возникать идея полиморфизма: вы используете «один и тот же интерфейс» (одно и то же имя функции) для работы с объектами разных типов.
При первом вызове custom_add(c, e)
компилятор проверяет тип входных параметров (int
, int
). Компилятор ищет функцию с сигнатурой custom_add( int , int )
и использует наиболее подходящую. После этого процесс повторяется для каждого вызова перегруженной функции.
Следует помнить: каждый раз, когда вы оставляете что-то для вывода компилятору, убедитесь, что не осталось места для двусмысленности.
Вызов функции custom_add(c, r)
с int c
и float r
неоднозначен и приведет к ошибке времени компиляции. Компилятор ищет функцию с сигнатурой custom_add( int , float )
и не находит ее. Обратите внимание: у нашей программы есть два потенциальных кандидата, custom_add( float , float )
и custom_add( int , int )
, причем обе функции в равной степени выполняют (или не выполняют) цель. В этом случае компилятор не может решить, какой из них использовать, и сообщает о проблеме.
Шаблоны
Шаблон функции
Идея перегрузки функций очень удобна, когда вам нужно делать несколько разные вещи с разными типами данных. Но что, если вам нужно сделать точно так же с аналогичными типами данных? Вы можете написать несколько почти идентичных функций для каждого типа данных, который хотите поддерживать, используя перегрузку функций. Однако C ++ предоставляет лучший способ подойти к этой задаче - здесь на помощь приходят шаблоны функций.
Шаблон функции можно рассматривать как образец для создания специализированных функций, подходящих для определенной цели. Ваша обязанность - создать алгоритм, пошаговую инструкцию, описывающую, какие работы необходимо выполнить. Компилятор отвечает за создание кода для различных типов ввода на основе предоставленных вами инструкций.
Краткое примечание: T
- очень распространенное имя для параметра шаблона, которое обычно используется вместо более значимых имен. Однако вы можете выбрать тот, который либо согласован в вашей рабочей среде, либо имеет для вас больше смысла.
Шаблонную функцию custom_add(T a, T b)
можно использовать с любым типом, который поддерживает все операции на всех описанных шагах. Поскольку алгоритм довольно простой, эту функцию можно использовать с любым встроенным типом и с любым настраиваемым типом данных (классом), который поддерживает операторы +
и <<
. Однако вызов custom_add(p, e)
с int p
и float e
вызовет ошибку времени компиляции. Чтобы понять почему, вам нужно пошагово взглянуть на процесс создания функции. Помните, что описанный процесс сильно упрощен.
Первый вызов custom_add<int>(p, i)
вызывает создание подходящей функции. Спецификатор типа <int>
определяет тип ввода. Компилятор берет заданный шаблон void custom_add (T a, T b)
, заменяет T
заданным типом int
, в результате получается custom_add( int , int )
, и процесс идет гладко.
При втором вызове custom_add(n, e)
компилятор сначала проверяет, выполнил ли он уже правильную функцию. То, что было раньше (custom_add( int , int )
), не совсем подходит для описания. Компилятор определяет типы ввода (float
, float
), заменяет T
на float
и успешно создает новую функцию.
Читая третий вызов функции custom_add( int , float )
, компилятор пытается заменить T
на int
, что приводит к custom_add ( int , int )
и на float
, что приводит к custom_add ( float , float )
, ни один из которых не соответствует исходному вызову. Компилятор сообщает о проблеме и прерывает процесс компиляции.
Спецификатор типа
Ввод спецификатора типа при использовании функции шаблона не требуется, но обычно считается хорошей практикой. Есть недостаток: добавление спецификатора типа custom_add<int>(p, e)
укажет компилятору обрабатывать оба ввода как int
значения (несмотря на то, что значение e
является float
) и скроет ошибку времени компиляции. В этом случае тип входного значения будет игнорироваться, что иногда может привести к неприятным ошибкам, которые трудно отладить. Шаблоны - это мощное средство, но при их использовании необходимо соблюдать осторожность.
Шаблон класса
Подумайте о ситуации, когда вам нужен базовый контейнер, в котором будут храниться два значения: контейнер для двух целых чисел, двух символов, двух чисел с плавающей запятой и т. Д. Вместо создания нескольких классов (каждый для каждого типа данных) вы можете легко применить предыдущий подход к созданию обобщенный код с использованием шаблона класса.
Вы все равно должны убедиться, что все инструкции, которые вы даете, применимы к типам данных, с которыми вы собираетесь их использовать. Вам не нужно сильно беспокоиться при работе со встроенными типами, но это становится большой проблемой, когда вы начинаете использовать шаблоны с настраиваемыми классами.
Кроме того, компилятор будет выполнять тот же процесс определения типа T
, что и в шаблоне функции, поэтому применяются те же ограничения разрешения.
Примечание. Несмотря на то, что класс Two_values
является контейнером общего назначения, который может хранить любые данные, метод custom_add()
ограничивает его использование. Этот метод можно применять к типам, поддерживающим операторы <<
и +
, что может не относиться к некоторым настраиваемым классам.
Специализированные шаблоны
Функция custom_add()
из предыдущего примера отлично работает для добавления двух числовых значений, но добавление двух символов и получение числового результата не имеет смысла в данном контексте. Есть хороший удобный способ, чтобы все работало как есть и с исключением для значений char
. Вы можете легко специализировать шаблон (функцию, класс или и то, и другое), чтобы в каждом конкретном случае он вел себя по-разному.
Теперь у вас есть обобщенный шаблон класса, который можно использовать для создания множества контейнеров для разных типов данных, и «особый случай» - создание экземпляра two_values
с двумя значениями char
вызовет уникальное поведение для метода custom_add()
.
Перегрузка против шаблонов
Итак, что лучше? На этот вопрос нет универсального ответа. Оба они обеспечивают полиморфное поведение в процессе компиляции, оба полезны и оба должны быть в вашем наборе инструментов. Как правило, используйте:
- Шаблоны, когда алгоритм одинаков для каждого случая (возможно, с несколькими исключительными случаями для некоторых типов данных);
- Перегрузка, когда алгоритм немного отличается в каждом случае;
- Ничего из вышеперечисленного, если алгоритм разный для каждого случая.
Бонусные темы для самостоятельного изучения:
- Принцип СУХОЙ (Начинающий)
- Вариативные шаблоны (выше среднего)
- Шаблонное метапрограммирование или выполнение во время компиляции (Upper-Intermediate)
- Шаблон любопытно повторяющегося шаблона (CRTP) (продвинутый)