Операторы const_cast и reinterpret_cast

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

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

На этом занятии мы с вами отдельно поговорим об операторах преобразования типов, которые были введены в язык C++. Как вы помните, в курсе по языку Си мы уже говорили про операцию приведения типов и часто ей пользовались. Например, с ее помощью можно указателем с типом char просмотреть побайтно содержимое любой переменной:

    int a = 10;
    char * ptr = (char *)&a; // приведение типа int* к char *

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

    int a = 10;
    const int* ptr_cnst = &a;
    int * ptr = (int *)ptr_cnst;

Или, с типом char работать, словно это тип int:

    char a = 10;
    int* ptr_a = (int *)&a;

И так далее. Операции преобразования типов легко могут стать причинами трудноуловимых ошибок. Тем более что программисту здесь дается полная свобода действий, без каких-либо особых ограничений со стороны компилятора. И эту анархию решил хоть как-то ограничить разработчик языка C++ Бьерн Страуструп, введя для преобразования типов четыре новых оператора:

  • const_cast – снимает или добавляет модификатор const (и volatile) для текущего типа данных;
  • reinterpret_cast – преобразование одного типа в другой, с некоторой дополнительной проверкой со стороны компилятора на возможность указанного действия;
  • static_cast – преобразование типов с учетом цепочки наследования классов или структур;
  • dynamic_cast – преобразование типов с учетом цепочки наследования классов или структур в процессе выполнения программы (динамически).

Давайте детальнее посмотрим, как они работают и чем отличаются от обычной операции приведения типов, которую по-прежнему можно использовать в языке C++.

Оператор const_cast

Первый оператор const_cast просто снимает или добавляет модификатор const к текущему типу данных. Работает он следующим образом:

    int a = 10;
    const int* ptr_a = &a;
    int * ptr = const_cast<int *>(ptr_a); // снятие const
    const int * ptr_cnst = const_cast<const int *>(&a); // добавление const

Обратите внимание, как он записан. После его имени обязательно следуют угловые скобки, в которых прописывается тип, к которому приводится указатель, а затем, в круглых скобках идет сам указатель. Результат присваивается другому указателю того же типа.

Ту же самую операцию можно записывать и для ссылок:

    const double a = 10;
    const double& lnk_a_var = a;
    double& lnk_a = const_cast<double &>(lnk_a_var); // снятие const
    const double& lnk_a_cnst = const_cast<const double &>(lnk_a); // добавление const

А вот с обычными переменными функция const_cast работать не будет:

double b = const_cast<double>(a);

В качестве аргумента допускается прописывать либо указатель, либо ссылку.

Также, если типы приводимых данных не будут совпадать:

float& lnk_a = const_cast<double &>(lnk_a_var);

то тоже возникнет ошибка на этапе компиляции. То есть, const_cast только снимает/добавляет модификатор const у ссылок и указателей без изменения их базового типа.

Конечно, у вас может возникнуть вопрос, зачем все же понадобилось вводить этот оператор, если он по-прежнему вносит в программу небезопасное поведение? Наверное, модификатор const прописывается не случайно и снимать его не следует? Все верно. Именно поэтому нужно избегать использования оператора const_cast в тексте программы. Если вдруг он вам по каким-либо причинам потребовался, то лучше пересмотреть структуру программы и понять, почему так произошло. Исправить этот момент без привлечения небезопасного оператора const_cast. И только в очень, очень редких случаях его применение оправданно.

Оператор reinterpret_cast

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

Во-первых, он применим все так же к указателям и ссылкам. С обычными переменными работать не будет. Например:

    int a = 10;
    int b = reinterpret_cast<int>(a); // ok
    char c = reinterpret_cast<char>(a); // ошибка

Если изменение типа не происходит, то выполнится обычное присвоение (инициализация) одной переменной другой переменной. Если же указать разные типы, то возникнет ошибка.

Во-вторых, оператор reinterpret_cast не может снимать модификаторы const и volatile. За это, как мы уже знаем, отвечает другой оператор const_cast и их действия не пересекаются. Например:

    int a = 10;
    const int* ptr_a_cnst = &a;
    int* ptr_a = reinterpret_cast<int *>(ptr_a_cnst); // ошибка

Во всем остальном он похож на обычную операцию приведения типов языка Си. Преобразование типов между указателями:

    int a = 10;
    int* ptr_a = &a;
    char* ptr_ch = reinterpret_cast<char *>(ptr_a);
    void* ptr_v = reinterpret_cast<void *>(ptr_a);
    double* ptr_d = reinterpret_cast<double *>(ptr_a);

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

    double b = 0.5;
    double &lnk_b = b;
 
    int& lnk_bi = reinterpret_cast<int&>(lnk_b);
    long double& lnk_bld = reinterpret_cast<long double&>(lnk_b);

Также мы можем делать приведение типов и между пользовательскими типами данных. Например, объявим две структуры:

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

И, затем:

    point pt {1, 2};
    point3D* ptr_3d = reinterpret_cast<point3D *>(&pt);
    point& lnk_pt = reinterpret_cast<point &>(*ptr_3d);

В языке C++ предпочтение отдается именно таким операторам преобразования типов, а не классической операции языка Си.

На следующем занятии мы продолжим эту тему и рассмотрим две оставшихся операции static_cast и dynamic_cast.

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

Видео по теме