Понимание / требования к полиморфизму
Чтобы понять полиморфизм - как этот термин используется в компьютерных науках - нужно начать с простого теста и определения его. Рассматривать:
Type1 x;
Type2 y;
f(x);
f(y);
Здесь f()
нужно выполнить некоторую операцию и получить значения x
и в y
качестве входных данных.
Чтобы проявить полиморфизм, f()
необходимо уметь работать со значениями по крайней мере двух различных типов (например, int
и double
), находя и выполняя отдельный код, соответствующий типу.
C ++ механизмы полиморфизма
Явный полиморфизм, указанный программистом
Вы можете написать так f()
, чтобы он мог работать с несколькими типами любым из следующих способов:
Предварительная обработка:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
Перегрузки:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
Шаблоны:
template <typename T>
void f(T& x) { x += 2; }
Виртуальная рассылка:
struct Base { virtual Base& operator+=(int) = 0; };
struct X : Base
{
X(int n) : n_(n) { }
X& operator+=(int n) { n_ += n; return *this; }
int n_;
};
struct Y : Base
{
Y(double n) : n_(n) { }
Y& operator+=(int n) { n_ += n; return *this; }
double n_;
};
void f(Base& x) { x += 2; } // run-time polymorphic dispatch
Другие связанные механизмы
Предоставляемый компилятором полиморфизм для встроенных типов, стандартные преобразования и приведение / принуждение обсуждаются позже для полноты следующим образом:
- в любом случае их обычно понимают интуитивно (что требует реакции " ах, вот это "),
- они влияют на порог требования и беспроблемность использования вышеуказанных механизмов, и
- объяснение - это неудобное отвлечение от более важных концепций.
терминология
Дальнейшая категоризация
Учитывая описанные выше полиморфные механизмы, мы можем классифицировать их по-разному:
1 - Шаблоны очень гибкие. SFINAE (см. Также std::enable_if
) эффективно допускает несколько наборов ожиданий для параметрического полиморфизма. Например, вы можете закодировать, что, когда тип данных, которые вы обрабатываете, имеет .size()
член, вы будете использовать одну функцию, в противном случае другая функция, которая не нужна .size()
(но, предположительно, каким-то образом страдает - например, при использовании более медленной strlen()
или не печатаемой как полезное сообщение в журнале). Вы также можете указать специальное поведение, когда шаблон создается с определенными параметрами, оставляя некоторые параметры параметрическими ( частичная специализация шаблона ) или нет ( полная специализация ).
«Полиморфный»
Альф Штайнбах комментирует, что в стандарте C ++ полиморфизм относится только к полиморфизму времени выполнения с использованием виртуальной диспетчеризации. General Comp. Sci. значение более инклюзивное, согласно глоссарию создателя C ++ Бьярна Страуструпа ( http://www.stroustrup.com/glossary.html ):
полиморфизм - предоставление единого интерфейса для сущностей разных типов. Виртуальные функции обеспечивают динамический полиморфизм (во время выполнения) через интерфейс, предоставляемый базовым классом. Перегруженные функции и шаблоны обеспечивают статический (во время компиляции) полиморфизм. TC ++ PL 12.2.6, 13.6.1, D&E 2.9.
Этот ответ - как и вопрос - связывает функции C ++ с Comp. Sci. терминология.
обсуждение
В стандарте C ++ используется более узкое определение «полиморфизма», чем в Comp. Sci. сообщества, чтобы обеспечить взаимопонимание для вашей аудитории, рассмотрите ...
- используя однозначную терминологию («можем ли мы сделать этот код повторно используемым для других типов?» или «можем ли мы использовать виртуальную диспетчеризацию?» вместо «можем ли мы сделать этот код полиморфным?») и / или
- четко определяя вашу терминологию.
Тем не менее, для того, чтобы стать отличным программистом на C ++, важно понимать, что на самом деле делает для вас полиморфизм ...
позволяя вам написать "алгоритмический" код один раз, а затем применить его ко многим типам данных
... а затем хорошо понимать, как различные полиморфные механизмы соответствуют вашим реальным потребностям.
Подходит для полиморфизма во время выполнения:
- ввод обрабатывается фабричными методами и выводится как разнородная коллекция объектов, обрабатываемая через
Base*
s,
- реализация, выбранная во время выполнения на основе файлов конфигурации, переключателей командной строки, настроек пользовательского интерфейса и т. д.,
- реализация менялась во время выполнения, например, для шаблона конечного автомата.
Когда нет четкого драйвера для полиморфизма времени выполнения, часто предпочтительнее использовать параметры времени компиляции. Рассматривать:
- Компиляция так называемого аспекта шаблонных классов предпочтительнее толстых интерфейсов, которые не работают во время выполнения
- SFINAE
- CRTP
- оптимизации (многие из них включают встраивание и устранение мертвого кода, развертывание цикла, статические массивы на основе стека против кучи)
__FILE__
, __LINE__
, Строковый литерал конкатенации и другие уникальные возможности макросов (которые остаются злым ;-))
- семантическое использование тестов шаблонов и макросов поддерживается, но не ограничивает искусственно то, как эта поддержка предоставляется (поскольку виртуальная отправка имеет тенденцию требовать точного соответствия переопределений функций-членов)
Другие механизмы, поддерживающие полиморфизм
Как и было обещано, для полноты картины освещены несколько второстепенных тем:
- предоставляемые компилятором перегрузки
- преобразования
- слепки / принуждение
Этот ответ завершается обсуждением того, как вышеперечисленное объединяется для расширения возможностей и упрощения полиморфного кода, особенно параметрического полиморфизма (шаблоны и макросы).
Механизмы отображения на операции, зависящие от типа
> Неявные перегрузки, предоставляемые компилятором
По сути, компилятор перегружает множество операторов для встроенных типов. Она концептуально не отличается от перегрузки, заданной пользователем, но указана, поскольку ее легко упустить. Например, вы можете добавить к int
s и double
s, используя ту же нотацию, x += 2
и компилятор выдаст:
- инструкции ЦП, зависящие от типа
- результат того же типа.
Затем перегрузка легко распространяется на определяемые пользователем типы:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Предоставляемые компилятором перегрузки для базовых типов распространены в компьютерных языках высокого уровня (3GL +), и явное обсуждение полиморфизма обычно подразумевает нечто большее. (2GL - языки ассемблера - часто требуют, чтобы программист явно использовал разные мнемоники для разных типов.)
> Стандартные преобразования
Четвертый раздел стандарта C ++ описывает стандартные преобразования.
Первый пункт хорошо резюмируется (из старого черновика - надеюсь, все еще в основном правильного):
-1- Стандартные преобразования - это неявные преобразования, определенные для встроенных типов. Предложение conv перечисляет полный набор таких преобразований. Стандартная последовательность преобразования - это последовательность стандартных преобразований в следующем порядке:
Ноль или одно преобразование из следующего набора: преобразование lvalue-to-rvalue, преобразование массива в указатель и преобразование функции в указатель.
Ноль или одно преобразование из следующего набора: интегральные продвижения, продвижение с плавающей запятой, интегральные преобразования, преобразования с плавающей запятой, преобразования с плавающей запятой, преобразования указателя, преобразования указателя в член и преобразования логического типа.
Ноль или одно преобразование квалификации.
[Примечание: стандартная последовательность преобразования может быть пустой, т. Е. Не содержать преобразований. ] Стандартная последовательность преобразования будет применена к выражению, если необходимо преобразовать его в требуемый тип назначения.
Эти преобразования позволяют использовать такой код, как:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Применяя предыдущий тест:
Чтобы быть полиморфным, [ a()
] должен иметь возможность работать со значениями как минимум двух разных типов (например, int
и double
), находя и выполняя соответствующий типу код .
a()
сам запускает код специально для double
и поэтому не является полиморфным.
Но, во втором вызове a()
компилятор знает , для создания типа-соответствующий код для «раскрутки с плавающей точкой» (Standard § 4) , чтобы конвертировать 42
в 42.0
. Этот дополнительный код находится в вызывающей функции. Мы обсудим значение этого в заключении.
> Принуждение, приведение, неявные конструкторы
Эти механизмы позволяют определяемым пользователем классам определять поведение, подобное стандартным преобразованиям встроенных типов. Давайте посмотрим:
int a, b;
if (std::cin >> a >> b)
f(a, b);
Здесь объект std::cin
оценивается в логическом контексте с помощью оператора преобразования. Это может быть концептуально сгруппировано с «интегральными рекламными предложениями» и др. Из стандартных преобразований в теме выше.
Неявные конструкторы фактически делают то же самое, но управляются типом cast-to:
f(const std::string& x);
f("hello"); // invokes `std::string::string(const char*)`
Последствия предоставленных компилятором перегрузок, преобразований и принуждения
Рассматривать:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Если мы хотим, чтобы сумма x
обрабатывалась как действительное число во время деления (т.е. была 6,5, а не округлялась до 6), нам нужно всего лишь изменить ее на typedef double Amount
.
Это хорошо, но это не было бы слишком много работы , чтобы сделать код явно «введите правильный»:
void f() void f()
{ {
typedef int Amount; typedef double Amount;
Amount x = 13; Amount x = 13.0;
x /= 2; x /= 2.0;
std::cout << double(x) * 1.1; std::cout << x * 1.1;
} }
Но учтите, что мы можем преобразовать первую версию в template
:
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Это из - за эти маленькие «удобные функции» , что это может быть так легко экземплярами для любого int
или double
и работать , как задумано. Без этих функций нам потребовались бы явные приведения типов, черты типов и / или классы политик, некоторый подробный беспорядок, подверженный ошибкам, например:
template <typename Amount, typename Policy>
void f()
{
Amount x = Policy::thirteen;
x /= static_cast<Amount>(2);
std::cout << traits<Amount>::to_double(x) * 1.1;
}
Таким образом, предоставляемая компилятором перегрузка операторов для встроенных типов, стандартные преобразования, приведение / принуждение / неявные конструкторы - все они вносят тонкую поддержку полиморфизма. Из определения в верхней части этого ответа они обращаются к «поиску и выполнению кода соответствующего типа» путем сопоставления:
Они не устанавливают полиморфные контексты сами по себе, но помогают расширить возможности / упростить код внутри таких контекстов.
Вы можете почувствовать себя обманутым ... это не кажется таким уж большим. Значение состоит в том, что в параметрических полиморфных контекстах (т.е. внутри шаблонов или макросов) мы пытаемся поддерживать произвольно большой диапазон типов, но часто хотим выразить операции с ними в терминах других функций, литералов и операций, которые были разработаны для небольшой набор типов. Это снижает потребность в создании почти идентичных функций или данных для каждого типа, когда операция / значение логически одинаковы. Эти функции взаимодействуют, чтобы добавить позицию «максимальных усилий», делая то, что интуитивно ожидается, используя ограниченные доступные функции и данные и останавливаясь только с ошибкой, когда есть реальная двусмысленность.
Это помогает ограничить потребность в полиморфном коде, поддерживающем полиморфный код, сужая более жесткую сеть вокруг использования полиморфизма, чтобы локализованное использование не вызывало повсеместного использования, и делая преимущества полиморфизма доступными по мере необходимости без увеличения затрат на раскрытие реализации на во время компиляции, иметь несколько копий одной и той же логической функции в объектном коде для поддержки используемых типов и при выполнении виртуальной диспетчеризации в отличие от встраивания или, по крайней мере, разрешенных вызовов во время компиляции. Как обычно в C ++, программисту предоставляется большая свобода управления границами, в которых используется полиморфизм.