Сегодня попробуем разобраться в том, как происходят преобразования типов в С++
Начнем с static_cast. Его проверки происходят на этапе компиляции. Кастовать можно базовые типы к друг другу
double y = 0.06; int x = static_cast<int>(y); |
Так же можно преобразовывать связанные типы. Например, класс потомок к базовому типу — upcast, так и в обратную сторону от базового класса к потомку — downcast.
struct Base { }; struct Derived : public Base { }; int main(int argc, char *argv[]) { Derived derived; //upcast Base *base = static_cast<Base*>(&derived); //downcast Derived *derived2 = static_cast<Derived*>(base); return 0; } |
Однако в данном случае нужно понимать, что проверяется только связанность типов. Возьмем пример ниже:
struct Base { }; struct Derived : public Base { }; int main(int argc, char *argv[]) { Base base; Derived *derived = static_cast<Derived*>(&base); return 0; } |
Мы создаем объект базового класса и его кастуем к классу потомку. Так как объекты связанные, то на этапе компиляции формально ошибок никаких не будет. Однако надо понимать, что в данных которые соответствуют Derived будет лежать мусор и использование его данных приведет к неопределенному поведению UB.
Следующий тип преобразований — const_cast. Может понадобиться для снятия константности у каких либо данных. Возьмем такой пример, который в теории может случиться на практике. У нас есть функция, которая на вход принимает тип char* и у нас есть строка string data.
void PrintData(char* data) { cout << data << endl; } int main(int argc, char *argv[]) { string data = "hello"; PrintData(data.c_str()); return 0; } |
Хотя формально тут нет ошибки, но данный пример не скомпилируется, так как data.c_str() возвращает const char*, а не char*. Поэтому в данном случае можно воспользоваться const_cast.
PrintData(const_cast<char *>(data.c_str())); |
Важно осознавать, что константные данные могут физически храниться в неизменяемой секции, например во флеше. Поэтому даже если обмануть компилятор и убрать проверку на этапе компиляции, то в рантайме это может привести к неопределенному поведению.
Еще один случай который нам нужно рассмотреть это reinterpret_cast. Допустим мы получили откуда нибудь из UART данные int data[] = {29, 5, 2022}; которые говорят нам о том, какой сейчас день, месяц и год. И допустим у нас есть структура, с которой можно удобно работать с датами struct Days. Раскладка по памяти одинаковая, а данные разные, как нам преобразовать одни данные в другие. Вот тут можно будет воспользоваться reinterpret_cast.
#include <iostream> using namespace std; struct Days { int day; int month; int year; }; void PrintData(Days& d) { cout << "day=" << d.day << endl; cout << "month=" << d.month << endl; cout << "year=" << d.year << endl; } int main(int argc, char *argv[]) { int data[] = {29, 5, 2022}; PrintData(reinterpret_cast<Days&>(data)); return 0; } |
Заключение. Сегодня мы рассмотрели 3 варианта преобразования типов. Про dynamic_cast уже упоминалось в статье про RTTI. Важно понимать, что reinterpret_cast, const_cast, static_cast проверяются в compile time, а dynamic_cast в runtime. Где и какое преобразование применять зависит от того какие необходимо производить манипуляции. Важно понимать что static_cast может сдвигать указатель при преобразованиях, а reinterpret_cast нет.
Зачем такие сложности, интересно. На С я пишу просто PrintData(&data); и всё работает и так…
Некоторые из этих операций будут эквивалентны тому, что вы пишите на Си и более того взаимозаменяемы. До тех пор пока вы не будете использовать шаблонную магию. По сути static_cast и reinterpret_cast просто дополнительно говорят компилятору проверять, связаны эти типы или нет.