Зачем использовать static_cast <int> (x) вместо (int) x?

667

Я слышал, что эта static_castфункция должна быть предпочтительнее C-стиля или простого преобразования в стиле функции. Это правда? Почему?

Томми Герберт
источник
36
Возражаю вашу честь, спрашиваю и отвечаю .
Грэм Перроу
25
Я не согласен, этот другой вопрос касался описания различий между приведениями в C ++. Этот вопрос о реальной полезности static_cast, который немного отличается.
Винсент Роберт
2
Конечно, мы могли бы объединить два вопроса, но что нам нужно сохранить в этом потоке, так это преимущество использования функций по сравнению с приведением в стиле C, которое в настоящее время упоминается только в однострочном ответе в другом потоке без голосования. ,
Томми Герберт
6
Этот вопрос касается «встроенных» типов, таких как int, тогда как этот вопрос касается типов классов. Это кажется достаточно существенной разницей, чтобы заслуживать отдельного объяснения.
9
На самом деле static_cast - это оператор, а не функция.
ThomasMcLeod

Ответы:

633

Основная причина заключается в том , что классические слепки C не делают никакого различия между тем, что мы называем static_cast<>(), reinterpret_cast<>(), const_cast<>()и dynamic_cast<>(). Эти четыре вещи совершенно разные.

А static_cast<>()обычно безопасно. Существует допустимое преобразование в языке или соответствующий конструктор, который делает это возможным. Единственный случай, когда это немного рискованно, это когда вы переходите в унаследованный класс; вы должны убедиться, что объект на самом деле является потомком, который, как вы утверждаете, является внешним по отношению к языку (например, флаг в объекте). A dynamic_cast<>()является безопасным, пока проверяется результат (указатель) или учитывается возможное исключение (ссылка).

А reinterpret_cast<>()(или const_cast<>()) с другой стороны всегда опасно. Вы говорите компилятору: «Поверьте мне: я знаю, что это не похоже на foo(это выглядит так, как будто оно не изменяемое), но это так».

Первая проблема заключается в том, что почти невозможно определить, какая из них будет происходить при приведении в стиле C, не глядя на большие и разбросанные фрагменты кода и не зная всех правил.

Давайте предположим это:

class CDerivedClass : public CMyBase {...};
class CMyOtherStuff {...} ;

CMyBase  *pSomething; // filled somewhere

Теперь эти два компилируются одинаково:

CDerivedClass *pMyObject;
pMyObject = static_cast<CDerivedClass*>(pSomething); // Safe; as long as we checked

pMyObject = (CDerivedClass*)(pSomething); // Same as static_cast<>
                                     // Safe; as long as we checked
                                     // but harder to read

Однако давайте посмотрим на этот почти идентичный код:

CMyOtherStuff *pOther;
pOther = static_cast<CMyOtherStuff*>(pSomething); // Compiler error: Can't convert

pOther = (CMyOtherStuff*)(pSomething);            // No compiler error.
                                                  // Same as reinterpret_cast<>
                                                  // and it's wrong!!!

Как видите, нет простого способа различить две ситуации, не зная много о всех участвующих классах.

Вторая проблема заключается в том, что броски в стиле C слишком сложно найти. В сложных выражениях может быть очень трудно увидеть броски в стиле C. Практически невозможно написать автоматический инструмент, который должен находить приведения в стиле C (например, инструмент поиска) без полноценного внешнего интерфейса компилятора C ++. С другой стороны, легко искать «static_cast <» или «reinterpret_cast <».

pOther = reinterpret_cast<CMyOtherStuff*>(pSomething);
      // No compiler error.
      // but the presence of a reinterpret_cast<> is 
      // like a Siren with Red Flashing Lights in your code.
      // The mere typing of it should cause you to feel VERY uncomfortable.

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

