Сегодня попробуем разобраться в том, как происходят преобразования типов в С++
Начнем с static_cast. Его проверки происходят на этапе компиляции. Кастовать можно базовые типы к друг другу
double y = 0.06;
int x = static_cast(y);
Так же можно преобразовывать связанные типы. Например, класс потомок к базовому типу — upcast, так и в обратную сторону от базового класса к потомку — downcast.
struct Base
{
};
struct Derived : public Base
{
};
int main(int argc, char *argv[])
{
Derived derived;
//upcast
Base *base = static_cast (&derived);
//downcast
Derived *derived2 = static_cast(base);
return 0;
}
Однако в данном случае нужно понимать, что проверяется только связанность типов. Возьмем пример ниже:
struct Base
{
};
struct Derived : public Base
{
};
int main(int argc, char *argv[])
{
Base base;
Derived *derived = static_cast(&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(data.c_str()));
Важно осознавать, что константные данные могут физически храниться в неизменяемой секции, например во флеше. Поэтому даже если обмануть компилятор и убрать проверку на этапе компиляции, то в рантайме это может привести к неопределенному поведению.
Еще один случай который нам нужно рассмотреть это reinterpret_cast. Допустим мы получили откуда нибудь из UART данные int data[] = {29, 5, 2022}; которые говорят нам о том, какой сейчас день, месяц и год. И допустим у нас есть структура, с которой можно удобно работать с датами struct Days. Раскладка по памяти одинаковая, а данные разные, как нам преобразовать одни данные в другие. Вот тут можно будет воспользоваться reinterpret_cast.
#include
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(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 просто дополнительно говорят компилятору проверять, связаны эти типы или нет.