【问题标题】:C++ Multiple Inheritance Memory Layout with "Empty classes"带有“空类”的 C++ 多继承内存布局
【发布时间】:2012-06-18 08:50:37
【问题描述】:

我知道多重继承的内存布局没有定义,所以我不应该依赖它。但是,我可以在特殊情况下依赖它吗?也就是说,一个类只有一个“真正的”超类。所有其他都是“空类”,即既没有字段也没有虚拟方法的类(即它们只有非虚拟方法)。在这种情况下,这些额外的类不应向类的内存布局添加任何内容。 (更简洁地说,在 C++11 的措辞中,该类具有 standard-layout

我可以推断所有超类都没有偏移量吗?例如:

#include <iostream>

class X{

    int a;
    int b;
};

class I{};

class J{};

class Y : public I, public X,  public J{};

int main(){

    Y* y = new Y();
    X* x = y;
    I* i = y;
    J* j = y;

    std::cout << sizeof(Y) << std::endl 
                  << y << std::endl 
                  << x << std::endl 
                  << i << std::endl 
                  << j << std::endl;
}

这里,Y 是类,X 是唯一真正的基类。程序的输出(在linux上用g++4.6编译时)如下:

8

0x233f010

0x233f010

0x233f010

0x233f010

正如我的结论,没有指针调整。但是这个实现是特定的还是我可以依赖它。即,如果我收到I 类型的对象(并且我知道只存在这些类),我可以使用reinterpret_cast 将其转换为X 吗?

我希望我可以依赖它,因为规范说对象的大小必须至少是一个字节。因此,编译器无法选择其他布局。如果它将IJ 布局在X 的成员后面,那么它们的大小将为零(因为它们没有成员)。因此,唯一合理的选择是对齐所有超类而不偏移。

如果我在这里使用从IX 的reinterpret_cast,我是正确的还是我在玩火?

【问题讨论】:

  • 继承的内存布局,单个或多个,空基与否,未定义。
  • 我会说你是在玩火,因为你试图制造一个非常奇怪的结构,这肯定会给将来必须理解它的任何人带来痛苦和痛苦,即使它升级编译器时不会中断!
  • 幸运的是,您的陈述在 C++11 中不再正确,请参阅答案 :)
  • @n.m.它是实现定义。实现需要记录它,所以它是定义的,只是不在标准中。

标签: c++ multiple-inheritance memory-alignment


【解决方案1】:

在 C++11 中,编译器需要对 标准布局 类型使用空基类优化。见https://stackoverflow.com/a/10789707/981959

对于您的具体示例,所有类型都是标准布局类,没有公共基类或成员(见下文),因此您可以依赖 在 C++11 中(以及实践中,我认为许多编译器已经遵循了这条规则,当然 G++ 也这样做了,还有其他编译器遵循 Itanium C++ ABI。)

警告:确保您没有任何相同类型的基类,因为它们必须位于不同的地址,例如

struct I {};

struct J : I {};
struct K : I { };

struct X { int i; };

struct Y : J, K, X { };

#include <iostream>

Y y;

int main()
{
  std::cout << &y << ' ' << &y.i << ' ' << (X*)&y << ' ' << (I*)(J*)&y << ' ' << (I*)(K*)&y << '\n';

}

打印:

0x600d60 0x600d60 0x600d60 0x600d60 0x600d61

对于Y 类型,只有I 基数之一可以位于偏移量零处,因此尽管X 子对象位于偏移量零处(即offsetof(Y, i) 为零)并且@987654332 之一@ bases 在同一个地址,但另一个I base 是(至少在 G++ 和 Clang++ 中)对象的一个​​字节,所以如果你有一个 I* 你不能 reinterpret_castX* 因为你不会知道 哪个 I 子对象指向,偏移量 0 处的 I 或偏移量 1 处的 I

编译器可以将第二个 I 子对象放在偏移量 1 处(即 inside int),因为 I 没有非静态数据成员,所以你实际上无法取消引用或访问该地址处的任何内容,只能获取指向该地址处对象的指针。如果您向I 添加了非静态数据成员,那么Y 将不再是标准布局并且不必使用EBO,offsetof(Y, i) 将不再为零。

【讨论】:

  • 是的,当然。我必须确保班级有标准布局。谢谢!
  • (更新了示例以更接近您的原始示例)。在我的示例中,Y 标准布局,但将reinterpret_castI*J*K*Y*X* 结合起来并不安全,因为您不知道它指向哪个I*
  • 好的,所以没有继承菱形,只有标准布局。就这样吗?
  • 在您编写此答案时,示例中的所有类型都是标准布局类型,但由于[class.prop](3.5),它们不再是。但是,如果您在最新版本的 GCC 和 clang 中运行代码,基本上 you still get the same addresses。鉴于指向YJ 的指针以及指向YK 的指针不再是pointer-intercovertible,你能解释一下这是怎么可能的吗?
  • @Belloc Itanium ABI 的规则没有在标准布局或指针互转换方面指定,因此当标准布局的定义发生变化时,所需的布局不会改变。最后两段中的所有内容(“对于类型Y...”和“编译器可以放置...”)在 C++20 中仍然适用。如果您愿意,请将“标准布局”替换为“用于布局目的的 POD”。
猜你喜欢
  • 2012-07-09
  • 1970-01-01
  • 2020-09-30
  • 2015-09-01
  • 2015-08-27
  • 2011-06-04
  • 1970-01-01
  • 2015-04-15
  • 2015-11-14
相关资源
最近更新 更多