static_cast, dynamic_cast, const_cast, reinterpret_cast: когда что использовать?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему C++ не прощает ошибок приведения типов?

Привет, друг! Если ты хоть раз писал на C++ — ты точно сталкивался с ситуацией, когда компилятор ругается на несоответствие типов. И тут ты, скорее всего, вспоминаешь старый добрый C-style cast: (int)x. Работает? Да. Безопасно? Нет. Профессионально? Ещё менее.

В C++ есть четыре встроенных оператора приведения, и каждый из них решает свою задачу: static_cast, dynamic_cast, const_cast и reinterpret_cast. Использовать их правильно — значит писать код, который не только работает, но и понятен другим разработчикам. А ещё — не падает в рантайме с загадочными ошибками.

В этом туториале мы разберём каждый каст на реальных примерах, поймём, когда какой применять, а когда лучше вообще не кастить. Поехали!

static_cast: твой надёжный друг для обычных преобразований

static_cast — самый популярный и безопасный каст. Он используется для явных преобразований между родственными типами: числовые типы, указатели вверх/вниз по иерархии наследования (без проверки в рантайме), перечисления в целые и наоборот.

Когда использовать static_cast?

  • Преобразование int в double (и наоборот).
  • Преобразование указателей в рамках одной иерархии классов (upcast — безопасно, downcast — нужно быть уверенным).
  • Преобразование void* к конкретному типу.
  • Вызов явных конструкторов или операторов преобразования.

Пример:

double pi = 3.14159;
int approx = static_cast<int>(pi); // 3

class Base { public: virtual ~Base() = default; };
class Derived : public Base { public: void foo() {} };

Base* b = new Derived;
Derived* d = static_cast<Derived*>(b); // OK, но без проверки
d->foo();
delete b;

Важно: static_cast не делает проверку типов в рантайме. Если ты уверен, что объект действительно того типа — используй его. Если нет — смотри на dynamic_cast.

dynamic_cast: безопасный downcast с проверкой

dynamic_cast — единственный каст, который работает только с полиморфными классами (должна быть хотя бы одна виртуальная функция). Он проверяет в рантайме, можно ли безопасно привести указатель/ссылку. Если нет — возвращает nullptr (для указателей) или кидает std::bad_cast (для ссылок).

Когда использовать dynamic_cast?

  • Когда ты не уверен, что объект — именно производного типа.
  • При работе с гетерогенными контейнерами (например, vector из Base*).
  • В паттернах проектирования (например, Visitor).

Пример:

class Base { public: virtual ~Base() = default; };
class Derived : public Base { public: void specific() {} };
class Other : public Base {};

vector<Base*> objects = {new Derived, new Other, new Derived};

for (Base* obj : objects) {
    Derived* d = dynamic_cast<Derived*>(obj);
    if (d) {
        d->specific(); // безопасно только для Derived
    }
}

Минус: dynamic_cast медленнее static_cast, потому что требует RTTI (Run-Time Type Information). Не злоупотребляй им в критичных к производительности участках.

const_cast: когда нужно снять константность (но осторожно!)

const_cast — единственный каст, который может убрать (или добавить) cv-квалификаторы (const, volatile). Звучит круто, но на практике его используют редко и только в крайних случаях.

Зачем он вообще нужен? Иногда ты получаешь константную ссылку на объект, но тебе нужно вызвать старую функцию, которая не помечена const, хотя она не меняет объект. Или при работе с C-библиотеками.

Пример (правильный):

void printInt(const int* p) {
    // старый C-код, который не умеет const
    // но мы уверены, что он не меняет данные
    int* modifiable = const_cast<int*>(p);
    printf("%d\n", *modifiable);
}

Опасность: Если ты снимешь const с объекта, который изначально был объявлен как const, и попытаешься его изменить — это undefined behavior. Никогда так не делай!

const int value = 42;
int* p = const_cast<int*>(&value);
*p = 100; // UB! Может упасть, может не упасть — но код невалиден

reinterpret_cast: последнее средство (и опасное оружие)

reinterpret_cast — самый мощный и самый опасный каст. Он просто переинтерпретирует битовое представление одного типа как другого. Компилятор не делает никаких проверок — ты берёшь на себя полную ответственность.

Когда использовать reinterpret_cast?

  • Преобразование указателя в целое число (и обратно) — например, для передачи через API с void*.
  • Работа с сырой памятью (например, при реализации аллокаторов).
  • Взаимодействие с железом или низкоуровневыми библиотеками.

Пример:

struct Color { uint8_t r, g, b, a; };
uint32_t pixel = 0xAABBCCDD;
Color* c = reinterpret_cast<Color*>(&pixel);
// Теперь c->r = 0xDD, c->g = 0xCC, c->b = 0xBB, c->a = 0xAA (зависит от порядка байт!)

Предупреждение: reinterpret_cast нарушает strict aliasing rule. Используй его только если точно знаешь, что делаешь. Для большинства задач лучше подойдёт std::bit_cast (C++20) или memcpy.

Сравнительная таблица: какой каст выбрать?

Блоги

Рекомендаций по книгам

СитуацияКакой каст?Безопасность
Числовые преобразования (int → double)static_castВысокая
Upcast (Derived* → Base*)static_cast (или implicit)Высокая
Downcast с проверкойdynamic_castСредняя (есть RTTI)
Downcast без проверкиstatic_castНизкая (ты отвечаешь)
Снятие constconst_castНизкая (риск UB)
Переинтерпретация битов