Евро Мицелли
источник
30
Вы не должны использовать static_castдля приведения иерархии наследования, а скорее dynamic_cast. Это вернет либо нулевой указатель, либо действительный указатель.
Дэвид Торнли
41
@ Дэвид Торнли: Я согласен, как правило. Я думаю, что указал предостережения для использования static_castв этой ситуации. dynamic_castможет быть безопаснее, но это не всегда лучший вариант. Иногда вы знаете, что указатель указывает на данный подтип, непрозрачный для компилятора, и static_castбыстрее. По крайней мере, в некоторых средах dynamic_castтребуется дополнительная поддержка компилятора и затраты времени выполнения (включение RTTI), и вы можете не захотеть включать его только для пары проверок, которые вы можете выполнить самостоятельно. RTTI в C ++ - это только одно из возможных решений проблемы.
Евро Мицелли
18
Ваше утверждение о C бросках является ложным. Все приведения C являются преобразованиями значений, примерно сопоставимыми с C ++ static_cast. Эквивалент C reinterpret_cast- это *(destination_type *)&, например, взятие адреса объекта, приведение этого адреса к указателю другого типа, а затем разыменование. За исключением случаев типов символов или определенных структурных типов, для которых C определяет поведение этой конструкции, это обычно приводит к неопределенному поведению в C.
R .. GitHub STOP HELPING ICE
11
Ваш прекрасный ответ обращается к основной части поста. Я искал ответ на заголовок "зачем использовать static_cast <int> (x) вместо (int) x". То есть, для типа intintотдельно), почему использование static_cast<int>vs. (int)в качестве единственного преимущества, кажется, с переменными класса и указателями. Просьба, чтобы вы уточнили это.
chux - Восстановить Монику
31
@chux, ибо int dynamic_castне применяется, но все остальные причины остаются в силе . Например: скажем v, это параметр функции, объявленный как float, то (int)vесть static_cast<int>(v). Но если вы измените параметр на float*, (int)vспокойно становится, reinterpret_cast<int>(v)пока static_cast<int>(v)незаконно и правильно отлавливается компилятором.
Евро Micelli
115

Один прагматичный совет: вы можете легко найти ключевое слово static_cast в исходном коде, если планируете привести в порядок проект.

Карл
источник
3
Вы также можете искать, используя скобки, такие как "(int)", но хороший ответ и веская причина использовать приведение в стиле C ++.
Майк
4
@Mike, который найдет ложные срабатывания - объявление функции с одним intпараметром.
Натан Осман
1
Это может дать ложные отрицания: если вы ищете кодовую базу, где вы не единственный автор, вы не найдете броски в стиле C, которые другие могли по каким-то причинам ввести.
Руслан
7
Как это поможет привести в порядок проект?
Bilow
Вы не будете искать static_cast, потому что он, скорее всего, правильный. Вы хотите отфильтровать static_cast при поиске reinterpret_cast, const_cast и, возможно, даже dynamic_cast, поскольку они будут указывать места, которые можно изменить. C-cast смешивает все вместе и не дает вам повода для кастинга.
Драган
78

Короче говоря :

  1. static_cast<>() дает вам возможность проверки времени компиляции, а C-Style - нет.
  2. static_cast<>()может быть легко обнаружен в любом месте исходного кода C ++; напротив, бросок C_Style труднее обнаружить.
  3. Намерения передаются намного лучше, используя приведения C ++.

Более объяснение :

Статическое приведение выполняет преобразования между совместимыми типами . Это похоже на приведение в стиле C, но более ограничено. Например, приведение в стиле C позволит целочисленный указатель указывать на символ.

char c = 10;       // 1 byte
int *p = (int*)&c; // 4 bytes

Так как это приводит к 4-байтовому указателю, указывающему на 1 байт выделенной памяти, запись в этот указатель либо вызовет ошибку времени выполнения, либо перезапишет некоторую смежную память.

*p = 5; // run-time error: stack corruption

В отличие от приведения типа C, статическое приведение позволит компилятору проверить совместимость типов данных указателя и указателя, что позволяет программисту уловить это неправильное назначение указателя во время компиляции.

int *q = static_cast<int*>(&c); // compile-time error

