结构的字节布局(#pragma pack 行为)在 MSVC 上与 clang/gcc 不同

Byte layout of structure (#pragma pack behavior) different on MSVC vs clang/gcc

以下代码在 MSVC 和 clang/gcc 上生成不同的内存布局。为什么?

#include <stdio.h>

#pragma pack(push,1)

struct empty_struct
{
};

class derived_struct : public empty_struct
{
    int m_derivedMember;
};

class derived_struct_container : public empty_struct
{
public:
    derived_struct  m_ds;
};

class foo
{
public:
    foo() {}
    derived_struct_container m_dsc;
    int m_foo_member;
};
#pragma pack(pop)


int main()
{
    foo fb;
    printf("pf->m_dsc offset: %ld\n", (char *)&fb.m_dsc - (char *)&fb);
    printf("pf->m_dsc.m_ds offset: %ld\n", (char *)&(fb.m_dsc.m_ds) - (char *)&fb);
    printf("pf->m_foo_member offset: %ld\n", (char *)&(fb.m_foo_member) - (char *)&fb);

    return 0;
}

MSVC x64 上的输出是:

fb.m_dsc offset: 0
fb.m_dsc.m_ds offset: 0
fb.m_foo_member offset: 4

Linux 下 clang x64 的输出是:

fb.m_dsc offset: 0
fb.m_dsc.m_ds offset: 1
fb.m_foo_member offset: 5

如何让 clang 布局匹配 MSVC 布局?

使用 #pragma pack 会导致实现定义的行为。

此外,foo 不是 标准布局 class 因为有多个相同类型的基础 class 子对象,所以即使没有 pack 它的布局也不受任何 ABI 的影响。

坦率地说,依靠非标准布局 class 的布局是一个糟糕的想法,而且肯定有更好的方法来实现这里的任何目标。

以下是一些不涉及更改代码的可能方法(当然,即使其中任何一种方法目前看起来有效,它也可能随时更改):

  • 在 Windows.
  • 中使用 clang 或 g++ 而不是 MSVC
  • 尝试将标志传递给 MSVC 以更改 EBCO 行为see here for a writeup,也许可以提供 0 1 5 版本。
  • 编辑 gcc 或 clang 的源代码以构建您自己的编译器并提供所需的布局。

在 gcc 中,空碱基 class 优化被 class 禁用,因为它有两个相同类型的碱基,因此您可以通过更改代码来启用它 as suggested in comments under this question:

struct empty_struct {};
struct E2 {};

class derived_struct : public E2

(其余代码与您的示例相同)。即使没有 pragma pack,这也会给我 0 0 4 输出。我不知道 gcc 或 clang 的任何标志会改变 EBCO 行为。

此规则的基本原理是,在标准 C++ 中,如果同一类型的两个有效指针具有相同的值,则它们必须指向同一对象。这两个空子对象是不同的对象,因此它们必须存在唯一的地址。 MSVC 在这方面不符合规范。

在 C++20 中有一个属性 [[no_unique_address]] 据称放宽了这个要求,但是我在安装 g++ 9.2.0 时试了一下它并没有改变布局。不确定这是错误还是预期的行为,但无论哪种方式,它似乎都不是解决问题的方法。