Каков правильный вариант использования 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?


person Community    schedule 25.04.2011    source источник


Ответы (5)


Легко заметить, что вместо написания динамических приведений мы можем просто добавить в Base виртуальную функцию 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 обычно указывает на то, что вы не можете достичь своей цели, используя общий интерфейс (т.е. виртуальные функции), поэтому вам необходимо привести его к точному типу, чтобы вызвать определенные функции-члены базового/производного типа. классы.

person Nawaz    schedule 25.04.2011

Проблема с виртуальными функциями заключается в том, что все классы в иерархии должны иметь реализацию или быть абстрактными, а это определенно не всегда правильно. Например, что, если Base — это интерфейс, а в if вам нужно получить доступ к деталям внутренней реализации Derived? Это, конечно, невозможно в виртуальной функции. Кроме того, dynamic_cast необходим как для повышения, так и для понижения приведения в определенных ситуациях множественного наследования. И есть ограничения на то, что можно делать в виртуальных функциях, например, в шаблонах. И, наконец, иногда вам нужно сохранить Derived*, а не просто вызвать для него функцию.

По сути, виртуальные функции работают только в некоторых случаях, а не всех.

person Puppy    schedule 25.04.2011

Я думаю, что есть два случая, когда использование dynamic_cast допустимо. Первый — проверить, поддерживает ли объект интерфейс, а второй — нарушить инкапсуляцию. Позвольте мне объяснить оба подробно.

Проверка интерфейса

Рассмотрим следующую функцию:

void DoStuffToObject( Object * obj )
{
    ITransactionControl * transaction = dynamic_cast<ITransactionControl>( obj );

    if( transaction )
        transaction->Begin();

    obj->DoStuff();

    if( transaction )
        transaction->Commit();
}

(ITransactionControl был бы чистым абстрактным классом.) В этой функции мы хотим «DoStuff» в контексте транзакции, если объект поддерживает семантику транзакции. Если это не так, все равно можно просто идти вперед.

Конечно, мы могли бы просто добавить в класс Object виртуальные методы Begin() и Commit(), но тогда каждый класс, производный от 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 приводят к множественному выделению памяти. Хуже того, оказывается, что «итератор» почти всегда является «Итератором массива». Если бы вы только могли сделать такое предположение, то ваши проблемы с производительностью исчезли бы. С 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 не бесплатен и открыт для злоупотреблений. (И, честно говоря, я видел, что им злоупотребляют гораздо чаще, чем хорошо его используют.) Если вы обнаружите, что часто его используете, неплохо рассмотреть другие подходы.

person Peter Ruderman    schedule 28.11.2012
comment
Не уверен, что я фанат этого. Мне кажется, что в CopyElements IIterator должен иметь виртуальный ToVector или что-то, что делает это полиморфно. - person paulm; 12.10.2014
comment
Да, это другой подход. Но я бы сказал, что это приводит к загрязнению ваших интерфейсов. В общем, мы хотим, чтобы интерфейсы были небольшими и связными (принцип разделения интерфейсов). Это также может потребовать довольно много работы в существующей кодовой базе. Добавление метода CopyTo может показаться привлекательным для такого тривиального примера, но что, если существует 20 существующих реализаций? - person Peter Ruderman; 14.10.2014
comment
@PeterRuderman Довольно серьезное замечание о раздувании интерфейса. Люди редко думают об этом в наши дни - person rostamn739; 10.06.2016

Подкласс может иметь другие методы, отсутствующие в базовом классе, и это может не иметь смысла в контексте других подклассов. Но вообще вы должны избегать этого.

person robert    schedule 25.04.2011

Что, если у вас есть метод (назовем его foo), который получает BaseClass* и использует его для DerivedClass*. Если я напишу:

BaseClass* x = new DerivedClass();

и вызовите foo с x, я получу foo (BaseClass varName), а не foo (DerivedClass varName).

Одно из решений состоит в том, чтобы использовать dynamic_cast и проверить его на соответствие NULL, а если он не нулевой, вызвать foo с приведенным var, а не x.

Это не самая объектно-ориентированная ситуация, но бывает, и dynamic_cast может вам в этом помочь (ну, кастинг вообще не слишком объектно-ориентирован).

person Neowizard    schedule 25.04.2011