Понимание / требования к полиморфизму
Чтобы понять полиморфизм - как этот термин используется в вычислительной науке - он помогает начать с простого теста и определения его. Рассмотрим:
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 ++ полиморфный относится только к полиморфизму во время выполнения с использованием виртуальной диспетчеризации.Общий комп.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., чтобы обеспечить взаимопонимание для ваша аудитория считает ...
- , используя однозначную терминологию («можем ли мы сделать этот код многократно используемым для других типов?» Или «мы можем использовать виртуальную диспетчеризацию?» Вместо «можем ли мысделать этот код полиморфным? ") и / или
- , четко определяющим вашу терминологию.
Тем не менее, что важно для того, чтобы быть greв C ++ программист понимает , что на самом деле делает полиморфизм для вас ...
позволяя вам написать «алгоритмический» код один раз, а затем применить его ко многим типам данных
... и затем вы должны быть в курсе того, как различные полиморфные механизмы соответствуют вашим фактическим потребностям.
Подходит для полиморфизма времени выполнения:
- ввод, обработанный фабричными методами ивыделяется как гетерогенный набор объектов, обрабатываемый с помощью реализации
Base*
s, - , выбранной во время выполнения на основе файлов конфигурации, переключателей командной строки, настроек пользовательского интерфейса и т. д., реализация
- варьируется во время выполнения, напримеркак для шаблона конечного автомата.
Когда нет четкого драйвера для полиморфизма во время выполнения, параметры времени компиляции часто предпочтительнее.Обратите внимание:
- аспект, называемый компилируемым, для шаблонных классов предпочтительнее, чем толстые интерфейсы, терпящие неудачу во время выполнения
- SFINAE
- Оптимизации * CRTP
- (многие, включая встраивание и устранение мертвого кода, развертывание цикла, статические массивы на основе стека и куча)
__FILE__
, __LINE__
, конкатенацию строковых литералов иподдерживаются другие уникальные возможности макросов (которые остаются злыми; -)) - Шаблоны и макросы проверяют семантическое использование теста, но не ограничивают искусственно, как предоставляется эта поддержка (поскольку виртуальная диспетчеризация требует наличия точно соответствующего членапереопределение функций)
Другие механизмы, поддерживающие полиморфизм
Как и было обещано, для полноты изложены некоторые второстепенные темы:
- перегрузки, предоставляемые компилятором
- преобразования
- приведение / принуждение
Этот ответ завершается обсуждением того, как вышеперечисленное сочетается для расширения возможностей и симуляцииplify полиморфный код - особенно параметрический полиморфизм (шаблоны и макросы).
Механизмы отображения на специфичные для типа операции
> Неявные перегрузки, предоставляемые компилятором
Концептуально, компилятор перегружает множество операторов для встроенных типов.Концептуально он не отличается от указанной пользователем перегрузки, но указан в списке, так как его легко пропустить.Например, вы можете добавить к int
s и double
s, используя одну и ту же запись x += 2
, и компилятор выдаст:
- специфичные для типа инструкции процессора
- результаттого же типа.
Перегрузка затем плавно распространяется на определяемые пользователем типы:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Предоставляемые компилятором перегрузки для основных типов распространены в компьютерах высокого уровня (3GL +)языки и явное обсуждение полиморфизма обычно подразумевают нечто большее.(2GL - языки ассемблера - часто требуют, чтобы программист явно использовал разные мнемоники для разных типов.)
> Стандартные преобразования
Четвертый раздел стандарта C ++ описывает Стандартные преобразования.
Первый пункт хорошо суммирует (из старого черновика - надеюсь, все еще в значительной степени правильный):
-1- Стандартные преобразования - это неявные преобразования, определенные для встроенных типов.Пункт conv перечисляет полный набор таких преобразований.Стандартная последовательность преобразования представляет собой последовательность стандартных преобразований в следующем порядке:
Ноль или одно преобразование из следующего набора: преобразование lvalue-to-rvalue, преобразование array-to-преобразование указателя и преобразование функции в указатель.
Ноль или одно преобразование из следующего набора: интегральные преобразования, повышение с плавающей запятой, интегральные преобразования, преобразования с плавающей запятой, преобразования с плавающей запятой, преобразования указателя, преобразования указателя в член и логическое значениепреобразования.
Ноль или одна квалификация.
[Примечание: стандартная последовательность преобразований может быть пустой, т. Е. Она не может содержать преобразований.] Стандартная последовательность преобразования будет применена к выражению, если необходимо преобразовать его в требуемый тип назначения.
Эти преобразования разрешают код, такой как:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Применение более раннего теста:
Чтобы быть полиморфным, [a()
] должен бытьспособен работать со значениями как минимум двух различных типов (например, int
и double
), нахождения и выполнения кода, соответствующего типу .
a()
сам запускает код специально для double
и поэтому не полиморфный.
Но во втором вызове a()
компилятор знает, что нужно сгенерировать соответствующий типу код для«Повышение с плавающей запятой» (Стандарт §4) для преобразования 42
в 42.0
.Этот дополнительный код находится в , вызывающем функцию .Мы обсудим значение этого в заключении.
> Принуждение, приведение, неявные конструкторы
Эти механизмы позволяют определяемым пользователем классам определять поведение, подобное встроенномуТипы 'Стандартные преобразования.Давайте посмотрим:
int a, b;
if (std::cin >> a >> b)
f(a, b);
Здесь объект std::cin
оценивается в логическом контексте с помощью оператора преобразования.Это может быть концептуально сгруппировано с «интегральными продвижениями» и др. Из стандартных преобразований в теме выше.
Неявные конструкторы фактически делают то же самое, но управляются типом приведения:
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 ++, программисту предоставляется большая свобода контролировать границы, в которых используется полиморфизм.