Операторы static_cast и dynamic_cast

Практический курс по ООП C++: https://stepik.org/a/205781

Смотреть материал на YouTube | RuTube

Продолжаем тему операторов преобразования типов языка C++ и рассмотрим следующий оператор static_cast. Это некий аналог предыдущего оператора reinterpret_cast, но с более строгими ограничениями. Лучше всего отличия и возможности static_cast показать на конкретных примерах.

Пусть в программе объявлены переменные разных типов и указатели на них:

int main()
{
    char ch {0};
    short sh {0};
    int i {0};
    double d {0};
 
    char* ptr_ch {&ch};
    short* ptr_sh {&sh};
    int* ptr_i {&i};
    double* ptr_d {&d};
 
    ch = static_cast<char>(sh);
    ch = reinterpret_cast<char>(sh); // ошибка
 
    d = static_cast<double>(i);
    d = reinterpret_cast<double>(i); // ошибка
 
    sh = static_cast<short>(ptr_i);  // ошибка 
    sh = reinterpret_cast<short>(ptr_i);
 
    ptr_d = static_cast<double *>(ptr_ch); // ошибка 
    ptr_d = reinterpret_cast<double *>(ptr_ch);
 
    return 0;
}

Как видим, оператор static_cast может совершенно спокойно выполнять преобразования типов между переменными, в отличие от оператора reinterpret_cast. А вот с указателями ситуация несколько иная. Во-первых, мы не можем с помощью static_cast приводить указатели к обычной переменной (и наоборот), и, во-вторых, приводить разные несвязанные между собой типы указателей. В отличие от оператора reinterpret_cast, который может выполнять подобные действия.

Что значит не связанные между собой типы указателей? Смотрите, если мы в программе объявим две независимые структуры:

struct point2D {
    int x, y;
};
 
struct point3D {
    int x, y, z;
};

То с точки зрения оператора static_cast эти типы независимы между собой. И, соответственно, приведение одного к другому будет невозможно:

int main()
{
    point2D* ptr_2d = new point2D {1, 2};
 
    point3D* ptr_3d = static_cast<point3D *>(ptr_2d); // ошибка
    point3D* ptr_3d_2 = reinterpret_cast<point3D *>(ptr_2d);
 
    delete ptr_2d;
    return 0;
}

Однако, если прописать наследование одного типа от другого:

struct point3D : point2D { ... };

то ошибки не будет. В этом смысл зависимостей одного типа от другого, который важен для оператора static_cast и не важен для оператора reinterpret_cast. Даже если цепочка наследования будет более длинной:

struct point { };
 
struct point2D : point {
    int x, y;
};
 
struct point3D : point2D {
    int x, y, z;
};

Оператор static_cast по-прежнему будет отслеживать взаимосвязь между типами:

int main()
{
    point* ptr_pt = new point;
 
    point3D* ptr_3d = static_cast<point3D *>(ptr_pt);
    point3D* ptr_3d_2 = reinterpret_cast<point3D *>(ptr_pt);
 
    delete ptr_pt;
    return 0;
}

Формально, можем записать и обратное преобразование типов от дочернего класса (структуры) к базовому:

int main()
{
    point3D* ptr_3d = new point3D;
 
    point* ptr_pt = static_cast<point *>(ptr_3d);
    point* ptr_pt_2 = reinterpret_cast<point *>(ptr_3d);
 
    delete ptr_3d;
    return 0;
}

Однако в таком преобразовании смысла нет, т.к. компилятор это способен делать автоматически (для связанных типов) без каких-либо дополнительных операций. Поэтому static_cast рассматривают именно как оператор обратного приведения типа от базового к дочернему.

Оператор dynamic_cast

Все рассмотренные операторы приведения типов:

const_cast, reinterpret_cast, static_cast

отрабатывают на этапе компиляции программы, а потому относятся к статическим операторам. Последний же оператор dynamic_cast выполняет приведение связанных типов указателей или ссылок подобно операции static_cast, но делает это в процессе работы программы. Отсюда и пошло его название  dynamic (динамический). Давайте посмотрим, зачем понадобился такой оператор и как он работает.

