Розуміння / вимоги до поліморфізму
Щоб зрозуміти поліморфізм - як цей термін використовується в обчислювальній науці - це допомагає почати з простого тесту та його визначення. Поміркуйте:
Type1 x;
Type2 y;
f(x);
f(y);
Тут f()
потрібно виконати деяку операцію і їй надаються значення x
і y
як входи.
Щоб виявити поліморфізм, f()
необхідно вміти працювати зі значеннями щонайменше двох різних типів (наприклад, int
та double
), знаходячи та виконуючи окремий відповідний типу код.
С ++ механізми поліморфізму
Явний поліморфізм, визначений програмістом
Ви можете написати 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 ++ поліморфний посилається лише на поліморфізм під час запуску за допомогою віртуальної диспетчеризації. Загальний склад. Наук. сенс є більш вражаючим, відповідно до словника творця C ++ Bjarne Stroustrup ( http://www.stroustrup.com/glossary.html ):
поліморфізм - надання єдиного інтерфейсу для сутностей різних типів. Віртуальні функції забезпечують динамічний (під час виконання) поліморфізм через інтерфейс, що надається базовим класом. Перевантажені функції та шаблони забезпечують статичний (час компіляції) поліморфізм. TC ++ PL 12.2.6, 13.6.1, D&E 2.9.
Ця відповідь - як і питання - стосується функцій C ++ до Comp. Наук. термінологія.
Обговорення
За допомогою стандарту C ++, використовуючи більш вузьке визначення поняття "поліморфізм", ніж порівняння. Наук. спільноти, щоб забезпечити взаєморозуміння вашої аудиторії, врахуйте ...
- використовуючи однозначну термінологію ("чи можемо ми зробити цей код повторним використання для інших типів?" або "чи можемо ми використовувати віртуальну диспетчеризацію?", а не "чи можемо ми зробити цей код поліморфним?") та / або
- чітко визначаючи свою термінологію.
І все-таки, що важливо бути великим програмістом на C ++ - це зрозуміти, що насправді робить поліморфізм для вас ...
дозволяє один раз написати "алгоритмічний" код, а потім застосувати його до багатьох типів даних
... і тоді будьте в курсі того, як різні поліморфні механізми відповідають вашим реальним потребам.
Поліморфізм під час виконання:
- вхід, оброблений фабричними методами, і виплеснутий як неоднорідний збір об'єктів, що обробляється через
Base*
s,
- реалізація, вибрана під час виконання на основі файлів конфігурації, комутаторів командного рядка, налаштувань інтерфейсу тощо,
- реалізація варіюється під час виконання, наприклад, для моделі машинного стану.
Коли немає чіткого драйвера для поліморфізму під час виконання, часто є кращими варіанти компіляції. Поміркуйте:
- аспект компіляції, що називається, шаблонні класи є кращими для жирових інтерфейсів, які виходять з ладу під час виконання
- SFINAE
- CRTP
- оптимізації (багато з них: усунення вбудованого та мертвого коду, розгортання циклу, масиви на основі статичних стеків та купи)
__FILE__
, __LINE__
, Строковий літерал конкатенації і інші унікальні можливості макросів (які залишаються злим ;-))
- підтримується семантичне використання шаблонів і макросів тестового використання, але не обмежуйте штучно способом надання такої підтримки (як правило, віртуальна відправка вимагає точно збігати функції учасника)
Інші механізми, що підтримують поліморфізм
Як було обіцяно, для повноти висвітлюються декілька периферійних тем:
- надані компілятором перевантаження
- конверсії
- касти / примус
Ця відповідь завершується обговоренням того, як вищезгадане поєднується для розширення можливостей та спрощення поліморфного коду - особливо параметричного поліморфізму (шаблони та макроси).
Механізми відображення для операцій, характерних для типу
> Неявні перевантаження, передбачені компілятором
Концептуально компілятор перевантажує багато операторів для вбудованих типів. Він концептуально не відрізняється від визначеної користувачем перевантаження, але перерахований у списку, оскільки його легко не помітити. Наприклад, ви можете додавати в int
s і double
s, використовуючи однакові позначення, x += 2
і компілятор створює:
- інструкції щодо процесорного типу
- результат того ж типу.
Потім перевантаження безперешкодно поширюється на визначені користувачем типи:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Компілятор, що надається компілятором, для основних типів є звичайним для комп'ютерних мов високого рівня (3GL +), а явне обговорення поліморфізму загалом передбачає щось більше. (2GLs - мови монтажу - часто вимагають від програміста явно використовувати різні мнемоніки для різних типів.)
> Стандартні конверсії
Четвертий розділ стандарту C ++ описує стандартні перетворення.
Перший пункт добре підсумовує (зі старої чернетки - сподіваємось, все ще суттєво правильний):
-1- Стандартні перетворення - це неявні перетворення, визначені для вбудованих типів. Стаття conv перераховує повний набір таких перетворень. Стандартна послідовність перетворень - це послідовність стандартних перетворень у такому порядку:
Нульова або одна конверсія з наступного набору: перетворення значення-в-значення, перетворення масиву в вказівник та перетворення функції на вказівник.
Нульова або одна конверсія з наступного набору: інтегральні акції, просування з плаваючою комою, інтегральні перетворення, перетворення з плаваючою комою, плаваючо-інтегральні перетворення, конверсії вказівника, перетворення вказівника на члени та булеві перетворення.
Нульова або одна кваліфікаційна конверсія.
[Примітка: стандартна послідовність перетворень може бути порожньою, тобто вона може складатися з перетворень. ] Для вираження буде застосовано стандартну послідовність перетворення для перетворення його в потрібний тип призначення.
Ці конверсії дозволяють код, наприклад:
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
оцінюється в булевому контексті за допомогою оператора перетворення. Це можна концептуально згрупувати з "інтегральними рекламними пропозиціями" та ін. Зі стандартних перетворень у вищевказаній темі.
Неявні конструктори ефективно роблять те саме, але керуються типом "литий тип":
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 ++, програмісту надається велика свобода контролювати межі, в межах яких використовується поліморфізм.