В чем разница между static_cast, dynamic_cast, reinterpret_cast и const_cast в C++?

Ответ

В C++ для приведения типов используются четыре оператора *_cast, каждый со строго определенной семантикой, что заменяет опасное C-стилевое приведение (type)value.

1. static_cast

  • Назначение: Стандартное, «разумное» приведение типов, проверяемое на этапе компиляции.
  • Применение:
    • Преобразования между числовыми типами (int -> float, enum -> int).
    • Явное преобразование указателей/ссылок вверх и вниз по иерархии наследования (downcast без проверки времени выполнения).
    • Преобразование void* к конкретному типу указателя.
    • Вызов явных конструкторов преобразования или операторов приведения.
  • Пример:

    float f = 3.14;
    int i = static_cast<int>(f); // i = 3 (усечение)
    
    class Base {};
    class Derived : public Base {};
    Base* b = new Derived();
    // Downcast: компилятор доверяет программисту.
    Derived* d = static_cast<Derived*>(b); // ОК, но если b не указывал на Derived -> UB

2. dynamic_cast

  • Назначение: Безопасное приведение указателей/ссылок в иерархии полиморфных классов с проверкой во время выполнения (RTTI - Runtime Type Information).
  • Применение: Преобразование вниз (downcast) или вбок (crosscast) по иерархии наследования.
  • Условия: Работает только с типами, имеющими хотя бы одну виртуальную функцию (полиморфными).
  • Поведение: Для указателей при неудаче возвращает nullptr. Для ссылок — генерирует исключение std::bad_cast.
  • Пример:

    class Base { virtual void foo() {} }; // Полиморфный класс
    class Derived : public Base {};
    
    Base* b1 = new Derived();
    Base* b2 = new Base();
    
    Derived* d1 = dynamic_cast<Derived*>(b1); // Успех, d1 != nullptr
    Derived* d2 = dynamic_cast<Derived*>(b2); // Неудача, d2 == nullptr
    
    try {
        Derived& rd1 = dynamic_cast<Derived&>(*b1); // Успех
        Derived& rd2 = dynamic_cast<Derived&>(*b2); // Выбросит std::bad_cast
    } catch (const std::bad_cast& e) {
        std::cerr << "Cast failed: " << e.what() << 'n';
    }

3. reinterpret_cast

  • Назначение: Низкоуровневое, «грубое» приведение, которое интерпретирует битовое представление значения одного типа как значение другого типа. Компилятор не выполняет никаких проверок.
  • Применение:
    • Преобразование между указателями на несвязанные типы (например, Foo* в Bar*).
    • Преобразование указателя в целочисленный тип (uintptr_t) и обратно.
    • Преобразование в void* и обратно для типов, не являющихся POD (с осторожностью).
  • Опасность: Использование результата такого приведения часто ведет к неопределенному поведению, если нарушаются strict aliasing rules.
  • Пример:
    int* ip = new int(0xDEADBEEF);
    // Трактуем биты int как биты float (не преобразование значения!)
    float* fp = reinterpret_cast<float*>(ip);
    // Преобразование указателя в число (адрес памяти)
    uintptr_t addr = reinterpret_cast<uintptr_t>(ip);

4. const_cast

  • Назначение: Добавление или удаление квалификаторов const и volatile.
  • Применение: В основном для вызова устаревших C-функций, которые принимают не-const указатели, но не модифицируют данные.
  • Важно: Применение const_cast для модификации по-настоящему константного объекта (созданного как const) приводит к неопределенному поведению.
  • Пример:

    void legacy_c_function(char* str); // Объявлена в C-библиотеке
    
    void modern_cpp_func(const char* input) {
        // legacy_c_function(input); // Ошибка компиляции: преобразование const char* -> char*
        legacy_c_function(const_cast<char*>(input)); // OK, если функция не меняет input
    }
    
    const int ci = 10;
    // int* pi = &ci; // Ошибка
    int* pi = const_cast<int*>(&ci);
    *pi = 20; // НЕОПРЕДЕЛЕННОЕ ПОВЕДЕНИЕ! Попытка изменить константный объект.

Общее правило выбора: Пытайтесь использовать static_cast. Для безопасного полиморфного downcast используйте dynamic_cast. К reinterpret_cast и const_cast прибегайте только в крайних случаях, четко понимая последствия.

Ответ 18+ 🔞

Ну, вот смотри, в C++ эти ваши приведения типов — это вообще ёперный театр. Раньше-то в C был один уродец — (type)value, и делай с ним что хочешь. А тут, бля, целых четыре спецоператора *_cast, и у каждого своя хитрая жопа. И доверия к ним, бля, ноль, если не понимаешь, что за манда с ушами перед тобой.

