Лекция 9

02.04.2025 Обновлено: 02.04.2025
int main(int, char**) {
    double d = -12.3456789;
    std::cout << d  << std::endl;

    float f = d;
    std::cout << f  << std::endl;

    int i = d;
    std::cout << i  << std::endl;

    uint32_t ui = d;
    std::cout << ui << std::endl;

    char ch = d;
    std::cout << ch << std::endl;

    return 0;
}

Какие касты существуют

  • Implicit (неявное преобразование)
  • Explicit (явные):
    • const_cast
    • static_cast
    • dynamic_cast
    • reinterpret_cast
    • C-style cast

Implicit cast

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

C-style cast

Синтаксис: (type)expression

int main(int, char**) {
    int i = 1;
    std::cout << *(double*)&i << std::endl;
}

Опасен тем, что компилятор не проверяет корректность — он просто делает то, что сказано. По сути это «умная» обёртка, которая последовательно пробует const_caststatic_castreinterpret_cast. Лучше использовать явные C++ касты.

const_cast

  • Убирает const или volatile с переменной.
  • Преобразует указатели и ссылки на одинаковые типы данных.

Использовать только если есть полная уверенность, что объект изначально не был константным — иначе undefined behavior. Обращаться очень аккуратно.

Типичные сценарии:

  • В константном методе модифицировать что-то (когда сам объект изначально не константный).
  • Совместимость с C-кодом, где API не принимает const-указатели, но не модифицирует данные.

static_cast

  • Работает в compile-time — быстро, без накладных расходов в рантайме.
  • Пытается выполнить преобразование через конструкторы и операторы приведения типов.
  • Подходит для стандартных типов (предпочтительная замена C-style касту).
  • Умеет приводить указатели внутри одной иерархии классов (вверх и вниз).
  • Умеет кастовать из void* (обратно к конкретному типу).

Гарантия компилятора: если цепочка преобразований невозможна — код просто не скомпилируется.

Осторожно с кастом вниз по иерархии (Base*Derived*):

  • Если объект изначально и был Derived — всё работает корректно.
  • Если объект изначально был Base — формально получим «полноценный» Derived, но новые поля производного класса никак не проинициализированы → undefined behavior (могут лежать какие угодно байты — мусор).
  • static_cast не проверяет в рантайме, корректен ли каст вниз. Для этого есть dynamic_cast.

dynamic_cast

  • Преобразует указатели и ссылки вверх и вниз по иерархии.
  • Работает в runtime через RTTI (Runtime Type Identification).
  • Использует таблицу виртуальных функций для проверки реального типа объекта.

Поведение при неудачном касте:

  • Если кастуем указатель — вернёт nullptr.
  • Если кастуем ссылку — бросит исключение std::bad_cast.

Цена:

  • Дополнительное время в рантайме на проверку.
  • Класс обязан иметь хотя бы одну виртуальную функцию (иначе нет vtable → класс неполиморфный → ошибка компиляции).

reinterpret_cast

Страшно, бойтесь, противозаконно…

  • Кастует несовместимые типы — просто переинтерпретирует побитовое представление памяти, никаких преобразований не делает.
  • Компилятор ничего не проверяет и не предупреждает.

Нужно точно знать, что лежит в этом блоке памяти. Типичный легитимный сценарий — сериализация: кастуем указатель на структуру к char* и пишем байты в файл; при чтении кастуем обратно, зная размер структуры.

Подводные камни:

  • Выравнивание (alignment) — у разных типов разные требования.
  • Размер одних и тех же типов может различаться на разных платформах (long, size_t).
  • Порядок байт — big-endian vs little-endian.
  • Представление float/double тоже может различаться.

Если запись и чтение происходят на одной и той же машине — об этих проблемах можно не беспокоиться.

Практическое применение — быстрая запись в бинарный файл: кастуем указатель на начало вектора к char* и пишем всё одним системным вызовом write. Это быстрее (один syscall вместо многих) и компактнее, чем текстовый вывод.