结构的字节布局(#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 时试了一下它并没有改变布局。不确定这是错误还是预期的行为,但无论哪种方式,它似乎都不是解决问题的方法。
以下代码在 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 时试了一下它并没有改变布局。不确定这是错误还是预期的行为,但无论哪种方式,它似乎都不是解决问题的方法。