Полиморфизм компиляции и времени выполнения
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy. Урок 56 из 104.
Полиморфизм означает «много форм» и является основополагающей концепцией ООП, которая позволяет обращаться с объектами единообразно, несмотря на их различное поведение. C++ поддерживает два различных типа полиморфизма, каждый из которых определяется на отдельном этапе выполнения программы.
Полиморфизм времени компиляции (также называемый статическим полиморфизмом) разрешается компилятором до запуска программы. Компилятор точно определяет, какую функцию вызвать, на основе сигнатуры функции. Сюда входят перегрузка функций и шаблоны:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // Компилятор выбирает print(int)
print(3.14); // Компилятор выбирает print(double)Полиморфизм времени выполнения (также называемый динамическим полиморфизмом) разрешается во время работы программы. Решение о том, какую функцию выполнить посредством call, зависит от фактического типа объекта, а не от типа указателя или ссылки. Это достигается с помощью виртуальных functions:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // Решается во время выполнения: "Drawing circle"Ключевой компромисс: полиморфизм времени компиляции не создаёт дополнительных затрат во время выполнения, поскольку решения принимаются во время компиляции, тогда как полиморфизм времени выполнения добавляет небольшие затраты (поиск в vtable), но обеспечивает большую гибкость при работе с объектами, типы которых неизвестны до момента выполнения.
Задание
ЛегкоДавайте создадим систему калькулятора, которая демонстрирует оба типа полиморфизма бок о бок. Вы создадите систему, в которой полиморфизм времени Compile обрабатывает разные типы inputs с помощью перегрузки functions, а полиморфизм времени выполнения позволяет динамически заменять различные стратегии calculation.
Вы организуете свой код в трёх файлах:
Calculator.h: define базовыйCalculatorclass, представляющий любую стратегию calculation:- Виртуальный
calculate(int a, int b)method, который возвращаетintи выводит:Base calculation: <a> ? <b>(возвращая 0) - Виртуальный destructor
- Виртуальный
Operations.h: define два производных calculator class, которые переопределяют поведение calculation:Adder: переопределитьcalculate(), чтобы вывестиAdding: <a> + <b>и вернуть суммуMultiplier: переопределитьcalculate(), чтобы вывестиMultiplying: <a> * <b>и вернуть произведение
override.main.cpp: Create систему, демонстрирующую оба типа полиморфизма. Считайте два целочисленных inputs (каждый в отдельной строке).Сначала продемонстрируйте полиморфизм времени Compile, создав три перегруженные
display()functions:display(int x)выводит:Integer value: <x>display(double x)выводит:Double value: <x>display(const std::string& x)выводит:String value: <x>
Затем продемонстрируйте полиморфизм времени выполнения, создав array указателей
Calculator*, содержащий базовыйCalculator,AdderиMultiplier. Выполните Loop по нему и вызовитеcalculate()для каждого элемента с вашими inputs, выводя результат после каждой calculation.Структурируйте вывод следующим образом:
=== Compile-Time Polymorphism === <display outputs for int, double, string> === Runtime Polymorphism === <calculate outputs with results>В разделе времени Compile вызовите
display()сначала с первым input как с целым числом, затем как с числом double (то же значение с добавленным .5), а затем со строкой "Result". После завершения Clean динамически allocated calculators.
Например, для inputs 10 и 3:
=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result
=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30Обратите внимание, как Compiler выбирает правильную перегрузку display() на основе типа аргумента (решение времени Compile), тогда как правильный calculate() method определяется фактическим типом объекта во время выполнения с помощью механизма vtable.
Попробуйте сами
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: Создайте три перегруженные функции display():
// 1. display(int x) - prints "Integer value: <x>"
// 2. display(double x) - выводит "Double value: <x>"
// 3. display(const std::string& x) - выводит "String value: <x>"
int main() {
// Прочитать два целочисленных ввода
int a, b;
std::cin >> a;
std::cin >> b;
// === Compile-Time Polymorphism ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: Вызовите display() с:
// - a как целое число
// - a как double (добавьте к нему 0.5)
// - строку "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Создайте массив указателей Calculator* с 3 элементами:
// - a base Calculator
// - Adder
// - Multiplier
// TODO: Пройдите по массиву, вызовите calculate(a, b) для каждого,
// и выведите "Result: <return_value>" после каждого вычисления
// TODO: Очистите динамически выделенную память
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнерПотренируйтесь самостоятельно: Онлайн-компилятор C++