Касты
Приведения типов C++, RTTI, const-correctness и ловушки с неопределённым поведением.
17 вопросов
JuniorТеорияОчень частоЧто такое array-to-pointer decay и как оно влияет на разрешение перегрузок?
Что такое array-to-pointer decay и как оно влияет на разрешение перегрузок?
Lvalue типа T[N] неявно преобразуется в T* на первый элемент — это array decay. При разрешении перегрузок decayed-массив соответствует T*, но не T (&)[N], поэтому ссылка на массив сохраняет размер.
Типичные ошибки
- ✗Вычислять длину массива как
sizeof(arr)/sizeof(arr[0])после decay внутри функции — получаем размер указателя / размер элемента - ✗Передавать 2D-массив
int a[3][4]вvoid f(int** p)— несовместимо, decay даётint(*)[4], а неint** - ✗Забывать, что строковые литералы — это
const char[N], и decay приводит кconst char*
Уточняющие вопросы
- →Как написать функцию, принимающую массив и выводящую его длину?
- →Почему
auto x = "hi";даётconst char*, а неconst char[3]?
JuniorТеорияЧастоЧто возвращает dynamic_cast при неудаче и как обрабатывать каждый вариант?
Что возвращает dynamic_cast при неудаче и как обрабатывать каждый вариант?
Указательная форма dynamic_cast<T*> при неудаче даёт nullptr — проверяйте перед разыменованием. Ссылочная dynamic_cast<T&> не может быть null и бросает std::bad_cast. Указательная — для ветвления, ссылочная — для инвариантов.
Типичные ошибки
- ✗Забывать проверить указатель и разыменовать
nullptrпосле неудачного downcast - ✗Ловить
std::exceptionдляbad_castбез понимания источника — лучше ловитьstd::bad_castпрямо - ✗Использовать исключения для штатного потока — для ожидаемых неудач предпочтительна указательная форма
Уточняющие вопросы
- →Зачем вообще ссылочная форма, если указательная покрывает всё?
- →Как ведёт себя
dynamic_cast<void*>?
JuniorТеорияЧастоЧем отличаются upcast и downcast и какие касты их выполняют?
Чем отличаются upcast и downcast и какие касты их выполняют?
Upcast Derived* → Base* — неявный и всегда безопасен. Downcast Base* → Derived* требует static_cast (без проверки, UB при ошибке) или dynamic_cast (RTTI-проверка, nullptr/bad_cast при ошибке, нужен полиморфный базовый класс).
Типичные ошибки
- ✗Использовать
static_castдля downcast и получать молча неверное поведение - ✗Вызывать
dynamic_castдля неполиморфного класса (нет virtual) — ошибка компиляции - ✗Забывать, что при множественном наследовании upcast может изменить числовой адрес указателя
Уточняющие вопросы
- →Как
dynamic_castработает изнутри (RTTI в vtable)? - →Какова стоимость
dynamic_castи когда его стоит избегать в горячих путях?
JuniorТеорияЧастоЧто такое narrowing-преобразование и когда компилятор его диагностирует?
Что такое narrowing-преобразование и когда компилятор его диагностирует?
Narrowing — неявное преобразование с возможной потерей информации (double→int, int→short, signed→unsigned). В brace-инициализации T x{e}; — ошибка компиляции; в copy-инициализации T x = e; и аргументах — молча разрешено.
Типичные ошибки
- ✗Писать
int x = 3.9;и ждать предупреждения — только brace-init запрещает narrowing - ✗Забывать, что
int → unsigned— это narrowing по стандарту - ✗Подавлять narrowing через
static_castвместо использования более широкого типа или проверки диапазона
Уточняющие вопросы
- →Почему
char c{300};— ошибка, аchar c = 300;— нет? - →Чем
gsl::narrowотличается отstatic_cast?
MiddleТеорияЧастоЧто плохого в C-style приведениях в C++ и какие C++-касты их заменяют?
Что плохого в C-style приведениях в C++ и какие C++-касты их заменяют?
C-style каст (T)x молча выбирает между const_cast, static_cast, reinterpret_cast, скрывая намерение и принимая опасные комбинации без предупреждения. Именованные C++-касты явные, grep-friendly, каждый — одна операция.
Типичные ошибки
- ✗Считать
(T)xравноценнымstatic_cast<T>(x)— он может молча вставитьreinterpret_cast - ✗Считать функциональный каст
T(x)безопаснее — для не-классовых типов семантика та же, что и у(T)x - ✗Прятать снятие const за C-style кастом и писать в результат — UB, если оригинал был действительно const
Уточняющие вопросы
- →Когда
T(x)становится вызовом конструктора, а не кастом? - →Как
cppcoreguidelines-pro-type-cstyle-castв clang-tidy помогает это контролировать?
MiddleТеорияЧастоКогда const_cast и reinterpret_cast опасны?
Когда const_cast и reinterpret_cast опасны?
const_cast безопасен только если исходный объект не был const; изменение реально const-объекта — UB. reinterpret_cast меняет тип выражения, не создавая новый объект, и может нарушить выравнивание, aliasing или ABI.
Типичные ошибки
- ✗Использование const_cast для изменения строковых литералов или const-глобальных объектов
- ✗Восприятие reinterpret_cast как безопасного механизма сериализации
- ✗Игнорирование strict aliasing и требований выравнивания
Уточняющие вопросы
- →Как безопасно посмотреть байты объекта?
- →Что такое std::bit_cast и чем он отличается?
MiddleТеорияЧастоЧто такое dynamic_cast и как RTTI его обеспечивает?
Что такое dynamic_cast и как RTTI его обеспечивает?
dynamic_cast<T*>(ptr) проверяет во время выполнения, указывает ли ptr на T или производный класс, возвращая null (указатель) или бросая std::bad_cast (ссылка). Требует RTTI-метаданные в vtable полиморфных классов.
Типичные ошибки
- ✗Применять
dynamic_castк не-полиморфному классу — ошибка компиляции; у базового класса должна быть хотя бы одна виртуальная функция - ✗Не проверять результат на null после приведения указателя — при неудаче разыменование является UB
- ✗Отключать RTTI с
-fno-rtti, но всё равно использоватьdynamic_castилиtypeid— неопределённое поведение
Уточняющие вопросы
- →Когда вы использовали бы
dynamic_castдля cross-cast между сестринскими классами? - →Как
typeidсвязан сdynamic_castи что он возвращает для полиморфного типа?
MiddleТеорияЧастоВ чём разница между static_cast и dynamic_cast в иерархии классов?
В чём разница между static_cast и dynamic_cast в иерархии классов?
static_cast выполняет преобразование на этапе компиляции и не проверяет динамический тип объекта. dynamic_cast проверяет реальный тип во время выполнения для полиморфных классов: при неудачном приведении указателя возвращает nullptr, при приведении ссылки бросает std::bad_cast.
Типичные ошибки
- ✗Использование static_cast для непроверенного downcast в ожидании безопасности
- ✗Забывать, что dynamic_cast требует полиморфный базовый тип
- ✗Ожидание, что dynamic_cast для ссылки вернёт null-ссылку
Уточняющие вопросы
- →Что произойдёт, если RTTI отключён?
- →Когда лучше изменить дизайн вместо использования dynamic_cast?
MiddleТеорияЧастоКак написать пользовательское преобразование и когда оно должно быть explicit?
Как написать пользовательское преобразование и когда оно должно быть explicit?
Два варианта: неявный конструктор T(U) даёт U → T, а член operator U() const — T → U. Помечайте explicit, если преобразование не точное и не очевидное, чтобы избежать незаметного использования при разрешении перегрузок.
Типичные ошибки
- ✗Забывать, что одно-аргументные конструкторы — converting, если нет
explicit(с C++11 — и многоаргументные при{}) - ✗Добавлять
operator bool()безexplicit— получаешь сюрпризы в арифметике и сравнениях - ✗Объявлять и converting-конструктор, и conversion-оператор между одной парой типов — неоднозначность
Уточняющие вопросы
- →Как контекстное преобразование к bool в C++11 взаимодействует с
explicit operator bool()? - →Почему
std::stringстроится изconst char*неявно, аstd::filesystem::pathизstd::string— явно?
JuniorТеорияИногдаЧто делает функциональный каст T(x) и чем он отличается от (T)x?
Что делает функциональный каст T(x) и чем он отличается от (T)x?
С одним аргументом T(x) эквивалентен (T)x — та же опасная эскалация через const/static/reinterpret_cast. С несколькими аргументами T(a, b) уже не каст, а вызов конструктора.
Типичные ошибки
- ✗Считать функциональный каст безопаснее C-style — для фундаментальных типов они идентичны
- ✗Писать
int(p)для преобразования указателя в целое — молча вставляетсяreinterpret_cast - ✗Смешивать
T(x)(может быть каст) иT{x}(всегда direct-list-init)
Уточняющие вопросы
- →Почему
T()без аргументов выполняет value-initialization? - →Когда
T(x)парсится как объявление функции (most-vexing-parse)?
MiddleТеорияИногдаЧто такое std::bit_cast и почему он предпочтительнее reinterpret_cast для type punning?
Что такое std::bit_cast и почему он предпочтительнее reinterpret_cast для type punning?
std::bit_cast<To>(from) (C++20) возвращает To, побитово скопированный из from. Требует trivially copyable и одинаковый размер; не нарушает strict aliasing, constexpr при возможности.
Типичные ошибки
- ✗Использовать
*reinterpret_cast<T*>(&x)для type punning — нарушение strict aliasing для не-char типов - ✗Применять
unionдля type punning в C++ — формально живёт только последний записанный член (UB в C++ до bit_cast в C++20) - ✗Забывать, что
bit_castтребует одинаковых размеров — для разных размеров нужен memcpy в буфер
Уточняющие вопросы
- →Как реализовать
bit_castдо C++20? - →Что такое strict aliasing и какие типы из него исключены?
MiddleТеорияИногдаКогда запись через указатель, у которого const_cast снял const, является UB?
Когда запись через указатель, у которого const_cast снял const, является UB?
Если оригинальный объект объявлен const, изменение через указатель со снятым const — UB. Безопасно только когда сам объект не const, но виден через const-интерфейс (например, legacy C-API).
Типичные ошибки
- ✗Снять const с
const int x = 5;и записать — UB - ✗Использовать
const_cast, чтобы заглушить ошибки компиляции, вместо починки const-корректности API - ✗Забывать, что
const_castне может снятьvolatileс действительно volatile объекта
Уточняющие вопросы
- →Что такое
mutableи как оно работает с const-методами? - →Почему стандартная библиотека предоставляет
std::as_const, но неstd::as_mutable?
MiddleТеорияИногдаЧто такое последовательность неявных преобразований и какие у неё стадии?
Что такое последовательность неявных преобразований и какие у неё стадии?
Последовательность неявных преобразований до трёх стадий: (1) стандартное (lvalue→rvalue, массив/функция→указатель); (2) одно пользовательское (конструктор или operator T()); (3) ещё одно стандартное. В одной последовательности допустимо только одно пользовательское.
Типичные ошибки
- ✗Ожидать цепочку из двух пользовательских преобразований — стандарт запрещает
- ✗Забывать ранжирование при разрешении перегрузок: exact match > promotion > standard > UDC > ellipsis
- ✗Не помечать конверсионные конструкторы
explicit— получать неожиданные неявные преобразования
Уточняющие вопросы
- →Чем converting-конструктор отличается от converting-оператора?
- →Как
explicitу конверсионного оператора влияет на разрешение перегрузок?
MiddleТеорияИногдаКак безопасно преобразовать указатель в целое число и обратно?
Как безопасно преобразовать указатель в целое число и обратно?
Используйте std::uintptr_t из <cstdint> с reinterpret_cast<std::uintptr_t>(ptr) и reinterpret_cast<T*>(integer). Round-trip указатель→целое→тот же указатель определён стандартом; арифметика над целым — implementation-defined.
Типичные ошибки
- ✗Преобразовать указатель в
intна 64-битной системе — молча обрезает, round-trip ломается - ✗Считать
(uintptr_t)ptr & MASKпереносимым — битовое тегирование указателей implementation-defined - ✗Путать
std::ptrdiff_tиstd::uintptr_t— разные задачи (вычитание vs. представление адреса)
Уточняющие вопросы
- →Что добавляет
std::intptr_tсверхstd::uintptr_t? - →Можно ли хранить указатель на функцию в
void*?
MiddleТеорияИногдаКак безопасно сделать round-trip типизированного указателя через void*?
Как безопасно сделать round-trip типизированного указателя через void*?
Преобразование T* → void* неявное и всегда безопасное. Обратное void* → T* требует static_cast<T*>(vp). Round-trip T* → void* → T* восстанавливает исходный указатель; каст к другому U* корректен только если U достаточно близок к T (например, cv-квалификации) — иначе проблема со strict aliasing при разыменовании.
Типичные ошибки
- ✗Использовать
reinterpret_castдляvoid* → T*— правильный выборstatic_cast - ✗Хранить указатели на функции в
void*— не переносимо; используйте отдельный typed-указательvoid(*)() - ✗Терять const при round-trip
const T* → void* → T*— нужен правильный cv-квалификатор
Уточняющие вопросы
- →Почему
T**не приводится неявно кvoid**? - →Что заменяет
void*в современном C++ для type-erased хранилищ?
SeniorТеорияРедкоКак RTTI работает внутри?
Как RTTI работает внутри?
По Itanium ABI слот vtable указывает на std::type_info класса. dynamic_cast обходит граф базовых классов через смещения и указатели type_info в __base_class_type_info. typeid(expr) возвращает type_info наиболее производного объекта через тот же слот; -fno-rtti удаляет эти структуры.
Типичные ошибки
- ✗Считать, что RTTI имеет пренебрежимо малую стоимость — оно увеличивает размер бинарника, а
dynamic_castвыполняет реальную работу, пропорциональную глубине иерархии - ✗Сравнивать объекты
type_infoчерез==через границы разделяемых библиотек — может потребоваться сравнение строк имён (name()) - ✗Думать, что
-fno-rttiтолько убираетdynamic_cast— также убираетtypeidи сопоставление типов исключений для полиморфных типов
Уточняющие вопросы
- →Как реализовать собственный лёгкий RTTI без версии, генерируемой компилятором?
- →В чём разница между
typeidна указателе иtypeidна разыменованном объекте?
SeniorТеорияРедкоПочему downcast от виртуального базового класса требует dynamic_cast, а не static_cast?
Почему downcast от виртуального базового класса требует dynamic_cast, а не static_cast?
При виртуальном наследовании смещение от виртуального базового класса до производного объекта неизвестно во время компиляции — зависит от фактического типа и хранится в vtable. static_cast — ошибка компиляции; dynamic_cast читает RTTI и вычисляет смещение во время выполнения.
Типичные ошибки
- ✗Пытаться
static_castот виртуального базового класса и не понимать ошибку компиляции - ✗Думать, что
dynamic_castнужен только в широких иерархиях — он также требуется для downcast от виртуального базового класса - ✗Строить большие diamond-иерархии и страдать от стоимости
dynamic_castна горячих путях
Уточняющие вопросы
- →Какова раскладка класса с виртуальным базовым классом?
- →Почему
dynamic_cast<void*>(p)возвращает указатель на самый производный объект?