Обычное приведение против static_cast против dynamic_cast
Я пишу код на C и C++ почти двадцать лет, но есть один аспект этих языков, который я никогда не понимал. Я, очевидно, использовал обычные приведения, т.е.
MyClass *m = (MyClass *)ptr;
повсюду, но, кажется, есть два других типа приведения, и я не знаю разницы. В чем разница между следующими строками кода?
MyClass *m = (MyClass *)ptr;
MyClass *m = static_cast<MyClass *>(ptr);
MyClass *m = dynamic_cast<MyClass *>(ptr);
8 ответов
static_cast
static_cast
используется в тех случаях, когда вы в основном хотите отменить неявное преобразование с некоторыми ограничениями и дополнениями. static_cast
не выполняет проверки во время выполнения. Это следует использовать, если вы знаете, что ссылаетесь на объект определенного типа, и, следовательно, проверка не требуется. Пример:
void func(void *data) {
// Conversion from MyClass* -> void* is implicit
MyClass *c = static_cast<MyClass*>(data);
...
}
int main() {
MyClass c;
start_thread(&func, &c) // func(&c) will be called
.join();
}
В этом примере вы знаете, что вы прошли MyClass
объект, и, следовательно, нет необходимости в проверке во время выполнения, чтобы гарантировать это.
dynamic_cast
dynamic_cast
полезно, когда вы не знаете, что такое динамический тип объекта. Он возвращает нулевой указатель, если указанный объект не содержит тип, приведенный к базовому классу (когда вы приводите ссылку, bad_cast
в этом случае исключение).
if (JumpStm *j = dynamic_cast<JumpStm*>(&stm)) {
...
} else if (ExprStm *e = dynamic_cast<ExprStm*>(&stm)) {
...
}
Вы не можете использовать dynamic_cast
если вы понижены (приведен к производному классу) и тип аргумента не является полиморфным. Например, следующий код недопустим, потому что Base
не содержит виртуальных функций:
struct Base { };
struct Derived : Base { };
int main() {
Derived d; Base *b = &d;
dynamic_cast<Derived*>(b); // Invalid
}
"Преобразование" (приведение к базовому классу) всегда допустимо для обоих static_cast
а также dynamic_cast
, а также без каких-либо приведений, поскольку "приведение" является неявным преобразованием.
Обычный актерский состав
Эти броски также называются бросками в стиле C. Приведение в стиле C в основном идентично испытанию ряда последовательностей приведения C++ и принятию первого приведения C++, который работает, даже не задумываясь dynamic_cast
, Излишне говорить, что это гораздо более мощный, так как объединяет все const_cast
, static_cast
а также reinterpret_cast
, но это также небезопасно, потому что не использует dynamic_cast
,
Кроме того, приведения в стиле C не только позволяют вам делать это, но и позволяют безопасно переходить в частный базовый класс, в то время как "эквивалент" static_cast
последовательность даст вам ошибку во время компиляции для этого.
Некоторые люди предпочитают броски в стиле C из-за их краткости. Я использую их только для числовых приведений и использую соответствующие преобразования 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
Переосмыслить приведение
Чтобы форсировать преобразование указателя, так же, как это делает приведение в стиле C в фоновом режиме, вместо этого будет использоваться приведение к новой интерпретации.
int *r = reinterpret_cast<int*>(&c); // forced conversion
Это приведение обрабатывает преобразования между определенными несвязанными типами, такими как из одного типа указателя в другой несовместимый тип указателя. Он просто выполнит двоичную копию данных без изменения базового битового шаблона. Обратите внимание, что результат такой низкоуровневой операции зависит от системы и поэтому не переносим. Следует использовать с осторожностью, если его нельзя избежать вообще.
Динамический состав
Этот используется только для преобразования указателей объектов и ссылок на объекты в другие указатели или ссылочные типы в иерархии наследования. Это единственное приведение, которое гарантирует, что указанный объект может быть преобразован путем выполнения проверки во время выполнения, что указатель ссылается на законченный объект целевого типа. Чтобы эта проверка во время выполнения была возможной, объект должен быть полиморфным. То есть класс должен определять или наследовать хотя бы одну виртуальную функцию. Это потому, что компилятор будет генерировать только необходимую информацию о типе времени выполнения для таких объектов.
Примеры динамического приведения
В приведенном ниже примере указатель MyChild преобразуется в указатель MyBase с использованием динамического приведения. Это преобразование из производной в базовую успешно выполнено, поскольку дочерний объект включает в себя полный базовый объект.
class MyBase
{
public:
virtual void test() {}
};
class MyChild : public MyBase {};
int main()
{
MyChild *child = new MyChild();
MyBase *base = dynamic_cast<MyBase*>(child); // ok
}
Следующий пример пытается преобразовать указатель MyBase в указатель MyChild. Поскольку базовый объект не содержит завершенного дочернего объекта, преобразование указателя завершится неудачно. Чтобы указать это, динамическое приведение возвращает нулевой указатель. Это дает удобный способ проверить, успешно ли выполнено преобразование во время выполнения.
MyBase *base = new MyBase();
MyChild *child = dynamic_cast<MyChild*>(base);
if (child == 0)
std::cout << "Null pointer returned";
Если вместо указателя преобразуется ссылка, динамическое приведение завершится неудачей, вызвав исключение bad_cast. Это должно быть обработано с помощью оператора try-catch.
#include <exception>
// …
try
{
MyChild &child = dynamic_cast<MyChild&>(*base);
}
catch(std::bad_cast &e)
{
std::cout << e.what(); // bad dynamic_cast
}
Динамическое или статическое приведение
Преимущество использования динамического приведения заключается в том, что он позволяет программисту проверять, успешно ли выполнено преобразование во время выполнения. Недостаток заключается в том, что при выполнении этой проверки возникают накладные расходы. По этой причине использование статического приведения было бы предпочтительнее в первом примере, потому что преобразование из производной в основную никогда не завершится неудачей.
MyBase *base = static_cast<MyBase*>(child); // ok
Однако во втором примере преобразование может быть либо успешным, либо неудачным. Он потерпит неудачу, если объект MyBase содержит экземпляр MyBase, и будет успешным, если он содержит экземпляр MyChild. В некоторых ситуациях это может быть неизвестно до времени выполнения. В этом случае динамическое приведение является лучшим выбором, чем статическое приведение.
// Succeeds for a MyChild object
MyChild *child = dynamic_cast<MyChild*>(base);
Если бы преобразование базы в производное было выполнено с использованием статического приведения вместо динамического приведения, преобразование не завершилось бы неудачей. Это возвратило бы указатель, который ссылался на неполный объект. Разыменование такого указателя может привести к ошибкам во время выполнения.
// Allowed, but invalid
MyChild *child = static_cast<MyChild*>(base);
// Incomplete MyChild object dereferenced
(*child);
Const Cast
Этот в основном используется для добавления или удаления модификатора const переменной.
const int myConst = 5;
int *nonConst = const_cast<int*>(&myConst); // removes const
Хотя метод const позволяет изменять значение константы, это все равно является недействительным кодом, который может вызвать ошибку во время выполнения. Это может произойти, например, если константа находится в разделе только для чтения.
*nonConst = 10; // potential run-time error
Вместо этого используется константное приведение, главным образом, когда есть функция, которая принимает аргумент неконстантного указателя, даже если она не изменяет объект-указатель.
void print(int *p)
{
std::cout << *p;
}
Затем функции можно передать постоянную переменную, используя константное приведение.
print(&myConst); // error: cannot convert
// const int* to int*
print(nonConst); // allowed
Вы должны взглянуть на статью C++ Программирование / Приведение типов.
Он содержит хорошее описание всех типов бросков. Следующее взято по вышеуказанной ссылке:
const_cast
const_cast (expression) const_cast<>() используется для добавления / удаления const(ness) (или volatile-ness) переменной.
static_cast
static_cast (expression) static_cast<>() используется для приведения между целочисленными типами. например, char->long, int->short и т. д.
Статическое приведение также используется для приведения указателей к связанным типам, например приведение void* к соответствующему типу.
dynamic_cast
Динамическое приведение используется для преобразования указателей и ссылок во время выполнения, как правило, с целью преобразования указателя или ссылки вверх или вниз по цепочке наследования (иерархия наследования).
dynamic_cast (выражение)
Тип назначения должен быть указателем или ссылочным типом, а выражение должно указывать на указатель или ссылку. Динамическое приведение работает только тогда, когда тип объекта, на который ссылается выражение, совместим с целевым типом, а базовый класс имеет хотя бы одну виртуальную функцию-член. Если нет, и типом выражения, являющегося приведенным, является указатель, возвращается значение NULL, если при динамическом приведении к ссылке происходит сбой, генерируется исключение bad_cast. Когда это не дает сбоя, динамическое приведение возвращает указатель или ссылку целевого типа на объект, на который ссылается выражение.
reinterpret_cast
Переинтерпретация приведения просто приводит один тип поразрядно к другому. Любой указатель или целочисленный тип может быть приведен к любому другому с помощью переинтерпретации, что позволяет легко использовать его неправильно. Например, при переинтерпретации приведении можно небезопасно привести целочисленный указатель к указателю строки.
К вашему сведению, я считаю, что Бьярн Страуструп говорит, что следует избегать приведений в стиле C и что вы должны использовать static_cast или dynamic_cast, если это вообще возможно.
Часто задаваемые вопросы о стиле C++ Барна Страуструпа
Возьми этот совет за то, что ты будешь. Я далеко не гуру C++.
Избегайте использования бросков C-Style.
Приведения в стиле C представляют собой смесь константных и переосмысленных приведений, и их трудно найти и заменить в вашем коде. Программист приложений C++ должен избегать приведения в стиле C.
Приведения в стиле C объединяют const_cast, static_cast и reinterpret_cast.
Я бы хотел, чтобы в C++ не было приведений в стиле C. Приведения типов в C++ выделяются должным образом (как и должно быть; приведения типов обычно указывают на то, что они делают что-то плохое) и должным образом различают различные виды преобразования, выполняемые приведениями. Они также позволяют писать похожие функции, например boost::lexical_cast, что довольно неплохо с точки зрения согласованности.
dynamic_cast
имеет проверку типов во время выполнения и работает только со ссылками и указателями, тогда как static_cast
не предлагает проверку типов во время выполнения. Для получения полной информации см. Статью MSDN Оператор static_cast.
dynamic_cast
поддерживает только указатель и ссылочные типы. Возвращается NULL
если приведение невозможно, если тип является указателем или выдает исключение, если тип является ссылочным типом. Следовательно, dynamic_cast
может использоваться для проверки того, принадлежит ли объект заданному типу, static_cast
не может (вы просто получите неверное значение).
С-образные (и другие) приведения были рассмотрены в других ответах.