Практический курс по ООП C++: https://stepik.org/a/205781
Продолжаем тему
операторов преобразования типов языка 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