dynamic_cast的适当用例是什么?

20

我听说过很多次(并在实践中也看到了),使用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
}

很明显,我们可以将动态转换(dynamic cast)改写成在Base中添加一个虚函数doStuff()并在Derived中重新实现它的方式来完成。

那么,我的问题是,为什么语言中还需要动态转换(dynamic_cast)?是否存在使用动态转换(dynamic_cast)合理的示例?

5个回答

31
虚函数的问题在于,继承层级中的所有必须有一个实现或者是抽象类,但这并不总是正确的做法。例如,如果Base是一个接口,而你需要在if语句中访问Derived的内部实现细节,这显然无法通过虚函数来实现。此外,在某些多重继承情况下,需要同时进行向上转型和向下转型,因此需要使用dynamic_cast。另外,虚函数的使用存在限制——例如,在模板中不能使用。最后,有时需要存储一个Derived*,而不仅仅是调用它的函数。

基本上,虚函数只适用于一些情况,而不是所有情况。


21

我认为有两种情况下使用dynamic_cast是一件合理的事情。第一种是检查一个对象是否支持一个接口,第二种是打破封装性。让我详细解释一下。

检查接口

考虑以下函数:

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

    if( transaction )
        transaction->Begin();

    obj->DoStuff();

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

在这个函数中,如果对象支持事务语义,我们希望在事务的上下文中执行“DoStuff”。如果不支持,则可以直接进行。

现在,我们肯定可以向Object类添加virtual 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导致了多次内存分配。更糟糕的是,事实证明“iterator”几乎总是一个“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;
   }
}

再次强调,我们可以向IIterator类添加一个虚拟的"CopyElements"方法,但这具有我上面提到的相同缺点。即它会使接口变得臃肿,并迫使所有实现者都拥有一个CopyElements方法,尽管只有ArrayIterator类才会在其中做出一些有趣的事情。

说了这么多,我建议谨慎使用这些技术。dynamic_cast不是免费的,也容易被滥用。(坦白地说,我看到它被滥用的次数比它被正确使用的次数多得多。)如果你发现自己频繁使用它,最好考虑其他方法。


我不确定我喜欢这个.. 在CopyElements中,IIterator应该有一个虚拟的ToVector或者类似的东西,可以用多态实现。 - paulm
5
是的,这是另一种方法。但我认为这会导致界面的污染。总的来说,我们想要保持界面小而内聚(接口隔离原则)。在现有的代码库中可能还需要做很多工作。像这样一个简单的示例中添加“CopyTo”方法可能看起来很有吸引力,但如果已经存在20个实现怎么办呢? - Peter Ruderman
1
@PeterRuderman 对于界面膨胀的观点非常有见地。现在人们很少考虑这个问题。 - rostamn739

8
很明显,我们可以通过在Base中添加虚函数doStuff()并在Derived中重新实现该函数来替代编写dynamic casts。
是的,这就是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;

你是否注意到了虚函数如何消除dynamic_cast

使用dynamic_cast通常表示您无法使用公共接口(即虚函数)实现目标,因此需要将其转换为确切的类型,以调用基类/派生类的特定成员函数。


0
子类可能有其他在基类中不存在的方法,而这些方法在其他子类的上下文中可能没有意义。但通常情况下应该避免这种情况。

0
如果你有一个方法(称之为foo),它接收BaseClass*,并且扩展为DerivedClass*。如果我写下以下代码:
BaseClass* x = new DerivedClass();

并且使用x调用foo时,我将到达foo(BaseClass varName),而不是foo(DerivedClass varName)。

一个解决方案是使用dynamic_cast并将其与NULL进行测试,如果它不是null,则使用转换后的var而不是x调用foo。

这不是最面向对象的情况,但它确实会发生,并且dynamic_cast可以帮助您解决它(嗯,总体来说,类型转换并不太面向对象)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接