Каков правильный вариант использования для dynamic_cast? - PullRequest
16 голосов
/ 25 апреля 2011

Мне много раз говорили (и я видел себя на практике), что использование 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 оправдано?

Ответы [ 5 ]

26 голосов
/ 25 апреля 2011

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

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

17 голосов
/ 28 ноября 2012

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

6 голосов
/ 25 апреля 2011

Легко видеть, что вместо записи динамических приведений мы можем просто добавить виртуальную функцию doStuff () в Base и повторно реализовать ее в Derived.

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

0 голосов
/ 25 апреля 2011

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

BaseClass* x = new DerivedClass();

и вызову foo с помощью x, я попаду в foo (BaseClass varName), а не в foo (DerivedClass varName).

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

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

0 голосов
/ 25 апреля 2011

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

...