Почему стоит использовать static_cast<T>(x) вместо (T)x?
Я слышал, что функцию static_cast
следует предпочитать кастингу в стиле C или простому вызову функции для приведения типов. Это правда? Почему?
5 ответ(ов)
Основная причина заключается в том, что классические приведения типов в C не делают различия между тем, что мы называем static_cast<>()
, reinterpret_cast<>()
, const_cast<>()
и dynamic_cast<>()
. Эти четыре операции совершенно разные.
static_cast<>()
обычно безопасен. В языке существует допустимое преобразование, или имеется соответствующий конструктор, который это позволяет. Единственный случай, когда это может быть рискованно — это если вы приводите к производному классу; нужно убедиться, что объект действительно является тем потомком, который вы утверждаете, используя методы, не зависящие от языка (например, флаг в объекте). dynamic_cast<>()
безопасен, если результат проверяется (указатель) или учитывается возможное исключение (ссылка).
С другой стороны, reinterpret_cast<>()
(или const_cast<>()
) всегда небезопасны. Вы сообщаете компилятору: "доверяй мне: я знаю, что это не выглядит как foo
(это выглядит как что-то неизменяемое), но это так".
Первая проблема в том, что почти невозможно понять, какое преобразование произойдет при касте в стиле C, не просматривая большие и разрозненные куски кода и не зная все правила.
Предположим, у нас есть следующий код:
class CDerivedClass : public CMyBase {...};
class CMyOtherStuff {...};
CMyBase *pSomething; // заполнено где-то
Теперь эти две строки компилируются одинаково:
CDerivedClass *pMyObject;
pMyObject = static_cast<CDerivedClass*>(pSomething); // Безопасно; если мы проверили
pMyObject = (CDerivedClass*)(pSomething); // То же, что и static_cast<>
// Безопасно; если мы проверили
// но труднее для чтения
Однако давайте рассмотрим почти идентичный код:
CMyOtherStuff *pOther;
pOther = static_cast<CMyOtherStuff*>(pSomething); // Ошибка компиляции: Нельзя преобразовать
pOther = (CMyOtherStuff*)(pSomething); // Никакой ошибки компиляции.
// То же самое, что и reinterpret_cast<>
// и это неверно!!!
Как вы можете видеть, нет простого способа различить эти две ситуации, не зная много о всех вовлеченных классах.
Вторая проблема заключается в том, что найти приведения в стиле C слишком сложно. В сложных выражениях может быть очень трудно увидеть приведения в стиле C. Практически невозможно написать автоматизированный инструмент, который должен находить приведения в стиле C (например, инструмент для поиска), не имея полного компилятора C++ в качестве фронтенда. С другой стороны, легко искать "static_cast<" или "reinterpret_cast<".
pOther = reinterpret_cast<CMyOtherStuff*>(pSomething);
// Никакой ошибки компиляции.
// Но наличие reinterpret_cast<>
// похоже на сирену с мигающими красными огнями в вашем коде.
// Само его введение должно вызывать у вас ОЧЕНЬ сильное беспокойство.
Это означает, что не только приведения в стиле C более опасны, но и гораздо труднее найти их все, чтобы убедиться в их корректности.
Один практичный совет: вы можете легко искать ключевое слово static_cast
в вашем исходном коде, если планируете упорядочить проект.
Вопрос о том, когда использовать static_cast<>
или С-стиль приведения типов, не такой простой, как кажется на первый взгляд. Существуют различные ситуации, в которых С-стиль приводит не к тем же результатам, что и C-операторы приведения типов. Операторы приведения в C созданы для того, чтобы сделать различные операции более явными.
На первый взгляд, static_cast<>
и С-стиль приведения выглядят одинаково, например, когда мы приводим одно значение к другому:
int i;
double d = (double)i; // С-стиль приведения
double d2 = static_cast<double>(i); // C++ приведение
Оба варианта приводят целочисленное значение к типу double. Однако при работе с указателями ситуация усложняется. Рассмотрим несколько примеров:
class A {};
class B : public A {};
A* a = new B;
B* b = (B*)a; //(1) что здесь происходит?
char* c = (char*)new int(5); //(2) это нормально?
char* c1 = static_cast<char*>(new int(5)); //(3) ошибка компиляции
В данном примере (1) может сработать, если объект, на который указывает a
, действительно является экземпляром класса B
. Но что, если в этот момент кода вы не уверены, на что указывает a
?
(2) может быть абсолютно законным (если вам нужно посмотреть только на один байт целого числа), но это также может быть ошибкой, и в таком случае было бы неплохо получить сообщение об ошибке, как в (3).
Операторы приведения в C++ призваны выявлять такие ошибки в коде, предоставляя сообщения об ошибках во время компиляции или выполнения, когда это возможно.
Таким образом, для строгого "приведения значений" вы можете использовать static_cast<>
. Если вам нужно провести полиморфное приведение указателей во время выполнения, используйте dynamic_cast<>
. Если вы действительно хотите забыть о типах, воспользуйтесь reinterpret_cast<>
. А чтобы просто игнорировать const
, существует const_cast<>
.
Эти операторы делают код более ясным, так что он будет выглядеть так, будто вы точно знаете, что делаете.
static_cast
означает, что вы не сможете случайно использовать const_cast
или reinterpret_cast
, что является положительным моментом.
C-стиль приведения типов может быть легко упущен в блоке кода. В то время как C++-стиль приведения является более предпочтительным, он также обеспечивает гораздо большую гибкость.
reinterpret_cast
позволяет выполнять преобразования между целочисленными и указательными типами, но может быть небезопасным при неправильном использовании.
static_cast
предлагает хорошее преобразование для числовых типов, например, от перечислений к целым числам или от целых к вещественным числам, а также для любых типов данных, в которых вы уверены в их типе. Он не выполняет никаких проверок во время выполнения.
В отличие от этого, dynamic_cast
выполняет проверки и сигнализирует о любых неоднозначных присвоениях или преобразованиях. Он работает только с указателями и ссылками и имеет дополнительные затраты на производительность.
Существуют и другие, но это основные, с которыми вы столкнётесь.
Сравнение регулярного приведения типов, static_cast и dynamic_cast
Когда следует использовать reinterpret_cast?
Что такое Правило трёх?
Разница между const int*, const int * const и int * const?
Имеют ли круглые скобки после имени типа значение при использовании new?