为什么C风格的演员表现与dynamic_cast不同?

时间:2013-08-03 18:50:42

标签: c++ inheritance casting polymorphism

我有以下类层次结构:

class IControl
{
    virtual void SomeMethod() = 0; // Just to make IControl polymorphic.
};

class ControlBase
{
public:
    virtual int GetType() = 0;
};

class ControlImpl : public ControlBase, public IControl
{
public:
    virtual void SomeMethod() { }

    virtual int GetType()
    {
        return 1;
    }
};

我有一个 IControl 抽象类和一个 ControlBase 类。 ControlBase类不从IControl继承,但我知道每个IControl实现都将从ControlBase派生。

我有以下测试代码,我使用 dynamic_cast IControl - 参考投射到 ControlBase (因为我知道它来源于它) >,以及 C风格演员:

int main()
{
    ControlImpl stb;
    IControl& control = stb;

    ControlBase& testCB1 = dynamic_cast<ControlBase&>(control);
    ControlBase& testCB2 = (ControlBase&)control;
    ControlBase* testCB3 = (ControlBase*)&control;

    std::cout << &testCB1 << std::endl;
    std::cout << &testCB2 << std::endl;
    std::cout << testCB3 << std::endl;
    std::cout << std::endl;
    std::cout << testCB1.GetType() << std::endl; // This properly prints "1".
    std::cout << testCB2.GetType() << std::endl; // This prints some random number.
    std::cout << testCB3->GetType() << std::endl; // This prints some random number.
}

只有dynamic_cast正常工作,其他两个强制转换才会返回略有不同的内存地址,GetType()函数会返回不正确的值。

这是什么原因? C风格演员最终是否使用 reinterpret_cast ?它与多态对象在内存中的对齐方式有关吗?

2 个答案:

答案 0 :(得分:8)

我认为你的例子中的类名有点令人困惑。我们称他们为InterfaceBaseImpl。请注意,InterfaceBase 无关

C ++标准定义了C风格的强制转换,在[expr.cast]中称为“显式类型转换(强制转换符号)”。您可以(也许应该)阅读整个段落,以确切了解C样式转换的定义方式。对于OP中的示例,以下内容就足够了:

C风格可以执行[expr.cast] / 4之一的转换:

  • const_cast
  • static_cast
  • static_cast后跟const_cast
  • reinterpret_cast
  • reinterpret_cast后跟const_cast

此列表的顺序很重要,因为:

  

如果转换可以用上面列出的多种方式解释,则使用列表中首先出现的解释,即使由该解释产生的转换格式不正确。

让我们来看看你的例子

Impl impl;
Interface* pIntfc = &impl;
Base* pBase = (Base*)pIntfc;

无法使用const_cast,列表中的下一个元素是static_cast。但是,InterfaceBase无关,因此没有static_cast可以从Interface*转换为Base* } 的。因此,使用了reinterpret_cast

附加说明: 您问题的实际答案是:由于上面的列表中没有dynamic_cast,因此C风格的广告素材从不像dynamic_cast


实际地址更改的方式不是C ++语言定义的一部分,但我们可以举例说明如何实现:

具有至少一个虚函数(继承或拥有)的类的每个对象包含(读取:可以包含,在此示例中)指向vtable的指针。如果它从多个类继承虚函数,它包含多个指向vtable的指针。由于空基类优化(没有数据成员),Impl的实例可能如下所示:

+=Impl=======================================+
|                                            |
|  +-Base---------+   +-Interface---------+  |
|  | vtable_Base* |   | vtable_Interface* |  |
|  +--------------+   +-------------------+  |
|                                            |
+============================================+

现在,例子:

     Impl  impl;

     Impl* pImpl  = &impl;
Interface* pIntfc = pImpl;
     Base* pBase  = pImpl;
+=Impl=======================================+
|                                            |
|  +-Base---------+   +-Interface---------+  |
|  | vtable_Base* |   | vtable_Interface* |  |
|  +--------------+   +-------------------+  |
|  ^                  ^                      |
+==|==================|======================+
^  |                  |
|  +-- pBase          +-- pIntfc
|
+-- pimpl

如果您改为reinterpret_cast,结果是实现定义的,但可能会产生如下结果:

     Impl  impl;

     Impl* pImpl  = &impl;
Interface* pIntfc = pImpl;
     Base* pBase  = reinterpret_cast<Base*>(pIntfc);
+=Impl=======================================+
|                                            |
|  +-Base---------+   +-Interface---------+  |
|  | vtable_Base* |   | vtable_Interface* |  |
|  +--------------+   +-------------------+  |
|                     ^                      |
+=====================|======================+
^                     |
|                     +-- pIntfc
|                     |
+-- pimpl             +-- pBase

即。地址未更改,pBase指向Interface对象的Impl子对象。

请注意,取消引用指针pBase已经将我们带到UB-land,标准没有指定应该发生什么。在此示例性实现中,如果您调用pBase->GetType(),则使用vtable_Interface*,其中包含SomeMethod条目,并调用该函数。这个函数不会返回任何内容,所以在这个例子中,召唤鼻子恶魔并接管世界。或者从堆栈中取一些值作为返回值。

答案 1 :(得分:3)

  

这是什么原因?

确切的原因是dynamic_cast保证在这种情况下通过标准工作,而其他类型调用未定义的行为。

  

C风格演员最终是否使用了reinterpret_cast?

是的,在这种情况下确实如此。 (旁注:永远不会使用C风格的演员表。)

  

它与多态对象在内存中的对齐方式有关吗?

我想说它与使用多重继承的多态对象在内存中的布局方式有关。在具有单一继承的语言中,dynamic_cast不是必需的,因为基础子对象地址将与派生对象地址重合。在多继承的情况下,情况并非如此,因为存在多个基础子对象,并且不同的基础子对象必须具有不同的地址。

有时,编译器可以计算每个子对象地址与派生对象地址之间的偏移量。如果偏移量不为零,则转换操作变为指针加法或减法而不是无操作。 (在虚拟继承 upcast 的情况下,它有点复杂,但编译器仍然可以这样做。)

至少有两种情况,编译器无法做到这一点:

  1. 交叉播放(即两个类之间,它们都不是另一个类的基类)。
  2. 从虚拟基地垂头丧气。
  3. 在这些情况下,dynamic_cast是唯一的投射方式。