Подробнее о том, в
чем разница между static_cast <> и приведением в стиле C
и
обычным приведением против static_cast и dynamic_cast

Rika
источник
21
Я не согласен, что static_cast<>()более читабельно. Я имею в виду, иногда это так, но в большинстве случаев - особенно для базовых целочисленных типов - это просто ужасно и излишне многословно. Например: это функция, которая заменяет байты 32-битного слова. Было бы почти невозможно читать с использованием static_cast<uint##>()приведений, но это довольно легко понять с помощью (uint##)приведений. Изображение кода: imgur.com/NoHbGve
Тодд Леман,
3
@ToddLehman: Спасибо, но я тоже не сказал always. (но в большинстве случаев да) В некоторых случаях приведение стиля c более читабельно. Это одна из причин того, что кастинг в стиле c все еще жив и крут в c ++ imho. :) Кстати, это был очень хороший пример
Рика
8
Код @ToddLehman в этом изображении использует два приведения в цепочку ( (uint32_t)(uint8_t)) для достижения сброса байтов, кроме младших. Для этого есть побитовое и ( 0xFF &). Использование приведений скрывает намерение.
Öö Tiib
28

Вопрос больше, чем просто использование static_cast или приведения в стиле C, потому что при использовании приведения в стиле C происходят разные вещи. Операторы приведения в C ++ предназначены для того, чтобы сделать эти операции более явными.

На поверхности приведение к стилям static_cast и C выглядит одинаково, например, при приведении одного значения к другому:

int i;
double d = (double)i;                  //C-style cast
double d2 = static_cast<double>( i );  //C++ cast

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

class A {};
class B : public A {};

A* a = new B;
B* b = (B*)a;                                  //(1) what is this supposed to do?

char* c = (char*)new int( 5 );                 //(2) that weird?
char* c1 = static_cast<char*>( new int( 5 ) ); //(3) compile time error

В этом примере (1), возможно, все в порядке, потому что объект, на который указывает A, действительно является экземпляром B. Но что если вы не знаете в тот момент кода, на что в действительности указывает объект? (2) может быть совершенно законным (вы хотите посмотреть только один байт целого числа), но это также может быть ошибкой, и в этом случае ошибка будет хорошей, например (3). Операторы приведения в C ++ предназначены для выявления этих проблем в коде, предоставляя, по возможности, ошибки времени компиляции или выполнения.

Таким образом, для строгого «приведения значений» вы можете использовать static_cast. Если вы хотите полиморфное приведение указателей во время выполнения, используйте dynamic_cast. Если вы действительно хотите забыть о типах, вы можете использовать reintrepret_cast. И чтобы просто выкинуть const из окна есть const_cast.

Они просто делают код более явным, чтобы вы знали, что делаете.

Дасти Кэмпбелл
источник
26

static_castозначает, что вы не можете случайно const_castили reinterpret_cast, что хорошо.

DrPizza
источник
4
Дополнительные (хотя и довольно незначительные) преимущества по сравнению с кастом в стиле C заключаются в том, что он выделяется больше (делать что-то потенциально плохое должно выглядеть некрасиво) и более удобен.
Майкл Берр
4
grep-способность - это всегда плюс, в моей книге.
Бранан
7
  1. Позволяет легко найти приведения в вашем коде, используя grep или аналогичные инструменты.
  2. Делает ясным, какой тип приведения вы делаете, и привлекает помощь компилятора в его применении. Если вы хотите отбросить только константу, вы можете использовать const_cast, который не позволит вам делать другие типы преобразований.
  3. Приведения типов по своей природе отвратительны - вы, как программист, игнорируете то, как компилятор обычно будет обращаться с вашим кодом. Вы говорите компилятору: «Я знаю лучше, чем вы». В этом случае имеет смысл, что выполнение приведения должно быть умеренно болезненным, и что они должны выделяться в вашем коде, поскольку они являются вероятным источником проблем.

См. Эффективное введение C ++

