以下代码显示了将派生类向上转换为其基类时的不同行为。
#include <cstdio>
struct base {
int value;
};
template <typename T> struct wrapper: base, T {
};
struct empty {};
struct nonempty { int value; };
template <typename T>
int ofst(wrapper<T> *t) { return (char *) ((T *) t) - (char *) t; }
template <typename T>
int ofst2(wrapper<T> *t) { return (char *) static_cast<T*>(t) - (char *) t; }
int main() {
wrapper<empty> empty;
wrapper<nonempty> nonempty;
printf("%d %d\n", ofst(&empty), ofst(&nonempty)); // give 0, 4
printf("%d %d\n", ofst2(&empty), ofst2(&nonempty)); // also give 0, 4
}
有人可以给我解释这个结果的参考文献吗?
C++ 标准并未完全指定
struct
或其他类类型的哪些字节用于其基类和成员子对象。尽管在“标准布局类类型”([class.prop]/3)的特殊情况下,还有更多的保证。
恰好你的
wrapper<empty>
是标准布局,但 wrapper<nonempty>
不是,因为 [class.prop]/3.6 需要一个标准布局类“拥有类中的所有非静态数据成员和位字段及其基类首先在同一个类中声明”。 wrapper<empty>
只有一个非静态数据成员,类型为 int
。但是 wrapper<empty>
有两个非静态数据成员,一个首先在 base
中声明,一个首先在 nonempty
中声明。
对于标准布局
wrapper<empty>
,wrapper<empty>
对象和 empty
子对象是“指针可相互转换”([basic.compound]/4)并且具有相同的地址。因此,尽管 ofst
和 ofst2
减去不指向任何数组元素的指针在技术上是未定义的行为,但实际上 ofst(&empty)
和 ofst2(&empty)
通常会返回零。
在早期版本的 C++ 中,
wrapper<empty>
不会是标准布局,但许多编译器确实选择按照现在的要求对 empty
的 wrapper<empty>
子对象使用相同的地址,这称为空基优化.
由于
wrapper<nonempty>
不是标准布局,因此由编译器决定如何安排其子对象的存储。但这里最明显的事情是根据基类在 base
类模板定义中列出的顺序,将 int
子对象及其 nonempty
放在 int
子对象及其 wrapper
之前。因此,看到 ofst(&nonempty)
和 ofst2(&nonempty)
返回正值是有意义的。再次强调,关于未定义行为的警告适用。