Что такое вообще полиморфизм?

Если дать определение настолько простое, насколько это теоретически возможно, полиморфизм - это способность рассматривать объекты разных типов, как если бы они были одного типа. Есть несколько типов полиморфизма и разные методы достижения полиморфизма.

Академическое определение

По словам Бьярна Страуструпа, отца языка 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) (продвинутый)