为啥 C-style cast 的行为与 dynamic_cast 不同?

Posted

技术标签:

【中文标题】为啥 C-style cast 的行为与 dynamic_cast 不同?【英文标题】:Why does C-style cast behave differently than dynamic_cast?为什么 C-style cast 的行为与 dynamic_cast 不同? 【发布时间】:2013-08-03 18:50:41 【问题描述】:

我有以下类层次结构:

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_castIControl-reference 投射到 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 吗?和多态对象在内存中的对齐方式有关系吗?

【问题讨论】:

When should static_cast, dynamic_cast and reinterpret_cast be used?的可能重复 我已经看到了这个问题并阅读了答案(以及有关此主题的其他答案)。但对我来说,仍然不清楚为什么会在这种具体情况下发生这种情况,这就是为什么我发布了一个单独的问题。 请注意ControlBase* testCB3 = static_cast&lt;ControlBase*&gt;(&amp;control);ControlBase* testCB4 = static_cast&lt;ControlImpl*&gt;(&amp;control); 之间存在差异。后者不调用 UB(并为 GetType() 测试正确生成 1)。 仅仅因为您知道 ControlBase 和 IControl 作为基类一起出现并不意味着编译器会推断出这个事实。该标准并没有说编译器需要弄清楚这一点。我认为 dynamic_cast 是有效的,因为编译器知道“控制”实际上是一个 ControlImpl,如果你通过函数参数传递引用,我认为它不会起作用。 我不能手动进行静态转换,这一行:ControlBase* testCB4 = static_cast&lt;ControlBase*&gt;(&amp;control); 给出编译器错误error C2440: 'static_cast' : cannot convert from 'IControl *' to 'ControlBase *'。这就是为什么我认为 C 风格的演员做了 reinterpret_cast。 【参考方案1】:

我认为您示例中的类名有点令人困惑。我们称它们为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 样式的强制转换永远不会像 @ 987654343@.


实际地址如何变化不是 C++ 语言定义的一部分,但我们可以举一个例子来说明它是如何实现的:

具有至少一个虚函数(继承的或拥有的)的类的每个对象都包含(阅读:可以包含,在本例中)一个指向 vtable 的指针。如果它从多个类继承虚函数,则它包含多个指向 vtable 的指针。由于空基类优化(无数据成员),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 | | +-- pimpl +-- pBase

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

请注意,取消引用指针pBase 已经将我们带到了UB-land,标准没有指定应该发生什么。在此示例性实现中,如果您调用pBase-&gt;GetType(),则使用包含SomeMethod 条目的vtable_Interface*,并调用该函数。此函数不返回任何内容,因此在此示例中,召唤了鼻恶魔并占领了世界。或者从堆栈中取出某个值作为返回值。

【讨论】:

感谢您的详尽回答,这很有意义!在这些行中:Interface* pIntfc = pImpl;Base* pBase = pImpl;,我们甚至不必使用 dynamic_cast,因为我们向上转换继承树,这可以隐式完成,对吧? 右:[conv.ptr]/3 "A prvalue of type "pointer to cv D",其中D是类类型,可以转换指向“指向cv B的指针”类型的纯右值,其中BD的基类。如果BD的不可访问或模棱两可的基类,需要进行这种转换的程序格式不正确。”【参考方案2】:

这究竟是什么原因?

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

C 风格的演员表最终会使用 reinterpret_cast 吗?

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

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

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

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

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

    交叉转换(即,在两个类之间,这两个类都不是另一个的基类)。 来自虚拟基地的沮丧。

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

【讨论】:

以上是关于为啥 C-style cast 的行为与 dynamic_cast 不同?的主要内容,如果未能解决你的问题,请参考以下文章

为啥受保护的继承会导致 dynamic_cast 失败?

为啥减法与static_cast溢出?

为啥 Linq Cast<> 助手不能与隐式转换运算符一起使用?

确保dynamic_cast不会导致未定义的行为C ++

C++ 联合与 reinterpret_cast

Java Class.cast()与强制转换运算符