看一个简单的例子:
struct Base { /* some virtual functions here */ };
struct A: Base { /* members, overridden virtual functions */ };
struct B: Base { /* members, overridden virtual functions */ };
void fn() {
A a;
Base *base = &a;
B *b = reinterpret_cast<B *>(base);
Base *x = b;
// use x here, call virtual functions on it
}
这个小片段是否有未定义的行为?
reinterpret_cast
的定义很好,它返回的值base
不变,只是类型为B *
。
但是我不确定Base *x = b;
行。它使用类型为b
的{{1}},但实际上它指向一个B *
对象。而且我不确定A
是否是“适当的” x
指针,是否可以用它来调用虚函数。
答案 0 :(得分:4)
static_cast
(或隐式派生到基址指针的转换,其作用完全相同)与reinterpret_cast
实质上不同。不能保证基础子对象的起始地址与完整对象的地址相同。
大多数实现将 first 基础子对象放置在与完整对象相同的地址上,但是当然,即使这样的实现也不能将两个不同的非空基础子对象放置在同一位置。相同的地址。 (具有虚拟功能的对象不能为空)。当基础子对象与完整对象不在同一地址时,static_cast
不是空操作,它涉及指针调整。
有些实现甚至从未将第一个基础子对象放置在与完整对象相同的地址上。例如,允许将基础子对象放置在派生的所有成员之后。 IIRC Sun C ++编译器用于以这种方式布局类(不知道它是否还在这样做)。在这样的实现中,几乎可以保证该代码将失败。
具有多个基数的B的相似代码将在许多实现中失败。 Example。
答案 1 :(得分:1)
如果两个类在布局上兼容,则reinterpret_cast
有效(可以取消引用结果);那是
但是这些类没有标准布局,因为StandardLayoutType
的要求之一是该类具有没有虚拟功能或虚拟基类。
关于从转换派生的指针的有效性,该标准在“安全派生的指针”部分中有此规定:
6.7.4.3安全派生的指针
4。一个实现可能具有宽松的指针安全性,在这种情况下,指针值的有效性不取决于它是否是安全得出的指针值。备选地,实现可以具有严格的指针安全性,在这种情况下,除非动态引用了具有动态存储持续时间的对象的指针值不是安全导出的指针值,否则该指针值是无效的指针值,除非先前已声明所引用的完整对象可到达。 [注意:使用无效的指针值(包括将其传递给释放函数)的效果是不确定的,请参见6.7.4.2。即使未安全派生的指针值可能与某些安全派生的指针值进行比较也是如此。 —end note]由实现定义,是实现是宽松的还是严格的指针安全性。
答案 2 :(得分:0)
是的,它确实具有未定义的行为。 A和B中有关Base主题的布局未定义。 x可能不是真正的基本对象。
答案 3 :(得分:0)
如果A
和B
是彼此的逐字记录副本(它们的名称除外),并且在相同的上下文中声明(相同的名称空间,相同的#define,没有__LINE__
的用法) ),然后普通的C ++编译器(gcc
,clang
)将产生两个完全可互换的二进制表示形式。
如果A
和B
使用相同的方法签名,但相应方法的主体不同,则将A*
强制转换为{{1}是不安全的 },因为编译器中的优化过程可能会在调用站点B*
上部分内联void B::method()
的主体,而程序员可能会假设b->method()
将调用b->method()
。因此,程序员一旦使用优化编译器,通过类型A::method()
访问A
的行为就变得未定义。
问题:即使在B*
,所有编译器总是至少在某种程度上“优化”传递给他们的源代码。如果C ++标准没有规定行为(即未定义行为),则在关闭所有优化后,编译器的隐式假设可能与程序员的假设不同。隐式假设由编译器的开发人员做出。
结论:如果程序员能够避免使用优化的编译器,则可以安全地通过-O0
访问A
。这样的程序员唯一需要解决的问题是非优化编译器不存在。
通过B*
将A*
强制转换为B*
时,访问reinterpret_cast
或调用b->field
时,托管的C ++实现可能会中止程序。其他一些托管的C ++实现可能会更努力地避免程序崩溃,因此当它看到程序通过b->method()
访问A
时,将诉诸于临时鸭子输入。
一些问题是: