Что такое статический и динамический полиморфизм

Слово полиморфизм означает наличие многих форм. В парадигме объектно-ориентированного программирования полиморфизм часто выражается как «один интерфейс, несколько функций».

Полиморфизм может быть статическим или динамическим. В статическом полиморфизме ответ на функцию определяется во время компиляции. В динамическом полиморфизме он решается во время выполнения.

Статический полиморфизм

Мы обсудим перегрузку оператора в следующей главе.

Перегрузка функции

Вы можете иметь несколько определений для одного и того же имени функции в той же области. Определение функции должно отличаться друг от друга по типам и / или количеству аргументов в списке аргументов. Вы не можете перегружать объявления функций, которые отличаются только возвращаемым типом.

В следующем примере показано использование функции print () для печати различных типов данных:

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

Динамический полиморфизм

C # позволяет создавать абстрактные классы, которые используются для обеспечения частичной реализации класса интерфейса. Реализация завершается, когда производный класс наследуется от него. Абстрактные классы содержат абстрактные методы, которые реализуются производным классом. Производные классы имеют более специализированную функциональность.

Вот правила об абстрактных классах:

Следующая программа демонстрирует абстрактный класс:

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

Когда у вас есть функция, определенная в классе, который вы хотите реализовать в унаследованном классе (-ах), вы используете виртуальные функции. Виртуальные функции могут быть реализованы по-разному в разных унаследованных классах, и вызов этих функций будет решаться во время выполнения. Динамический полиморфизм реализуется абстрактными классами и виртуальными функциями.

Следующая программа демонстрирует это:

Когда приведенный выше код компилируется и выполняется, он производит следующий результат:

Источник

Как полиморфизм реализован внутри JVM

Перевод данной статьи подготовлен специально для студентов курса «Разработчик Java».

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

В моей предыдущей статье Everything About Method Overloading vs Method Overriding (“Все о перегрузке и переопределении методов”) были рассмотрены правила и различия перегрузки и переопределения методов. В этой статье мы посмотрим, как обрабатывается перегрузка и переопределение методов внутри JVM.

Для примера возьмем классы из предыдущей статьи: родительский Mammal (млекопитающее) и дочерний Human (человек).

На вопрос о полиморфизме мы можем посмотреть с двух сторон: с “логической” и “физической”. Давайте сначала рассмотрим логическую сторону вопроса.

Логическая точка зрения

С логической точки зрения, на этапе компиляции вызываемый метод рассматривается как относящийся к типу ссылки. Но во время выполнения будет вызываться метод объекта, на который указывает ссылка.

Это все довольно просто, пока мы остаемся на концептуальном уровне. Но как же JVM обрабатывает это все внутри? Как JVM вычисляет, какой метод должен быть вызван?

Также мы знаем, что перегруженные методы (overload) не называются полиморфными и резолвятся во время компиляции. Хотя иногда перегрузку методов называют полиморфизмом времени компиляции или ранним/статическим связыванием.

Переопределенные методы (override) резолвятся во время выполнения, так как компилятор не знает, есть ли переопределенные методы в объекте, который присваивается ссылке.

Физическая точка зрения

Команда выше покажет две секции байткода.

