为啥 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_cast将 IControl-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<ControlBase*>(&control);
和ControlBase* testCB4 = static_cast<ControlImpl*>(&control);
之间存在差异。后者不调用 UB(并为 GetType()
测试正确生成 1
)。
仅仅因为您知道 ControlBase 和 IControl 作为基类一起出现并不意味着编译器会推断出这个事实。该标准并没有说编译器需要弄清楚这一点。我认为 dynamic_cast 是有效的,因为编译器知道“控制”实际上是一个 ControlImpl,如果你通过函数参数传递引用,我认为它不会起作用。
我不能手动进行静态转换,这一行:ControlBase* testCB4 = static_cast<ControlBase*>(&control);
给出编译器错误error C2440: 'static_cast' : cannot convert from 'IControl *' to 'ControlBase *'
。这就是为什么我认为 C 风格的演员做了 reinterpret_cast。
【参考方案1】:
我认为您示例中的类名有点令人困惑。我们称它们为Interface
、Base
和Impl
。请注意,Interface
和 Base
是不相关的。
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
。但是Interface
和Base
类不相关,因此没有static_cast
可以从Interface*
转换为Base*
。因此,使用了reinterpret_cast
。
补充说明:您的问题的实际答案是:由于上面的列表中没有 dynamic_cast
,因此 C 样式的强制转换永远不会像 @ 987654343@.
实际地址如何变化不是 C++ 语言定义的一部分,但我们可以举一个例子来说明它是如何实现的:
具有至少一个虚函数(继承的或拥有的)的类的每个对象都包含(阅读:可以包含,在本例中)一个指向 vtable 的指针。如果它从多个类继承虚函数,则它包含多个指向 vtable 的指针。由于空基类优化(无数据成员),Impl
的实例可能如下所示:
现在,例子:
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->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
的指针”类型的纯右值,其中B
是D
的基类。如果B
是D
的不可访问或模棱两可的基类,需要进行这种转换的程序格式不正确。”【参考方案2】:
这究竟是什么原因?
确切的原因是标准保证dynamic_cast
在这种情况下工作,而其他类型会调用未定义的行为。
C 风格的演员表最终会使用 reinterpret_cast 吗?
是的,在这种情况下确实如此。 (附注:从不使用 C 风格的演员表)。
是否与多态对象在内存中的对齐方式有关?
我会说这与使用多重继承的多态对象在内存中的布局方式有关。在具有单继承的语言中,dynamic_cast
不是必需的,因为基本子对象地址与派生对象地址一致。在多重继承的情况下,情况并非如此,因为有多个基础子对象,并且不同的基础子对象必须具有不同的地址。
有时编译器可以计算每个子对象地址和派生对象地址之间的偏移量。如果偏移量不为零,则强制转换操作将变为指针加法或减法,而不是无操作。 (在虚拟继承 upcast 的情况下,它有点复杂,但编译器仍然可以做到这一点。
至少有两种情况编译器无法做到这一点:
-
交叉转换(即,在两个类之间,这两个类都不是另一个的基类)。
来自虚拟基地的沮丧。
在这些情况下,dynamic_cast
是唯一的投射方式。
【讨论】:
以上是关于为啥 C-style cast 的行为与 dynamic_cast 不同?的主要内容,如果未能解决你的问题,请参考以下文章