JohnMcG
источник
Я полностью согласен с этим для классов, но имеет ли смысл использовать приведение стиля C ++ для типов POD?
Захари Краус
Я думаю так. Все 3 причины относятся к POD, и полезно иметь только одно правило, а не отдельные правила для классов и POD.
JohnMcG
Интересно, что мне, возможно, придется изменить способ приведения типов в будущем коде для типов POD.
Захария Крауса
7

Речь идет о том, какую безопасность типов вы хотите навязать.

Когда вы пишете (bar) foo(что эквивалентноreinterpret_cast<bar> foo что вы не указали оператор преобразования типа), вы говорите компилятору игнорировать безопасность типов и просто делаете, как было сказано.

Когда вы пишете, static_cast<bar> fooвы просите компилятор хотя бы проверить, имеет ли смысл преобразование типов и, для целочисленных типов, вставить некоторый код преобразования.


РЕДАКТИРОВАТЬ 2014-02-26

Я написал этот ответ более 5 лет назад, и я ошибся. (См. Комментарии.) Но он все равно получает голоса!

Pitarou
источник
8
(bar) foo не эквивалентно reinterpret_cast <bar> (foo). Правила для "(TYPE) expr" заключаются в том, что он выберет подходящий тип стиля C ++ для использования, который может включать reinterpret_cast.
Ричард Корден
Хорошая точка зрения. Евро Мичелли дал однозначный ответ на этот вопрос.
Питару
1
Кроме того, это static_cast<bar>(foo)с круглыми скобками. То же самое для reinterpret_cast<bar>(foo).
LF
6

Броски в стиле C легко пропустить в блоке кода. Приведение в стиле C ++ - это не только лучшая практика; они предлагают гораздо большую степень гибкости.

reinterpret_cast позволяет интегрировать преобразования типа указателя, однако может быть небезопасным при неправильном использовании.

static_cast предлагает хорошее преобразование для числовых типов, например, от перечислений до целых чисел или от целых чисел до чисел с плавающей точкой или любых типов данных, которые вы уверены в типе. Он не выполняет никаких проверок во время выполнения.

С другой стороны, dynamic_cast будет выполнять эти проверки, отмечая любые неоднозначные назначения или преобразования. Он работает только с указателями и ссылками и несет накладные расходы.

Есть несколько других, но это основные, с которыми вы столкнетесь.

Konrad
источник
4

static_cast, помимо манипулирования указателями на классы, также может использоваться для выполнения преобразований, явно определенных в классах, а также для выполнения стандартных преобразований между основными типами:

double d = 3.14159265;
int    i = static_cast<int>(d);
Пракаш
источник
4
Зачем кому-то писать static_cast<int>(d), хотя, когда (int)dон намного более лаконичен и читабелен? (Я имею в виду базовые типы, а не указатели на объекты.)
Тодд Леман,
@ gd1 - Почему кто-то ставит последовательность выше читабельности? (на самом деле наполовину серьезно)
Тодд Леман
2
@ToddLehman: Я, учитывая, что делать исключения для определенных типов только потому, что они для вас особенные, не имеет никакого смысла для меня, и я также не согласен с вашим представлением о читабельности. Короче не значит более читабельный, как я вижу из картинки, которую вы разместили в другом комментарии.
gd1
2
static_cast - это четкое и осознанное решение сделать очень специфический вид конверсии. Поэтому это добавляет ясности намерения. Это также очень удобно в качестве маркера для поиска исходных файлов для конверсий в обзоре кода, ошибке или обновлении.
Persixty
1
Контрапункт @ToddLehman: Зачем кому-то писать, (int)dкогда int{d}он намного лучше читается? ()Синтаксис конструктора или функционально-подобного, если у вас есть , не так быстро превратится в кошмарный лабиринт скобок в сложных выражениях. В этом случае это было бы int i{d}вместо int i = (int)d. Гораздо лучше ИМО. Тем не менее, когда мне просто нужно временное выражение, я использую static_castи никогда не использовал приведение конструктора, я не думаю. Я использую только (C)castsкогда поспешно пишу отладку coutс ...
underscore_d