Приведение вверх  —  это фундаментальная и важная для объектно-ориентированного программирования концепция C++. Чтобы освоить и эффективно применять эту концепцию, изучим ее нюансы, проиллюстрировав их практическими примерами и реальными сценариями.

Что такое «приведение вверх»?

Это процесс преобразования указателя или ссылки на производный класс в указатель или ссылку на базовый класс. Он назван приведением «вверх», потому что это процесс перемещения вверх по иерархии наследования  —  от конкретного типа к более общему.

Начнем с простого примера:

#include <iostream>

class Animal {
public:
virtual void makeSound() {
std::cout << "The animal makes a sound" << std::endl;
}
};

class Dog : public Animal {
public:
void makeSound() override {
std::cout << "The dog barks" << std::endl;
}
};

int main() {
Dog* dog = new Dog();
Animal* animal = dog; // Выполняется приведение

animal->makeSound(); // Выводится: "The dog barks"

delete dog;
return 0;
}

Здесь выполняется приведение собаки Dog* к животному Animal*. Хотя использован указатель Animal, для Dog вызывается метод makeSound()  —  из-за поведения виртуальной функции.

Неявное и явное приведения

На C++ возможно как неявное, так и явное приведения вверх:

Dog* dog = new Dog();

// Неявное приведение вверх
Animal* animal1 = dog;

// Явное приведение вверх
Animal* animal2 = static_cast<Animal*>(dog);

Неявное приведение вверх обычно предпочтительнее: оно чище и менее подвержено ошибкам. Явное приведение со static_cast применяется, когда нужно сделать приведение вверх заметнее или в сценариях с множественным наследованием.

Приведение вверх и полиморфизм

На C++ приведение вверх тесно связано с полиморфизмом. Поэтому здесь пишется код для работы с указателями или ссылками на базовый класс, который затем используется с объектами любого производного класса:

#include <iostream>
#include <vector>

class Shape {
public:
virtual double area() const = 0;
virtual void draw() const = 0;
virtual ~Shape() {}
};

class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
void draw() const override {
std::cout << "Drawing a circle" << std::endl;
}
};

class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
void draw() const override {
std::cout << "Drawing a rectangle" << std::endl;
}
};

void drawShapes(const std::vector<Shape*>& shapes) {
for (const auto& shape : shapes) {
shape->draw();
std::cout << "Area: " << shape->area() << std::endl;
}
}

int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle(5));
shapes.push_back(new Rectangle(4, 6));

drawShapes(shapes);

for (auto shape : shapes) {
delete shape;
}

return 0;
}

В этом примере приведение вверх используется для сохранения указателей на объекты Circle («Окружность») и Rectangle («Прямоугольник») в векторе указателей Shape («Фигура»). Функция drawShapes нужна для работы с указателями базового класса, благодаря чему ею обрабатываются любые типы Shape.

Приведение вверх и конструкторы/деструкторы

При приведении вверх важно понимать, как вызываются конструкторы и деструкторы:

#include <iostream>

class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
virtual ~Base() { std::cout << "Base destructor" << std::endl; }
};

class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor" << std::endl; }
~Derived() override { std::cout << "Derived destructor" << std::endl; }
};

int main() {
Base* ptr = new Derived();
delete ptr;
return 0;
}

Вывод:

Base constructor
Derived constructor
Base destructor

Обратите внимание: производный деструктор не вызывается при удалении через базовый указатель. Поэтому деструктор базового класса важно объявлять как виртуальный.

Приведение вверх и множественное наследование

С множественным наследованием приведение вверх сложнее:

#include <iostream>

class A {
public:
virtual void foo() { std::cout << "A::foo()" << std::endl; }
};

class B {
public:
virtual void bar() { std::cout << "B::bar()" << std::endl; }
};

class C : public A, public B {
public:
void foo() override { std::cout << "C::foo()" << std::endl; }
void bar() override { std::cout << "C::bar()" << std::endl; }
};

int main() {
C* c = new C();
A* a = c; // Приводится к «A»
B* b = c; // Приводится к «B»

a->foo(); // Вызывается «C::foo()»
b->bar(); // Вызывается «C::bar()»

delete c;
return 0;
}

