Что такое приведение типов
Приведение типов означает преобразование значения из одного типа в другой — превращение double в int, char в его числовой код или указателя на базовый класс в указатель на производный. C++ выполняет некоторые из этих преобразований за вас автоматически, но именно те, что он делает молча, и есть место, где прячутся ошибки.
Есть две разновидности: неявные преобразования, которые происходят сами собой, и явные касты, которые вы пишете вручную. С одним из проявлений этого вы уже встречались в операторах — целочисленное деление. Приведение типов — это способ взять его под контроль.
Неявные преобразования
Когда вы смешиваете числовые типы в выражении, C++ повышает «меньший» тип до «большего», чтобы обе стороны совпадали. Обычно это делает именно то, что вам нужно.
Беда начинается, когда преобразование идёт в обратную сторону — от более широкого типа к более узкому. Это сужающее преобразование, и оно может молча потерять данные.
При приведении float к int происходит усечение в сторону нуля — не округление, поэтому 3.99 становится 3. А попытка втиснуть 300 в char приводит к переполнению. Многие компиляторы здесь предупреждают; некоторые — нет. Когда вы действительно намерены сузить, скажите об этом явно с помощью каста, чтобы следующий читатель знал, что это сделано намеренно.
Исправление ловушки целочисленного деления
Самая частая причина для каста — деление. Когда оба операнда целые, / выполняет целочисленное деление и отбрасывает остаток.
Исправление — static_cast<double> на один операнд до деления. Частая ошибка — static_cast<double>(got / total): это слишком поздно, потому что к моменту выполнения каста got / total уже равно 0, так что вы получаете 0.0. Приводите операнд, а не результат.
static_cast: ваш каст по умолчанию
C++ даёт вам четыре именованных каста. Тот, который вы будете использовать в 95 % случаев, — это static_cast<T>(value), выполняющий чётко определённые преобразования между родственными типами: числовые преобразования, enum в int, void* обратно в типизированный указатель и переход вверх/вниз по иерархии классов, когда тип вам уже известен.
Предпочитайте static_cast старому касту в стиле C (int)balance. Каст в стиле C попробует любое преобразование, лишь бы код скомпилировался, — включая опасные ниже, — поэтому он может молча снять const или переинтерпретировать сырые байты. static_cast разрешает только те преобразования, которые компилятор действительно может обосновать, а многословный static_cast<...> тривиально искать при ревью кода.
// Избегать - каст в стиле C, без страховки:
int dollars = (int) balance;
// Предпочесть - явный, проверяемый, легко находимый:
int dollars = static_cast<int>(balance);
Остальные три каста (используйте умеренно)
Оставшиеся касты существуют для конкретных, узких задач. Прибегайте к ним лишь тогда, когда static_cast действительно не справляется.
const_cast убирает (или добавляет) const. Его единственное законное применение — вызов API в стиле C, в котором забыли пометить параметр как const. Изменять через const_cast объект, который изначально был объявлен как const, — это неопределённое поведение.
void legacyApi(char* msg); // старый API, не принимает const
const char* text = "hello";
legacyApi(const_cast<char*>(text)); // допустимо, только если legacyApi не пишет в него
reinterpret_cast переинтерпретирует сырой битовый узор — например, указатель как целочисленный адрес. Он не выполняет никакого преобразования и крайне небезопасен; почти всегда это признак того, что стоит пересмотреть проектное решение.
dynamic_cast безопасно преобразует указатель или ссылку на базовый класс в производный тип во время выполнения, используя фактический тип объекта. Он требует полиморфной базы (класса хотя бы с одной виртуальной функцией) и возвращает nullptr, если приведение неприменимо.
Если бы a указывал на какой-то другой Animal, dynamic_cast<Dog*> вернул бы nullptr и выполнилась бы ветка else — именно поэтому он безопаснее, чем слепое использование static_cast для спуска вниз по иерархии.
Распространённые ошибки, которых стоит избегать
- Приведение результата вместо операнда.
static_cast<double>(a / b)сначала отбрасывает вашу дробную часть. Приводитеaилиb. - Предположение, что float к int округляется. Он усекается:
static_cast<int>(2.99)равно2. Для округления используйтеstd::round,std::lroundи т. д. - Использование каста в стиле C. Он скрывает, какое преобразование происходит. Используйте
static_cast, и вы получите ошибку компиляции, когда преобразование небезопасно, вместо тихого сюрприза. - Сужение в слишком маленький тип. Приведение
300кcharили огромногоlongкintпереполняется по кольцу или выходит за пределы. Выбирайте целевой тип, достаточно широкий для диапазона.
Далее: If-Else
Теперь, когда вы умеете аккуратно преобразовывать и сравнивать значения, следующий шаг — принимать с их помощью решения. Оператор if-else выполняет разный код в зависимости от того, истинно ли условие (true), — основа любой программы с ветвлением.
Часто задаваемые вопросы
В чём разница между static_cast и приведением в стиле C в C++?
Приведение в стиле C вроде (int)x поочерёдно пробует каждое преобразование — оно может незаметно превратиться в опасный reinterpret_cast или снять const. static_cast<int>(x) выполняет только родственные преобразования, которые компилятор может проверить, поэтому компилятор отвергает бессмыслицу. В современном C++ всегда предпочитайте static_cast приведениям в стиле C; это безопаснее и гораздо проще искать через grep.
Как преобразовать int в double в C++?
Используйте static_cast<double>(x). Важнее всего это при делении: 5 / 2 — это целочисленное деление, дающее 2, а static_cast<double>(5) / 2 даёт 2.5. Приводите один из операндов до того, как произойдёт деление: приводить результат, static_cast<double>(5 / 2), уже поздно — всё равно получится 2.0.
Почему приведение большого значения к меньшему типу даёт неправильное число в C++?
Преобразование к типу, который не способен вместить значение, — это сужающее преобразование. При приведении float к int дробная часть отбрасывается (static_cast<int>(3.99) равно 3), а целое число вне диапазона либо переполняется по кольцу (беззнаковое), либо ведёт себя определённым реализацией образом (знаковое). Компилятор обычно вас не останавливает, поэтому приводите осознанно и убедитесь, что целевой тип достаточно широк.