【问题标题】:will the padding of base class be copied into the derived class?基类的填充是否会复制到派生类中?
【发布时间】:2018-10-15 13:57:44
【问题描述】:

最近,我一直在阅读“c++ 对象模型内部”。它说基类中使用的填充也应该复制到派生类中,以防您要将基类分配给派生类。因此,我在 64 位计算机下运行测试:

    class A {
    public:
        int valA;
        char a;
    };
    class B : public A {
    public:
        char b;
    };
    class C : public B {
    public:
        char c;
    };
    int main(){
        std::cout << sizeof(A) << " " << sizeof(B) << " " << sizeof(C) 
        << std::endl;
        C c;
        printf("%p\n%p\n%p\n",&c,&c.b,&c.c);
    }

结果如下:

    8 12 12

    0x7ffd22c5072c

    0x7ffd22c50734

    0x7ffd22c50735

那么为什么 C 的大小与 B 的大小相同?虽然看起来 B 使用了 A 中的 3 字节填充。

【问题讨论】:

  • 显然,单个 char 类没有被填充(没有必要)。如果您在C 中的c 字段是int,那么无疑会正确对齐。
  • c 更改为int 并再次运行您的程序。您的怀疑可能对您的工具链是正确的。
  • 标准对此不做任何保证。在某些情况下,该类可能会得到memcpyed,或者它可能会被成员复制。由于优化,复制可能不会发生,并使其看起来像复制了填充。很多事情都可能发生。

标签: c++


【解决方案1】:

那么为什么 C 的大小与 B 相同?

因为B 的尾随填充被重复用于C::b。填充可以重复使用,因为B 不是 POD(plain old data)类(因为它不是标准布局类)。

虽然看起来 B 在 A 中使用了 3 字节填充。

A 的填充不能被B 的其他子对象重用,因为A 是一个标准布局类并且可以简单地复制,即A 是一个POD 类。


基类的填充是否会复制到派生类中?

我想你不是要问复制,而是派生类的基类子对象是否与单个类型具有相同的填充。

答案是,可以从上面推断:填充将是相同的,除了尾随填充可以重新用于其他子对象,除非基类是 POD,在这种情况下它的填充不能重复使用。

对于padding可以重复使用的情况,标准没有规定是否重复使用,编译器之间存在差异。


请解释或链接到“标准布局类型”的定义。

现行标准草案:

[基本类型]

... 标量类型、标准布局类类型 ([class.prop])、此类类型的数组以及这些类型的 cv 限定版本统称为标准布局类型。


[class.prop](在旧版本的标准中,这些可以直接在[class]下找到)

一个类 S 是一个标准布局类,如果它:

  • (3.1) 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,

  • (3.2) 没有虚函数,也没有虚基类,

  • (3.3) 对所有非静态数据成员具有相同的访问控制,

  • (3.4) 没有非标准布局的基类,

  • (3.5) 最多有一个任意给定类型的基类子对象,

  • (3.6) 具有类中的所有非静态数据成员和位域,并且其基类首先在同一类中声明,并且

  • (3.7) 没有类型集合 M(S) 的元素作为基类,其中对于任何类型 X,M(X) 定义如下。107 [ 注:M(X) 是 所有非基类子对象的类型集,它们可能位于 X 中的零偏移量。— 尾注]

    • (3.7.1) 如果 X 是没有(可能继承的)非静态数据成员的非联合类类型,则集合 M(X) 为空。

    • (3.7.2) 如果 X 是具有类型 X0 的非静态数据成员的非联合类类型,该数据成员的大小要么为零,要么是第一个 X 的非静态数据成员(其中所述成员可能是匿名的 union),集合 M(X) 由 X0 和 M(X0) 的元素组成。

    • (3.7.3) 如果 X 是联合类型,则集合 M(X) 是所有 M(Ui) 和包含所有 Ui 的集合的联合,其中每个 Ui 是 X 的第 i 个非静态数据成员。

    • (3.7.4) 如果 X 是元素类型为 Xe 的数组类型,则集合 M(X) 由 Xe 和 M(Xe) 的元素组成。

    • (3.7.5) 如果 X 是非类、非数组类型,则集合 M(X) 为空。

第 (3.6) 项适用于这种情况。 B 的某些成员不是首先在 B 中声明的。特别是,B::A::valA 和 B::A::a 在 A 中首先声明。描述规则的更友好的方式是:类必须具有没有直接成员,或者它的祖先都没有成员。在这种情况下,基类和派生类都有成员,因此不是标准布局。

【讨论】:

  • 请解释或链接到“标准布局类型”的定义。
  • @Sjoerd 现在是 3.6。 B 的某些成员不是首先在 B 中声明的。特别是,B::A::valAB::A::a 首先在 A 中声明。描述规则的更友好的方式是:该类必须没有直接成员,或者其祖先都必须没有成员。在这种情况下,基类和派生类都有成员,因此不是标准布局。
  • 如果我正确理解您的断言,您是说如果B 是POD,那么从B 派生的class A : B {...} 不能重用B 的填充?
  • This 似乎是一个反例,至少如果您假设编译器遵循标准。 simple 是 POD(至少根据 std::is_pod 的意见和 C++11 规则,虽然不是 C++03 规则),但 simple 的填充被 derived 重新使用。跨度>
  • @BeeOnRope 我想答案是错误的。实际上,只要该类是非 POD (或非标准布局),它就可以重新使用子对象的填充(例如您的示例中的派生 is 和 does )。但这并不能解释派生 B 是非 POD 的 OP 中缺乏重用。要么是我错过了某些规则,要么 ABI 根本没有利用它(可能是由于向后兼容性)。
【解决方案2】:

CB 大小相同,因为在您的平台上,ABI 选择使用B 中的填充来存储1 字节成员C::cB 在末尾有 3 个字节的填充,因为整个 B 对象具有对齐 4(由于 A 中的 int 成员)。

BA 的大小不同,但是,因为在这种情况下,即使有空间,ABI 显然也不允许将B::b 存储在A 的填充中。当所有A 成员都是公开的时会发生这种情况,就像在您的示例中一样:如果您将任何成员设为私有,ABC 的大小都将为 8。我相信这可能是为了 ABI 向后兼容,而不是出于标准中的任何语言的动机。

我不知道标准中是否有直接允许这样做的语言(但没有必要),但似乎这种类型的填充重用是在继承的情况下考虑。例如,std::memcpy 的文档说:

如果对象可能重叠或不是 TriviallyCopyable,则未指定 memcpy 的行为并且可能未定义。

继续定义potentially-overlapping:

一个子对象可能重叠,如果它是一个

  • 基类子对象,或
  • 使用 [[no_unique_address]] 属性声明的非静态数据成员。

第二个条件仅适用于 C++20。

这似乎是为了允许共享填充:如果不存在此子句,则指向 CB 子类的指针上的 memcpy 将覆盖存储的 C::c 的值在 B 的通常填充中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-10-01
    • 2015-01-03
    • 2011-12-07
    • 2011-11-23
    • 2012-10-14
    • 2011-10-15
    • 2013-01-14
    • 2020-05-21
    相关资源
    最近更新 更多