1. static_cast

  • Зачем нужен: Это типа стандартное, «разумное» приведение, которое компилятор проверяет, пока ты код пишешь. Не в рантайме, а прямо тут, на месте.
  • Где впендюрить:
    • Числа туда-сюда гонять (int -> float, enum -> int).
    • Указатели по иерархии наследования двигать (вверх-вниз). Но вниз — это на свой страх и риск, проверки-то нет!
    • Из void* назад в нормальный тип вытащить.
    • Вызвать явные конструкторы или операторы приведения, если они есть.
  • Пример, чтобы понятно было:
    float f = 3.14;
    int i = static_cast<int>(f); // i = 3, просто отрезал хвост, как собака

    А вот с наследованием прикол:

    class Base {};
    class Derived : public Base {};
    Base* b = new Derived();
    // Downcast, то есть вниз кастим. Компилятор тебе верит на слово, чувак.
    Derived* d = static_cast<Derived*>(b); // Если b НЕ указывал на Derived — тебе пиздец, неопределённое поведение.

2. dynamic_cast

  • Зачем нужен: А вот это уже безопасный пацан. Он для полиморфных классов (где есть виртуальные функции) и проверяет всё на ходу, во время работы программы (RTTI).
  • Где впендюрить: Когда нужно безопасно скастовать указатель или ссылку вниз по иерархии или даже вбок.
  • Фишка: Работает только если в классе есть хоть одна виртуальная функция. Иначе — хуй с горы.
  • Как работает: Для указателей — если не получилось, вернёт nullptr. Для ссылок — выбросит исключение std::bad_cast, и будет тебе хиросима.
  • Пример, чтобы жизнь мёдом не казалась:

    class Base { virtual void foo() {} }; // Виртуальная есть — полиморфный, ок
    class Derived : public Base {};
    
    Base* b1 = new Derived();
    Base* b2 = new Base(); // Создаём именно Base, а не Derived
    
    Derived* d1 = dynamic_cast<Derived*>(b1); // Успех, d1 != nullptr
    Derived* d2 = dynamic_cast<Derived*>(b2); // Неудача, d2 == nullptr, сиди и думай, что делать
    
    try {
        Derived& rd1 = dynamic_cast<Derived&>(*b1); // Прокатило
        Derived& rd2 = dynamic_cast<Derived&>(*b2); // А вот тут тебе в сраку исключение std::bad_cast
    } catch (const std::bad_cast& e) {
        std::cerr << "Cast failed: " << e.what() << 'n'; // И пиши пропало
    }

3. reinterpret_cast

  • Зачем нужен: Это, бля, самый отбитый каст. Низкоуровневый, грубый. Он просто берёт биты одного типа и говорит: «А теперь ты — другой тип». Никаких проверок, вообще нихуя.
  • Где впендюрить:
    • Преобразовать указатель на Foo в указатель на Bar, когда они нихуя не родственники.
    • Запихнуть адрес указателя в целое число (типа uintptr_t) и обратно.
    • В void* и обратно для сложных типов (но осторожно, а то сломаешь).
  • Опасность: Используешь результат — и можешь получить неопределённое поведение, потому что нарушишь strict aliasing rules. Сам потом будешь искать, почему всё накрылось медным тазом.
  • Пример для полного охуения:
    int* ip = new int(0xDEADBEEF);
    // Сейчас мы просто скажем, что биты этого int — это на самом деле биты float. Это НЕ преобразование значения!
    float* fp = reinterpret_cast<float*>(ip); // Держись крепче
    // А тут адрес указателя в число превратим
    uintptr_t addr = reinterpret_cast<uintptr_t>(ip);

4. const_cast

  • Зачем нужен: Только и исключительно для того, чтобы добавить или снять const и volatile.
  • Где впендюрить: Чаще всего, когда вызываешь какую-то старую, дурно пахнущую C-функцию, которая хочет char*, а у тебя const char*, и ты знаешь, что она ничего не меняет.
  • Важно, блядь!: Если ты снимешь const с по-настоящему константного объекта (который создали как const) и попробуешь его изменить — это прямой билет в ад неопределённого поведения. Терпения у компилятора ноль, ебать.
  • Пример, где можно и где нельзя:

    void legacy_c_function(char* str); // Функция из каменного века, из C-библиотеки
    
    void modern_cpp_func(const char* input) {
        // legacy_c_function(input); // Не скомпилируется, ибо константность
        legacy_c_function(const_cast<char*>(input)); // Ок, если функция честная и input не трогает
    }
    
    const int ci = 10; // Настоящая константа
    // int* pi = &ci; // Ошибка, логично же
    int* pi = const_cast<int*>(&ci); // Сняли const, компилятор пропустил
    *pi = 20; // НЕОПРЕДЕЛЁННОЕ ПОВЕДЕНИЕ! Ты попытался изменить константу. Готовься к сюрпризам.

Итог, чувак: Пытайся везде пихать static_cast — он самый нормальный. Для безопасного полиморфного приведения вниз — только dynamic_cast. А reinterpret_cast и const_cast — это как гранаты, бери в руки, только если точно знаешь, за кого тебя ебать в сраку, если что-то пойдёт не так.