В этом случае выполняется приведение C* либо к A*, либо к B*. В каждом случае вызывается корректная виртуальная функция.

Приведение вверх против Dynamic_Cast

Приведение вверх всегда безопасно, а вот приведение вниз  —  преобразование указателя базового класса в указатель производного класса  —  может быть опасным. Здесь и приходится кстати dynamic_cast:

#include <iostream>

class Base {
public:
virtual ~Base() {}
};

class Derived1 : public Base {
public:
void derived1Method() { std::cout << "Derived1 method" << std::endl; }
};

class Derived2 : public Base {
public:
void derived2Method() { std::cout << "Derived2 method" << std::endl; }
};

void processObject(Base* obj) {
Derived1* d1 = dynamic_cast<Derived1*>(obj);
if (d1) {
d1->derived1Method();
}

Derived2* d2 = dynamic_cast<Derived2*>(obj);
if (d2) {
d2->derived2Method();
}
}

int main() {
Base* b1 = new Derived1();
Base* b2 = new Derived2();

processObject(b1);
processObject(b2);

delete b1;
delete b2;
return 0;
}

Здесь dynamic_cast используется для безопасного приведения вниз и определения фактического типа объекта во время выполнения.

Реальный сценарий: игровой движок

Рассмотрим практический пример приведения вверх в простом игровом движке:

#include <iostream>
#include <vector>
#include <memory>

class GameObject {
public:
virtual void update() = 0;
virtual void render() = 0;
virtual ~GameObject() {}
};

class Player : public GameObject {
public:
void update() override {
std::cout << "Updating player position" << std::endl;
}
void render() override {
std::cout << "Rendering player sprite" << std::endl;
}
};

class Enemy : public GameObject {
public:
void update() override {
std::cout << "Updating enemy AI" << std::endl;
}
void render() override {
std::cout << "Rendering enemy sprite" << std::endl;
}
};

class Projectile : public GameObject {
public:
void update() override {
std::cout << "Updating projectile trajectory" << std::endl;
}
void render() override {
std::cout << "Rendering projectile" << std::endl;
}
};

class GameEngine {
std::vector<std::unique_ptr<GameObject>> gameObjects;

public:
void addObject(std::unique_ptr<GameObject> obj) {
gameObjects.push_back(std::move(obj));
}

void updateAll() {
for (auto& obj : gameObjects) {
obj->update();
}
}

void renderAll() {
for (auto& obj : gameObjects) {
obj->render();
}
}
};

int main() {
GameEngine engine;

engine.addObject(std::make_unique<Player>());
engine.addObject(std::make_unique<Enemy>());
engine.addObject(std::make_unique<Projectile>());

std::cout << "Updating game objects:" << std::endl;
engine.updateAll();

std::cout << "\nRendering game objects:" << std::endl;
engine.renderAll();

return 0;
}

Здесь приведение вверх используется для сохранения различных типов игровых объектов  —  игрок Player, враг Enemy, пуля Projectile  —  в одном контейнере указателей GameObject. Благодаря этому в GameEngine все объекты единообразны, методы их обновления и отрисовки вызываются без необходимости знать их конкретные типы.

Заключение

Приведение вверх  —  это функционал C++, которым обеспечиваются полиморфное поведение и переиспользование кода. Благодаря тому, что с объектами производного класса обращаются как с объектами базового класса, упрощается написание гибкого и расширяемого кода.

Главные выводы:

  1. Приведение вверх  —  это преобразование указателя/ссылки на производный класс в указатель/ссылку на базовый класс.
  2. На C++ это безопасно и обычно неявно.
  3. Приведение вверх важно для реализации полиморфизма.
  4. Виртуальными функциями обеспечивается корректное поведение при вызове методов через приведенные вверх указатели.
  5. При множественном наследовании объект приводится к любому из его базовых классов.
  6. В отличие от приведения вниз, приведению вверх не требуется проверка типов во время выполнения.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: C++ Upcasting: A Comprehensive Guide

Предыдущая статья15 общедоступных проектов, которые каждый разработчик должен добавить в закладки
Следующая статья4 причины, почему агенты ИИ не заменят программистов