1. Пул констант. Содержит почти все, что необходимо для выполнения программы. Например, ссылки на методы ( #Methodref ), классы ( #Class ), литералы строк ( #String ).

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

2. Байткод программы. Выполняемые инструкции байткода.

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

Почему перегрузка методов называется статическим связыванием

Таким образом, в случае перегрузки метода, компилятор способен идентифицировать инструкции байткода и адреса методов во время компиляции. Именно поэтому это называют статическим связыванием или полиморфизмом времени компиляции.

Почему переопределение методов называется динамическим связыванием

Для вызова методов anyMammal.speak() и humanMammal.speak() байткод одинаковый, так как с точки зрения компилятора оба метода вызываются для класса Mammal :

Итак, теперь возникает вопрос, если у обоих вызовов одинаковый байткод, как JVM узнает, какой метод вызвать?

Инструкция invokevirtual вызывает метод экземпляра через диспетчеризацию по (виртуальному) типу объекта. Это нормальная диспетчеризация методов в языке программирования Java.

JVM использует инструкцию invokevirtual для вызова в Java методов, эквивалентных виртуальным методам C++. В C++ для переопределения метода в другом классе, метод должен быть объявлен как виртуальный (virtual). Но в Java по умолчанию все методы виртуальные (кроме final и static методов), поэтому в дочернем классе мы можем переопределить любой метод.

Инструкция invokevirtual принимает указатель на метод, который нужно вызвать ( #4 — индекс в пуле констант).

Но ссылка #4 ссылается дальше на другой метод и Class.

Все эти ссылки используются совместно для получения ссылки на метод и класс, в котором находится нужный метод. Это также упоминается в спецификации JVM (прим. переводчика: ссылка на JVM spec 2.7):

Java Virtual Machine не требует какой-либо определённой внутренней структуры объектов.

В некоторых реализациях Java Virtual Machine, выполненных компанией Oracle, ссылка на экземпляр класса представляет собой ссылку на обработчик, который сам по себе состоит из пары ссылок: одна указывает на таблицу методов объекта и указатель на объект Class, представляющий тип объекта, а другая на область данных в куче, содержащую данные объекта.

Это означает, что каждая ссылочная переменная содержит два скрытых указателя:

Из приведенных выше рассуждений можно сделать вывод, что ссылка на объект косвенно содержит ссылку/указатель на таблицу, которая содержит все ссылки на методы этого объекта. Java позаимствовала эту концепцию из C ++. Эта таблица известна под различными именами, такими как таблица виртуальных методов (VMT), таблица виртуальных функций (vftable), виртуальная таблица (vtable), таблица диспетчеризации.

Мы не можем быть уверены в том, как vtable реализован в Java, потому что это зависит от конкретной JVM. Но мы можем ожидать, что стратегия будет примерно такая же, как и в C ++, где vtable — это структура, похожая на массив, которая содержит имена методов и их ссылки. Всякий раз, когда JVM пытается выполнить виртуальный метод, она запрашивает его адрес в vtable.

Для каждого класса существует только одна vtable, это означает, что таблица уникальна и одинакова для всех объектов класса, аналогично объекту Class. Объекты Class подробнее рассмотрены в статьях Why an outer Java class can’t be static и Why Java is Purely Object-Oriented Language Or Why Not.

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

Инструкция invokevirtual заставляет JVM обрабатывать значение в ссылке на метод # 4 не как адрес, а как имя метода, которое нужно искать в vtable для текущего объекта.
Я надеюсь, теперь стало более понятно то, как JVM использует пул констант и таблицу виртуальных методов для определения того, какой метод вызывать.
Код примера вы можете найти в репозитории Github.

Источник

В чем разница между динамическим и статическим полиморфизмом в Java?

может ли кто-нибудь привести простой пример, который объясняет разницу между динамический и Static полиморфизм в Java?

12 ответов

полиморфизм

1. Статическая привязка / привязка времени компиляции/ранняя привязка / перегрузка метода.(в том же классе)

2. Динамическая привязка/привязка во время выполнения/поздняя привязка / переопределение метода.(в разных классах)

перегрузка пример:

переопределение пример:

перегрузка метода будет примером статического полиморфизма

в то время как переопределение было бы примером динамического полиморфизма.

потому что, в случае перегрузки, во время компиляции компилятор знает, какой метод связать с вызовом. Однако он определяется во время выполнения для динамического полиморфизма

динамический (во время выполнения) полиморфизм полиморфизм существовал во время выполнения. Здесь компилятор Java не понимает, какой метод вызывается во время компиляции. Только JVM решает, какой метод вызывается во время выполнения. Перегрузка метода и переопределение метода с использованием методов экземпляра являются примерами динамического полиморфизма.

например,

рассмотрим приложение, которое сериализует и десериализует разных типы документов.

класс документа определит ‘Serialize ()’ и ‘ De-serialize() ’ методы как виртуальные, и каждый производный класс будет реализовывать эти методы по-своему основаны на фактическом содержании документов.

когда различным типам документов нужно быть сериализации/де-сериализации объектов документ будет передан ссылка на класс’ Document ‘ (или указатель) и когда ‘ На нем вызывается метод Serialize() ’ или ‘ De-serialize (), соответствующие версии виртуальных методов.

например,

объект employee может иметь два метода print (), один из которых не принимает Аргументы и один, принимающий строку префикса для отображения вместе с данные сотрудников.

учитывая эти интерфейсы, когда метод print() вызывается без каких-либо аргументы, компилятор, глядя на аргументы функции, знает, какая функция предназначен для вызова и генерирует объектный код соответственно.

Подробнее читайте в разделе «Что такое полиморфизм» (Google it).

полиморфизм: Полиморфизм-способность объекта принимать различные формы. Наиболее распространенное использование полиморфизма в ООП происходит, когда ссылка на родительский класс используется для ссылки на дочерний объект класса.

Динамическое Связывание / Полиморфизм Времени Выполнения:

полиморфизм времени выполнения, также известный как переопределение метода. В этом механизме, с помощью которого вызов переопределенной функции разрешен в время выполнения.

выход:

внутри метода запуска автомобиля

статический полиморфизм связывания / времени компиляции:

какой метод должен быть вызван, решается только во время компиляции.

выход: Внутри коллекции сортировка metho

способ перегрузки пример compile time / static polymorphism потому что привязка метода между вызовом метода и определением метода происходит во время компиляции, и это зависит от ссылки класса(ссылка создается во время компиляции и переходит в стек).

способ переопределения пример run time / dynamic polymorphism потому что привязка метода между вызовом метода и определением метода происходит во время выполнения, и это зависит от объекта класса (созданного объекта во время выполнения и идет в кучу).

простыми словами :

статический полиморфизм : то же имя метода перегружен С различным типом или количеством параметров в тот же класс (разные подписи). Целевые вызова метода во время компиляции.

динамический полиморфизм: тот же метод переопределяется С та же подпись в разные классы. Тип объекта, на который вызывается метод, не известен во время компиляции, но будет определен во время выполнения.

вообще перегрузка не будет рассматриваться как полиморфизм.

подклассы класса могут определять свое собственное уникальное поведение и все же делиться некоторыми из тех же функций родительского класса

привязка относится к связи между вызовом метода и определением метода.

на этом рисунке четко показано, что является обязательным.

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

на этом рисунке » a1.вызов methodOne () «привязывается к соответствующему определению methodOne () и» a1.вызов methodTwo () » привязывается к соответствующему определению methodTwo ().

для каждого вызова метода должно быть правильное определение метода. Это правило на java. Если компилятор не видит правильного определения метода для каждого вызова метода, он выдает ошибку.

теперь перейдем к статической привязке и динамической привязке в java.

Статическая Привязка В Java:

статическая привязка-это привязка, которая происходит во время компиляции. Это также называется ранняя привязка, потому что привязка происходит перед программой на самом деле работает

статическую привязку можно продемонстрировать как в под изображением.

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

на этом рисунке ‘a1 «является ссылочной переменной типа класса A, указывающей на объект класса A.» a2 » также является ссылочной переменной типа класса A, но указывающей на объект класса B.

во время компиляции при привязке компилятор не проверяет тип объекта, на который указывает конкретная ссылочная переменная. Он просто проверяет тип ссылочной переменной, через которую вызывается метод, и проверяет существует ли определение метода для этого типа.

Динамическая Привязка В Java:

динамическая привязка-это привязка, которая происходит во время выполнения. Также вызывается позднее связывание, потому что связывание происходит, когда программа на самом деле бегущий.

во время выполнения для привязки используются фактические объекты. Например, для «a1.вызов метода () «на приведенном выше рисунке будет вызван метод () фактического объекта, на который указывает «a1». Для «А2.метод () » вызов, метод() будет вызван фактический объект, на который указывает «a2». Этот тип привязки называется динамической привязкой.

динамическая привязка приведенного выше примера может быть продемонстрирована, как показано ниже.

Что такое статический и динамический полиморфизм. Смотреть фото Что такое статический и динамический полиморфизм. Смотреть картинку Что такое статический и динамический полиморфизм. Картинка про Что такое статический и динамический полиморфизм. Фото Что такое статический и динамический полиморфизм

Способ Перегрузки называется Статический Полиморфизм и Время Компиляции Полиморфизм или Статическое Связывание потому что перегруженные вызовы метода разрешаются во время компиляции компилятором на основе списка аргументов и ссылки, по которой мы вызываем метод.

и Способ Переопределения называется Динамический Полиморфизм или просто полиморфизм или Метод Выполнения Dispatch или Динамическое Связывание потому что переопределенный вызов метода разрешен во время выполнения.

чтобы понять, почему это так, давайте возьмем пример Mammal и Human класс

Я включил вывод, а также байт-код в ниже строк кода

и, посмотрев на код выше, мы можем увидеть, что байт-коды humanMammal.говори (), человек.говорят () и человеческие.говорят («хинди») являются совершенно разные, потому что компилятор может различать их на основе списка аргументов и ссылки на класс. И вот почему Способ Перегрузки называется Статический Полиморфизм.

но байт-код для anyMammal.speak () и humanMammal.speak () одинаков, потому что в соответствии с компилятором оба метода вызываются по ссылке Mammal, но выходные данные для обоих вызовов методов различны, потому что во время выполнения JVM знает, какой объект содержит ссылка и JVM вызывает метод на объекте, и именно поэтому переопределение метода известно как динамический полиморфизм.

таким образом, из приведенного выше кода и байт-кода ясно, что во время фазы компиляции метод вызова рассматривается из ссылочного типа. Но во время выполнения метод будет вызываться из объекта, который удерживает ссылка.

если вы хотите знать больше об этом вы можете прочитать больше о как JVM обрабатывает перегрузку и переопределение метода Внутренне.

полиморфизм времени компиляции (статическое связывание / раннее связывание): в статическом полиморфизме, если мы вызываем метод в нашем коде, то какое определение этого метода должно быть вызвано на самом деле, разрешается только во время компиляции.

во время компиляции Java знает, какой метод вызывать, проверяя сигнатуры метода. Итак, это называется полиморфизмом времени компиляции или статической привязкой.

Динамический Полиморфизм (Позднее Связывание / Время Выполнения Полиморфизм): во время выполнения Java ожидает выполнения, чтобы определить, на какой объект фактически указывает ссылка. Разрешение метода было принято во время выполнения, из-за чего мы называем полиморфизмом времени выполнения.

перегрузка метода-это полиморфизм времени компиляции, давайте возьмем пример, чтобы понять концепцию.

в этом примере у человека есть метод eat, который представляет, что он может есть пиццу или лапшу. Что метод eat перегружен, когда мы компилируем этого человека.java компилятор разрешает вызов метода » e.eat (noodles) [который находится в строке 6] с определением метода, указанным в строке 8, то есть это метод, который принимает лапшу в качестве параметра и весь процесс выполняется компилятором, поэтому это полиморфизм времени компиляции. Процесс замены вызова метода определением метода называется привязкой, в этом случае это делается компилятором, поэтому он называется ранней привязкой.

полиморфизм относится к способности объекта вести себя по-разному для одного и того же триггера.

статический полиморфизм (полиморфизм времени компиляции)

Динамический Полиморфизм (Полиморфизм Времени Выполнения)

Источник

Полиморфизм на Яве

Откройте для себя полиморфизм на Java.

Полиморфизм на Яве

1. Обзор

Все языки объектно-ориентированного программирования (OOP) должны проявлять четыре основные характеристики: абстракция, инкапсуляция, наследование и полиморфизм.

2. Статический полиморфизм

Например, наша ТекстФиле класс в приложении файлового менеджера может иметь три метода с одинаковой подписью читать () метод:

Во время компиляции кода компилятор проверяет, что все вызовы читать метод соответствует по крайней мере одному из трех методов, определенных выше.

3. Динамический полиморфизм

В гипотетическом приложении файлового менеджера давайте определим родительский класс для всех файлов, называемых ОбщиеФиле :

Мы также можем реализовать ImageFile класс, который расширяет ОбщиеФиле но переопределяет getFileInfo() метод и приложения более подробную информацию:

Вышеупомяпная конструкция аналогична методу переопределения. Мы можем подтвердить это, ссылаясь на getFileInfo() метод по:

Как и ожидалось, genericFile.getFileInfo () запускает getFileInfo() метод ImageFile класс, как поменьше на выходе ниже:

4. Другие полиморфные характеристики на Java

В дополнение к этим двум основным типам полиморфизма в Java, есть и другие характеристики в языке программирования Java, которые демонстрируют полиморфизм. Давайте обсудим некоторые из этих характеристик.

4.1. Принуждение

Полиморфное принуждение имеет дело с неявной конверсией типа, выполненной компилятором для предотвращения ошибок типа. Типичный пример виден в интегративной и струнной конкатеации:

4.2. Перегрузка оператора

Перегрузка оператора или метода относится к полиморфной характеристике одного и того же символа или оператора, имеющих различные значения (формы) в зависимости от контекста.

Например, символ плюс может быть использован как для математического добавления, так и для Струнные конкатенация. В любом случае интерпретацию символа определяет только контекст (т.е. типы аргументов).

4.3. Полиморфные параметры

Параметрический полиморфизм позволяет ассоциировать название параметра или метода в классе с различными типами. У нас есть типичный пример ниже, где мы определяем содержание в качестве Струнные а затем в качестве Интегер :

Важно также отметить, что объявление полиморфных параметров может привести к проблеме, известной как переменная скрытие где локавная декларация параметра всегда перекрывает глобальную декларацию другого параметра с тем же именем.

Для решения этой проблемы часто рекомендуется использовать глобальные ссылки, такие как этот ключевое слово, чтобы указать на глобальные переменные в локальном контексте.

4.4. Полиморфные подтипы

Полиморфный подтип удобно позволяет нам назначить несколько подтипов к типу и ожидать, что все вызовы на типе, чтобы вызвать доступные определения в подтипе.

Например, если у нас есть коллекция ОбщиеФиле s и мы ссылаемся на getInfo () метод на каждом из них, мы можем ожидать, что выход будет отличаться в зависимости от подтипа, из которого каждый элемент в коллекции был получен:

Чтобы решить проблему не в состоянии вызвать подтип-специфических методов при upcasting к супертипу, мы можем сделать вниз наследования от супертипа к подтипу. Это делается:

5. Проблемы с полиморфизмом

Давайте посмотрим на некоторые двусмысленности в полиморфизме, которые потенциально могут привести к ошибкам времени выполнения, если не должным образом проверены.

5.1. Идентификация типа во время downcasting

Напомним, что ранее мы потеряли доступ к некоторым подтип-специфическим методам после выполнения upcast. Хотя мы смогли решить это с downcast, это не гарантирует фактической проверки типа.

Например, если мы выполняем upcast и последующее вниз:

Следовательно, если мы попытаемся вызвать getHeight() метод на imageFile класса, мы получаем ClassCastException как ОбщиеФиле не определяет getHeight() метод:

Чтобы решить эту проблему, JVM выполняет проверку типа Run-Time (RTTI). Мы также можем попытаться точно идентифицировать тип с помощью instanceof ключевое слово так же, как это:

Вышесказанного помогает избежать ClassCastException исключение во время выполнения. Другой вариант, который может быть использован, это упаковка литых в попробуйте и поймать блокировать и ловить ClassCastException.

Следует отметить, что Проверка RTTI стоит дорого из-за времени и ресурсов, необходимых для эффективной проверки правильности типа. Кроме того, частое использование instanceof ключевое слово почти всегда подразумевает плохой дизайн.

5.2. Хрупкая проблема базового класса

Рассмотрим декларацию суперкласса под названием ОбщиеФиле и его подкласс ТекстФиле :

Когда мы изменяем ОбщиеФиле класс:

Мы отмечаем, что вышеупомяпная модификация ТекстФиле в бесконечной рекурсии в writeContent () метод, который в конечном итоге приводит к переполнению стека.

Для решения хрупкой проблемы базового класса мы можем использовать окончательный ключевое слово, чтобы предотвратить подклассы от переопределения writeContent () метод. Правильная документация также может помочь. И последнее, но не менее то, что состав, как правило, следует предпочтительным, чем наследство.

6. Заключение

В этой статье мы обсудили основополагающие концепции полиморфизма, сосредоточив внимание как на преимуществах, так и на недостатках.

Источник

CRTP. Static polymorphism. MixIn. Размышления на тему

Введение

Как известно, С++ является мультипарадигмовым языком. На нем можно писать в процедурном стиле, использовать языковые конструкции, обеспечивающие поддержку объектно-ориентированного программирования, шаблоны делают возможным обобщенное программирование, STL и новые возможности языка (lambda, std::function, std::bind) позволяют при желании писать в функциональном стиле в рантайме, а метапрограммирование шаблонов представляет собой функциональное программирование в чистом виде в compile time.
Несмотря на то, что в любой реальной большой программе скорее всего можно встретить смесь всех этих техник, объектно-ориентированная парадигма, реализуемая с помощью концепции классов, открытого интерфейса и закрытой реализации (инкапсуляция), наследования, и динамического полиморфизма, реализуемого посредством виртуальных функций, несомненно, является наиболее широко используемой.

Однако, динамический полиморфизм не бесплатен. Несмотря на то, что временные затраты на вызов виртуальной функции не слишком большие, при определенных обстоятельствах, например, цикл, обрабатывающий множество полиморфных объектов, оверхэд такого решения по сравнению с обычными функциями становится заметным.

Статический полиморфизм

В то время как динамический полиморфизм является полиморфизмом времени выполнения и явных интерфейсов, статический полиморфизм является полиморфизмом времени компиляции и неявных интерфейсов. Давайте разберемся что это значит.
Глядя на следующий код

мы можем сказать следующее: переданный в функцию process() указатель, должен указывать на объект, реализующий интерфейс (наследующий) base, и выбор реализаций функций prepare() и work() будет осуществлен во время выполнения программы в зависимости от того на объект какого именно производного от base типа указывает b.

Если же мы рассмотрим следущий код:

то мы можем сказать, что во-первых у объекта типа T должны быть функции prepare() и work(), а во-вторых реализации этих функций будут выбраны во время компиляции, основываясь на выведенном реальном типе T.
Как видите, при всей разности подходов, главная (с практической точки зрения) общая особенность обоих видов полиморфизма заключается в том, что в клиентском коде не нужно ничего менять при работе с объектами разных типов, при условии что они удовлетворяют описанным выше требованиям.

Раз все так замечательно, код, в принципе, не усложняется, рантайм оверхед нивелируется, почему бы тогда полностью не заменить динамический полиморфизм статическим? К сожалению, как обычно это и бывает, все не так просто. Существует ряд как субъективных так и объективных недостатков статического полиморфизма. К субъективным можно отнести, например, то, что явный интерфейс часто упрощает жизнь разработчикам, особенно в больших проектах. Иметь перед глазами заголовочный файл с классом — интерфейсом, который тебе нужно реализовать, гораздо удобнее, чем исследовать код шаблонных функций на предмет того какие функции тебе нужно реализовать и как, чтобы этот код работал. Представьте к тому же, что этот код написан давно и сейчас уже не у кого спросить что имелось в виду в том или ином кусочке.

Объективные же причины можно так или иначе свести к тому, что после инстанциирования шаблонные классы (функции) имеют разные, часто никак не связанные друг с другом типы.
Почему это плохо? Объекты таких типов без дополнительных ухищрений (см. boost::variant, boost::tuple, boost::any, boost::fusion etc.) невозможно положить в один контейнер и следовательно пакетно обработать. Невозможно, например, во время исполнения подменить объект — член класса, объектом другого типа, в рамках реализации “Стратегии” или “Состояния”. И хотя и эти паттерны можно реализовать и другими способами без классовых иерархий, например используя std::function или просто указатели на функции, ограничение, тем не менее, на лицо.

Но никто не заставляет нас строго придерживаться какой-то одной парадигмы. Самые мощные, гибкие и интересные решения возникают на стыке этих двух подходов, на стыке ООП парадигмы и generic парадигмы. Идиома CRTP как раз и является одним из примеров такого слияния парадигм.

CRTP (Curiously Recurring Template Pattern) — это идиома проектирования, заключающаяся в том, что класс наследует от базового шаблонного класса с самим собой в качестве параметра шаблона базового класса. Звучит запутано, но в коде выглядит довольно просто.

Что это может нам дать? Такая конструкция делает возможным обращение к производному классу из базового.

А возможность такой коммуникации, в свою очередь, открывает несколько интересных возможностей.

Явный интерфейс

В главе про статический полиморфизм я назвал отсутствие явных интерфейсов субъективным недостатком статического полиморфизма. На эту тему можно спорить, но так или иначе, явный интерфейс несложно определить, используя CRTP. Действительно, мы можем определить набор обязательных функций интерфейса через вызовы этих функций из базового класса.

С помощью такой конструкции один разработчик (архитектор) может задать интерфейс некого набора классов, и остальным программистам будет на что ориентироваться при реализации этого интерфейса. Но этим возможности CRTP не ограничиваются.

MixIn

MixIn — это прием проектирования, при котором класс (интерфейс, модуль и т.п.) реализует некоторую функциональность, которую можно “подмешать”, внести в другой класс. Самостоятельно же MixIn класс обычно не используется. Этот прием не является специфическим для С++, и в некоторых других языках он поддерживается на уровне языковых конструкций.
В С++ нет нативной поддержки MixIn’ов, но тем не менее эту идиому вполне можно реализовать с помощью CRTP.
Например, MixIn класс может реализовывать функциональность синглтона или подсчета ссылок на объект. А для того чтобы использовать такой класс достаточно отнаследовать от него с “собой” в качестве параметра шаблона.

Зачем здесь CRTP? Почему бы просто не наследовать от класса, реализующую некую нужную нам функциональность?

Дело в том, что внутри MixIn’а нам нужен доступ к функциям наследуемого класса (в случае синглтона к конструктору) и здесь на помощь приходит CRTP. И если пример с синглтоном кажется надуманным (действительно, кто сегодня использует синглтон?), то ниже вы найдете два более близких к реальности примера.

Enable_shared_from_this

MixIn структура (boost)std::enable_shared_from_this позволяет получить shared_ptr на объект, не создавая новую группу владения.

В этом случае каждый shared_ptr, полученный с помощью функции bad::get(), открывает новую группу владения объектом, и когда настанет время уничтожения shared_ptr’ов, delete для нашего объекта вызовется больше чем один раз.

Правильно же делать вот так:

Устроена эта вспомогательная структура примерно так:

Как видите, здесь CRTP позволяет базовому классу “увидеть” тип производного класса и вернуть указатель именно на него.

MixIn функции

MixIn функциональность не обязательно должна быть включена внутрь некоторого класса. Иногда возможно ее реализовать в виде свободной функции. В качестве примера реализуем оператор “!=” для всех классов, у которых определен оператор “==”.

MixIn наоборот

Представьте, что мы пишем реализацию классов игровых космических кораблей. Наши корабли будут двигаться по одинаковому алгоритму, за исключением некоторых моментов, например, механизм подсчета оставшегося горючего и текущей скорости будут различаться от корабля к кораблю. Классическая реализация паттерна шаблонный метод (а это именно он) будет выглядеть следующим образом:

Теперь попробуем применить CRTP.

В этой реализации мы избавились от виртуальных функций да и сам код стал короче (не нужно описывать чисто виртуальные функции в базовом классе).

Концепция MixIn’а при таком подходе переворачивается с ног на голову. Основная работа делается в базовом классе, а дополнительную (различающуюся) функциональность мы “подмешиваем” из производных классов.

Хочу акцентировать ваше внимание на этом приеме проектирования и Mixin’ах в целом. Пусть вас не смущает искусственный пример с космическими кораблями или синглтоном. В реальных задачах этот подход позволяет строить очень гибкие архитектуры, избегать повторяющегося кода, локализовывать функциональность в небольших классах и впоследствии “микшировать” их в нужную в данный момент смесь. Особенно он начинает блистать в кооперации со средствами, позволяющими пакетно обрабатывать множество объектов разных типов (см. boost::fusion).

MixIn вариации

Главная теорема разработки программного обеспечения (FTSE) гласит: “Любую проблему можно решить, вводя дополнительные уровни косвенности”. Посмотрим, как это можно применить к CRTP MixIn’ам.
Возможно, вы заметили в предыдущих главах “Явный интерфейс” и “MixIn наоборот” я использовал открытые функции в производном классе. Вообще говоря, это не очень хорошо, так как нарушает инкапсуляцию. Получается, что наружу у нас “торчат” функции, которые не предназначены для того, что пользователь вызывал их напрямую.
Можно решить эту проблему, сделав базовые классы друзьями производного. После этого можно вносить эти функции в private секцию, но представьте, что вам нужно отнаследовать от нескольких базовых MixIn’ов. Придется делать друзьями все базовые классы. Для комплексного решения этой проблемы, а также для того, чтобы обеспечить компиляцию на некоторых старых компиляторах, можно ввести новый уровень косвенности. Он представляет из себя структуру, функции которой перенаправляют вызовы из базы в производный класс.

Теперь из базовых классов, мы вызываем не функции производного, а функции промежуточной структуры.

В производном же классе нам достаточно внести в друзья только структуру access.

К дополнительным плюсам такого подхода можно отнести то, что базовые классы перестают что либо “знать” о своих производных классах, в частности какие конкретные функции из них нужно вызвать, а слабосвязанная система, как правило более гибка, чем сильносвязанная, а также то, что все вызовы к производному классу собраны в одном месте (в структуре access), позволяя тем самым легче визуально отделить их от функций производного класса, выполняющих другую работу.
Минусом же, как это часто бывает, является усложнение проектного решения. Поэтому я ни в коем случае не призываю использовать такую схему и в хвост и в гриву, но иметь о ней представление, мне кажется, не будет лишним.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *