dynamic_cast的正确用例是什么?

What is the proper use case for dynamic_cast?

本文关键字:是什么 cast dynamic      更新时间:2023-10-16

我多次被告知(并且在实践中亲眼目睹)使用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是合理的例子?

虚函数的问题在于层次结构中的所有必须有实现或者是抽象的,而这肯定不总是正确的事情。例如,如果Base是一个接口,并且在if中,您需要访问Derived的内部实现细节,该怎么办?这在虚拟函数中肯定是不可行的。此外,在某些多重继承情况下,上行和下行都需要dynamic_cast。在虚函数中可以做的事情是有限制的——例如,模板。最后,有时您需要存储Derived*,而不仅仅是在其上调用函数。

本质上,虚函数只在某些情况下起作用,而不是在所有情况下都起作用。

我认为有两种情况下使用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会导致多个内存分配。更糟糕的是,"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不是免费的,很容易被滥用。(坦率地说,我看到它被滥用的次数远远多于我看到它被很好地使用的次数。)如果您发现自己经常使用它,那么考虑其他方法是一个好主意。

可以很容易地看到,我们可以在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通常表明您不能使用公共接口(即函数)实现您的目标,因此您需要将其强制转换为精确类型,以便调用类型基类/派生类的特定成员函数。

子类可能有基类中没有的其他方法,这些方法在其他子类的上下文中可能没有意义。但一般来说,你应该避免使用它。

如果,你有一个方法(调用它foo),它接收BaseClass*,并为派生类*扩展。如果我写:

BaseClass* x = new DerivedClass();

和调用foo与x,我将得到foo (BaseClass varName),而不是foo (DerivedClass varName)。

一种解决方案是使用dynamic_cast并测试它是否为NULL,如果它不是NULL,则使用转换后的var而不是x调用foo。

这不是最面向对象的情况,但它确实发生了,dynamic_cast可以帮助您解决这个问题(好吧,一般来说,强制转换不太面向对象)。