Используйте `dynamic_cast` там, где навигация по иерархии классов неизбежна
Причина
dynamic_cast проверяется во время выполнения.
Пример
struct B { // интерфейс
virtual void f();
virtual void g();
virtual ~B();
};
struct D : B { // более широкий интерфейс
void f() override;
virtual void h();
};
void user(B* pb)
{
if (D* pd = dynamic_cast<D*>(pb)) {
// ... используем интерфейс D ...
}
else {
// ... обходимся интерфейсом B ...
}
}
Использование других приведений может нарушить типобезопасность и привести к обращению к переменной типа X как к переменной несвязанного типа Z:
void user2(B* pb) // плохо
{
D* pd = static_cast<D*>(pb); // знаю, что pb реально указывает на D; доверяйте мне
// ... используем интерфейс D ...
}
void user3(B* pb) // небезопасно
{
if (some_condition) {
D* pd = static_cast<D*>(pb); // знаю, что pb реально указывает на D; доверяйте мне
// ... используем интерфейс D ...
}
else {
// ... обходимся интерфейсом B ...
}
}
void f()
{
B b;
user(&b); // OK
user2(&b); // ошибка
user3(&b); // OK *если* программист правильно проверил some_condition
}
Примечание
Как и другие приведения, dynamic_cast используется слишком часто. Предпочитайте виртуальные функции приведениям. Предпочитайте статический полиморфизм навигации по иерархии там, где это возможно (не требуется разрешение во время выполнения) и удобно.
Примечание
Некоторые используют dynamic_cast там, где более уместным был бы typeid; dynamic_cast — это общая операция «является ли разновидностью» для нахождения лучшего интерфейса к объекту, тогда как typeid — это операция «дайте мне точный тип этого объекта» для нахождения фактического типа объекта. Последнее — принципиально более простая операция, которая должна быть быстрее. Последнее (typeid) легко реализовать вручную при необходимости (например, при работе в системе, где RTTI по какой-то причине запрещён), тогда как первое (dynamic_cast) гораздо сложнее реализовать корректно в общем случае.
Рассмотрим:
struct B {
const char* name {"B"};
// если pb1->id() == pb2->id(), *pb1 имеет тот же тип, что *pb2
virtual const char* id() const { return name; }
// ...
};
struct D : B {
const char* name {"D"};
const char* id() const override { return name; }
// ...
};
void use()
{
B* pb1 = new B;
B* pb2 = new D;
cout << pb1->id(); // "B"
cout << pb2->id(); // "D"
if (pb2->id() == "D") { // выглядит безобидно
D* pd = static_cast<D*>(pb2);
// ...
}
// ...
}
Результат pb2->id() == "D" на самом деле определяется реализацией. Мы добавили это для предупреждения об опасностях самодельного RTTI. Этот код может работать как ожидается годами, а затем выйти из строя на новой машине, новом компиляторе или новом компоновщике, который не объединяет строковые литералы.
Если вы реализуете собственный RTTI, будьте осторожны.
Исключение
Если ваша реализация предоставляет очень медленный dynamic_cast, вам, возможно, придётся использовать обходной путь. Однако все обходные пути, которые не могут быть разрешены статически, включают явное приведение (обычно static_cast) и подвержены ошибкам. По сути, вы будете создавать собственный специализированный dynamic_cast. Поэтому сначала убедитесь, что ваш dynamic_cast действительно медленный (существует немало неподтверждённых слухов) и что ваше использование dynamic_cast действительно критично для производительности.
Мы считаем, что текущие реализации dynamic_cast излишне медленны. Например, при подходящих условиях возможно выполнение dynamic_cast за быстрое константное время. Однако совместимость затрудняет изменения, даже если все согласны с целесообразностью оптимизации.
В очень редких случаях, если вы измерили, что накладные расходы dynamic_cast существенны, у вас есть другие способы статически гарантировать успешность downcasting (например, вы аккуратно используете CRTP) и не задействовано виртуальное наследование — рассмотрите тактическое использование static_cast с видным комментарием и отказом от ответственности, резюмирующим этот абзац и указывающим, что при сопровождении требуется внимание человека, поскольку система типов не может проверить корректность. Тем не менее, по нашему опыту, такие ситуации «я знаю, что делаю» всё равно являются известным источником ошибок.
Исключение
Рассмотрим:
template<typename B>
class Dx : B {
// ...
};
Контроль
- Помечать все использования
static_castдля downcasting, включая C-style приведения, выполняющиеstatic_cast. - Это правило является частью профиля типобезопасности.