Полиморфизм компиляции и времени выполнения
Часть раздела Объектно-ориентированное программирование путешествия по 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)Полиморфизм времени выполнения (также называемый динамическим полиморфизмом) разрешается во время выполнения программы. Решение о том, какую функцию вызывать, зависит от фактического типа объекта, а не от типа указателя или ссылки. Это достигается с помощью виртуальных функций:
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), но обеспечивает большую гибкость для работы с объектами, типы которых неизвестны до момента выполнения.
Задание
ЛегкоДавайте создадим систему калькулятора, которая демонстрирует оба типа полиморфизма бок о бок. Вы создадите систему, в которой полиморфизм времени компиляции обрабатывает различные типы входных данных через перегрузку функций, в то время как полиморфизм времени выполнения позволяет динамически менять различные стратегии вычислений.
Вы организуете свой код в трех файлах:
Calculator.h: Определите базовый классCalculator, который представляет любую стратегию вычислений:- Виртуальный метод
calculate(int a, int b), который возвращаетintи выводит:"Base calculation: " << a << " ? " << b(возвращая 0) - Виртуальный деструктор
- Виртуальный метод
Operations.h: Определите два производных класса калькулятора, которые переопределяют поведение вычислений:Adder: Переопределитеcalculate(), чтобы выводить"Adding: " << a << " + " << bи возвращать суммуMultiplier: Переопределитеcalculate(), чтобы выводить"Multiplying: " << a << " * " << bи возвращать произведение
override.main.cpp: Создайте систему, демонстрирующую оба типа полиморфизма. Считайте два целых числа (каждое на отдельной строке).Сначала продемонстрируйте полиморфизм времени компиляции, создав три перегруженные функции
display():display(int x)выводит:"Integer value: " << xdisplay(double x)выводит:"Double value: " << xdisplay(const std::string& x)выводит:"String value: " << x
Затем продемонстрируйте полиморфизм времени выполнения, создав массив указателей
Calculator*, содержащий базовыйCalculator,AdderиMultiplier. Пройдите в цикле и вызовитеcalculate()для каждого с вашими входными значениями, выводя результат после каждого вычисления.Структурируйте вывод следующим образом:
=== Compile-Time Polymorphism === <вывод display для int, double, string> === Runtime Polymorphism === <вывод calculate с результатами>Для раздела времени компиляции вызовите
display()с первым входным значением как целым числом, затем как числом с плавающей точкой (то же значение с добавлением .5), а затем как строкой "Result". По завершении очистите динамически выделенную память для калькуляторов.
Например, при входных данных 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Обратите внимание, как компилятор выбирает правильную перегрузку display() на основе типа аргумента (решение во время компиляции), в то время как правильный метод calculate() определяется фактическим типом объекта во время выполнения через механизм vtable.
Шпаргалка
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)Полиморфизм времени выполнения (динамический полиморфизм) разрешается во время выполнения с использованием виртуальных функций. Фактический тип объекта определяет, какая функция будет вызвана:
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), но обеспечивает большую гибкость.
Попробуйте сами
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: Создайте три перегруженные функции display():
// 1. display(int x) — выводит "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;
// === Полиморфизм времени компиляции ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: Вызовите display() с:
// — a как integer
// — a как double (добавьте к нему 0.5)
// — строкой "Result"
std::cout << std::endl;
// === Полиморфизм времени выполнения ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Создайте массив указателей Calculator* из 3 элементов:
// — базовый 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Итоги — Обобщенный контейнер