Каков правильный вариант использования dynamic_cast?
Мне много раз говорили (и видели на практике), что использование dynamic_cast часто означает плохой дизайн, потому что его можно и нужно заменить виртуальными функциями.
Например, рассмотрим следующий код:
class Base{...};
class Derived:public Base{...};
...
Base* createSomeObject(); // Might create a Derived object
...
Base* obj = createSomeObject();
if(dynamic_cast<Derived*>(obj)){
// do stuff in one way
}
else{
// do stuff in some other way
}
Нетрудно заметить, что вместо написания динамических бросков мы можем просто добавить виртуальную функцию doStuff()
в Base
и повторно реализовать ее в Derived
.
В этом случае, мой вопрос: почему у нас есть dynamic_cast на языке вообще? Есть ли пример, в котором использование dynamic_cast оправдано?
Ответы
Ответ 1
Нетрудно заметить, что вместо написания динамических бросков мы можем просто добавить виртуальную функцию doStuff() в базу и повторно реализовать ее в Derived.
ДА. Вот для чего предназначены функции virtual
.
class Base
{
public:
virtual void doStuff();
};
class Derived: public Base
{
public:
virtual void doStuff(); //override base implementation
};
Base* createSomeObject(); // Might create a Derived object
Base* obj = createSomeObject();
obj->doStuff(); //might call Base::doStuff() or Derived::doStuff(), depending on the dynamic type of obj;
Вы заметили, что функция virtual
исключает dynamic_cast
?
Использование dynamic_cast
обычно указывает на то, что вы не можете достичь своей цели, используя общий интерфейс (то есть виртуальные функции), поэтому вам нужно указать его на точный тип, чтобы вызвать конкретные функции-члены классов типа/производных классов.
Ответ 2
Проблема с виртуальными функциями заключается в том, что все классы в иерархии должны иметь реализацию или быть абстрактными, и это определенно не всегда правильно. Например, что, если Base
является интерфейсом, а в if, вам нужно получить доступ к внутренним деталям реализации Derived
? Это, безусловно, не выполнимо в виртуальной функции. Кроме того, dynamic_cast
необходим как для повышения, так и для понижения в определенных ситуациях множественного наследования. И есть ограничения относительно того, что можно сделать в виртуальных функциях - например, шаблонах. И, наконец, иногда вам нужно сохранить Derived*
, а не просто вызвать функцию на нем.
По сути, виртуальные функции работают только в некоторых случаях, а не во всех из них.
Ответ 3
Я думаю, что есть два случая, когда использование dynamic_cast является правильной вещью. Первый - проверить, поддерживает ли объект интерфейс, а второй - разбить инкапсуляцию. Позвольте мне объяснить как подробно.
Проверка интерфейса
Рассмотрим следующую функцию:
void DoStuffToObject( Object * obj )
{
ITransactionControl * transaction = dynamic_cast<ITransactionControl>( obj );
if( transaction )
transaction->Begin();
obj->DoStuff();
if( transaction )
transaction->Commit();
}
(ITransactionControl будет чистым абстрактным классом.) В этой функции мы хотим "DoStuff" в контексте транзакции, если объект поддерживает семантику транзакций. Если это не так, все равно просто идти вперед.
Теперь мы, конечно, можем просто добавить виртуальные методы Begin() и Commit() к классу Object, но тогда каждый класс, полученный из Object, получает методы Begin() и Commit(), даже если они не осведомлены о транзакциях. Использование виртуальных методов в базовом классе просто загрязняет его интерфейс в этом случае. Приведенный выше пример способствует более строгому соблюдению принципа единой ответственности и принципа разделения сегрегации.
Нарушение инкапсуляции
Это может показаться странным советом, учитывая, что dynamic_cast обычно считается вредным, потому что он позволяет разрывать инкапсуляцию. Однако, сделано правильно, это может быть совершенно безопасный и мощный метод. Рассмотрим следующую функцию:
std::vector<int> CopyElements( IIterator * iterator )
{
std::vector<int> result;
while( iterator->MoveNext() )
result.push_back( iterator->GetCurrent() );
return result;
}
Здесь нет ничего плохого. Но теперь предположите, что вы начинаете видеть проблемы с производительностью в поле. После анализа вы обнаружите, что ваша программа проводит много времени внутри этой функции. Push_backs приводит к множественным распределениям памяти. Хуже того, оказывается, что "итератор" почти всегда является "ArrayIterator". Если бы вы смогли сделать это предположение, ваши проблемы с производительностью исчезли бы. С dynamic_cast вы можете сделать именно это:
std::vector<int> CopyElements( IIterator * iterator )
{
ArrayIterator * arrayIterator = dynamic_cast<ArrayIterator *>( iterator );
if( arrayIterator ) {
return std::vector<int>( arrayIterator->Begin(), arrayIterator->End() );
} else {
std::vector<int> result;
while( iterator->MoveNext() )
result.push_back( iterator->GetCurrent() );
return result;
}
}
Еще раз, мы могли бы добавить виртуальный метод "CopyElements" в класс IIterator, но это имеет те же недостатки, о которых я говорил выше. А именно, он раздувает интерфейс. Это заставляет всех конструкторов иметь метод CopyElements, хотя ArrayIterator - единственный класс, который сделает что-то интересное в нем.
Все сказанное, я рекомендую использовать эти методы экономно. dynamic_cast не является бесплатным и доступен для злоупотреблений. (И, честно говоря, я видел, как он злоупотреблял гораздо чаще, чем я видел, что он хорошо использовался.) Если вы слишком много используете его, рекомендуется рассмотреть другие подходы.
Ответ 4
Подкласс может иметь другие методы, отсутствующие в базовом классе, и это может не иметь смысла в контексте других подклассов. Но в целом вы должны избегать этого.
Ответ 5
Что делать, если у вас есть метод (назовите его foo), который получает BaseClass *, и он израсходован на DerivedClass *.
Если я напишу:
BaseClass* x = new DerivedClass();
и вызовите foo с помощью x, я получу foo (BaseClass varName), а не foo (DerivedClass varName).
Одно из решений - использовать dynamic_cast и проверять его на NULL, а если оно не равно null, вызовите foo с casted var, а не x.
Это не самая объектно-ориентированная ситуация, но это происходит, и dynamic_cast может помочь вам с ней (ну, кастинг вообще не слишком объектно-ориентированный).