Довольно часто в ООП используется механизм наследования классов с применением виртуальных функций. И давайте представим, что у нас имеется несколько классов для описания товаров магазина:

class Thing {
public:
    virtual void print() const { }
};
 
class Ball : public Thing {
    int radius;
    int color;
public:
    virtual void print() const override { puts("Ball"); }
    void get_data(int& r, int& c) const { r = radius; c = color; }
};
 
class Mouse : public Thing {
public:
    virtual void print() const override { puts("Mouse"); }
};

Здесь объявлен базовый класс Thing и производные от него классы Ball и Mouse. Затем, объявим класс Cart (тележка) для выбора покупаемых товаров:

class Cart {
public:
    void add_thing(const Thing& th)
    {
        th.print();
 
        const Ball* ptr_ball = dynamic_cast<const Ball *>(&th);
 
        if(ptr_ball) {
            int radius, color;
            ptr_ball->get_data(radius, color);
            puts("th is a Ball");
        }
        else
            puts("th is not a Ball");
    }
};

Это очень упрощенный класс исключительно для демонстрации работы оператора dynamic_cast. В метод add_thing передается константная ссылка на добавляемый в тележку товар. Затем, через виртуальный метод print() печатается переданный объект. И после этого, допустим, нам необходимо определить: относится ли объект th к объекту дочернего класса Ball? Так как через параметр можно передать совершенно любой класс, унаследованный от Thing, то его принадлежность к классу Ball следует определять в момент выполнения программы. На момент компиляции такой определенности мы не имеем. Поэтому следует использовать оператор dynamic_cast для динамического приведения типа указателя.

Если переданный объект действительно является объектом класса Ball, то указатель ptr_ball будет ссылаться на него. Если же это не так, то dynamic_cast вернет значение nullptr и указатель ptr_ball будет принимать это значение. Поэтому дальше можно прописать проверку на значение nullptr. И, если указатель не равен ему, то, например, вызвать метод get_data, который объявлен именно в классе Ball и ни в каком другом.

В функции main мы можем воспользоваться этими классами следующим образом:

int main()
{
    Cart cr;
 
    Ball b;
    Mouse m;
 
    cr.add_thing(b);
    cr.add_thing(m);
 
    return 0;
}

После запуска программы увидим в консоли строчки:

Ball
th is a Ball
Mouse
th is not a Ball

Обратите внимание, что оператор dynamic_cast работает только с указателями и реже со ссылками. Кроме того, для определения типа того или иного класса dynamic_cast использует адрес виртуальной таблицы методов классов. Поэтому он может работать только с классами, содержащими хотя бы один виртуальный метод.

Вообще, на практике лучше воздерживаться от применения оператора dynamic_cast, по крайней мере, по двум причинам. Во-первых, само приведение базового типа к дочернему – это сомнительная практика и, скорее всего, свидетельство неверно спроектированной архитектуры программы. И, во-вторых, оператор dynamic_cast использует механизм RTTI (Run-Time Type Identification), который позволяет идентифицировать тип объекта в процессе выполнения программы. Из-за этого скорость работы оператора dynamic_cast заметно снижается и его лучше не использовать при реализации высокоскоростных алгоритмов.

Операторы static_cast и dynamic_cast с указателями shared_ptr

В заключение этого занятия отмечу возможность применения операторов static_cast и dynamic_cast со смарт-указателями типа shared_ptr. Для этого используются следующие специальные функции:

#include <iostream>
#include <memory>
 
int main()
{
    std::shared_ptr<Thing> th{std::make_shared<Thing>()};
 
    std::shared_ptr<Ball> d_bl{std::dynamic_pointer_cast<Ball>(th)};
    std::shared_ptr<Ball> s_bl{std::static_pointer_cast<Ball>(th)};
 
    if(d_bl)
        puts("Ball");
    else
        puts("Not Ball");
 
    return 0;
}

Во всем остальном их работа идентична.

Практический курс по ООП C++: https://stepik.org/a/205781

Видео по теме