— Бьярн Страуструп (Bjarne Stroustrup), [Stroustrup00] §13
Аналогично: место для вашего введения.
В этом разделе мы считаем наиболее значимой рекомендацию 64 — "Разумно сочетайте статический и динамический полиморфизм".
64. Разумно сочетайте статический и динамический полиморфизм
Резюме
Статический и динамический полиморфизм дополняют друг друга. Следует ясно представлять себе их преимущества и недостатки, чтобы использовать каждый из них там, где он дает наилучшие результаты, и сочетать их так, чтобы получить лучшее из обоих миров.
Обсуждение
Динамический полиморфизм предстает перед нами в форме классов с виртуальными функциями и объектов, работа с которыми осуществляется косвенно — через указатели или ссылки. Статический полиморфизм включает шаблоны классов и функций.
Полиморфизм означает, что данное значение может иметь несколько типов, а данная функция может принимать аргументы типов, отличающихся от точных типов ее параметров. "Полиморфизм представляет собой способ получить немного свободы динамической проверки типов, не теряя преимуществ статической проверки" — [Webber03].
Сила полиморфизма состоит в том, что один и тот же фрагмент кода может работать с разными типами, даже с теми, которые не были известны в момент написания этого кода. Такая "применимость задним числом" является краеугольным камнем полиморфизма, поскольку существенно увеличивает пригодность и возможность повторного использования кода (см. рекомендацию 37). (В противоположность этому мономорфный код работает только со строго конкретными типами, теми, для работы с которыми он изначально создавался.)
Динамический полиморфизм позволяет значению иметь несколько типов посредством открытого наследования. Например, Derived* p
можно рассматривать как указатель не только на Derived
, но и на объект любого типа Base
, который прямо или косвенно является базовым для Derived
(свойство категоризации). Динамический полиморфизм известен также как включающий полиморфизм, поскольку множество, моделируемое Base
, включает специализации, моделируемые Derived
.
Благодаря своим характеристикам динамический полиморфизм в С++ наилучшим образом подходит для решения следующих задач.
• Единообразная работа, основанная на отношении надмножество/подмножество. Работа с различными классами, удовлетворяющими отношению надмножество/подмножество (базовый/производный), может выполняться единообразно. Функция, работающая с объектом Employee
(Служащий), будет работать и с объектами Secretary
(Секретарь).
• Статическая проверка типов. В С++ все типы проверяются статически.
• Динамическое связывание и раздельная компиляция. Код, который использует иерархию классов, может компилироваться отдельно от этой иерархии. Это становится возможным благодаря косвенности, обеспечиваемой указателями (как на объекты, так и на функции).
• Бинарная согласованность. Модули могут компоноваться как статически, так и динамически, до тех пор, пока схемы виртуальных таблиц подчиняются одним и тем же правилам.
Статический полиморфизм посредством шаблонов также позволяет значению иметь несколько типов. Внутри шаблона
template void f(T t) { /* ... */ }
t
может иметь любой тип, который можно подставить в f
для получения компилируемого кода. Это называется "неявным интерфейсом" в противоположность явному интерфейсу базового класса. Таким образом достигается та же цель полиморфизма — написание кода, который работает с разными типами — но совершенно иным путем.
Статический полиморфизм наилучшим образом подходит для решения следующих задач.
• Единообразная работа, основанная на синтаксическом и семантическом интерфейсе. Работа с типами, которые подчиняются синтаксическому и семантическому интерфейсу, может выполняться единообразно. Интерфейсы в данном случае представляют синтаксическую сущность и не основаны на сигнатурах, так что допустима подстановка любого типа, который удовлетворяет данному синтаксису. Например, пусть дана инструкция int i = p->f(5);
. Если p
— указатель на класс Base
, эта инструкция вызывает определенную функцию интерфейса, вероятно, virtual int f(int)
. Но если p
имеет обобщенный тип, то этот вызов может быть связан со множеством различных вещей, включая, например, вызов перегруженного оператора operator->
, который возвращает тип, в котором определена функция X f(double)
, где X
— тип, который может быть преобразован в int.
Читать дальше