为什么C样式转换的行为与dynamic_cast不同

Why does C-style cast behave differently than dynamic_cast?

本文关键字:dynamic cast 不同 样式 转换 为什么      更新时间:2023-10-16

我有以下类层次结构:

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_castC-style转换了一个IControl-对ControlBase的引用(因为我知道它派生自它):

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样式的强制转换是否最终使用interpret_cast?它与多态对象在内存中的排列方式有关吗?

我认为您的示例中的类名有点令人困惑。让我们称它们为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的指针。如果它从多个类继承虚拟函数,那么它包含多个指向vtables的指针。由于空基类优化(没有数据成员),Impl的实例可能如下所示:

+=Impl=======================================+|||+-底座-------++-接口-------++|||vtable_Base*||vtable_Interface*|||+----------------+----------------+|||+============================================+

现在,示例:

     Impl  impl;
     Impl* pImpl  = &impl;
Interface* pIntfc = pImpl;
     Base* pBase  = pImpl;
+=Impl=======================================+|||+-底座-------++-接口-------++|||vtable_Base*||vtable_Interface*|||+----------------+----------------+||^^|+==|==================|======================+^|||+--pBase+--pIntfc|+--皮条客

如果您改为执行reinterpret_cast,则结果是由实现定义的,但它可能会导致以下结果:

     Impl  impl;
     Impl* pImpl  = &impl;
Interface* pIntfc = pImpl;
     Base* pBase  = reinterpret_cast<Base*>(pIntfc);
+=Impl=======================================+|||+-底座-------++-接口-------++|||vtable_Base*||vtable_Interface*|||+----------------+----------------+||^|+=====================|======================+^||+--pIntfc||+--皮条+--pBase

即地址不变,pBase指向Impl对象的Interface子对象。

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

这到底是什么原因?

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

C样式的强制转换最终会使用repret_cast吗?

是的,在这种情况下确实如此。(附带说明:永远不要使用C样式转换)。

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

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

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

编译器至少有两种情况不能做到这一点:

  1. 交叉转换(也就是说,在两个类之间,这两个类都不是另一个的基类)
  2. 从虚拟基地进行的向下广播

在这些情况下,dynamic_cast是唯一的投射方式。