微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

C++ 是否保证具有单个平凡成员的“平凡”结构具有相同的二进制布局?

如何解决C++ 是否保证具有单个平凡成员的“平凡”结构具有相同的二进制布局?

我们的项目中有一些严格类型的整数类型:

struct FooIdentifier {
  int raw_id; // the only data member

  // ... more shenanigans,but it stays a "trivial" type.
};

struct BarIdentifier {
  int raw_id; // the only data member

  // ... more shenanigans,but it stays a "trivial" type.
};

基本上是提议的 here 或类似于 Unit Library 中使用的东西。

这些结构基本上整数,除了类型系统。

我现在这里的问题是:C++ 语言是否保证这些类型在内存中的布局与常规 int 一样 100%?

注意:因为我可以静态检查类型是否具有相同的大小(即没有填充),所以我真的只对没有意外填充的情况感兴趣。 我应该从一开始就添加这个注释

// Precodition. If platform would yield false here,I'm not interested in the result.
static_assert(sizeof(int) == sizeof(ID_t)); 

也就是说,根据 C++ 标准 POV 执行以下操作

int integer_array[42] = {}; // zero init
ID_t id_array[42] = {}; // zero init

static_assert(sizeof(int) == sizeof(ID_t)); // Precodition. If platform would yield false here,I'm not interested in the result.

const char* const pIntArrMem = static_cast<const char*>(static_cast<const void*>(integer_array));
const char* const pIdArrMem = static_cast<const char*>(static_cast<const void*>(id_array));
assert(0 == memcmp(pIntArrMem,pIdArrMem,sizeof(int))); // Always ???

解决方法

TL;DR 不,标准似乎不能保证(据我所知)。从技术上讲,您必须依赖于拥有健全的 ABI。

您可能需要放弃支持 ds9k。


该标准并没有对布局做出明确的保证。我们充其量可以根据我们确实拥有的保证对实际实现可以做什么做出一些合理的假设。

[基本化合物]

两个对象 a 和 b 是指针可相互转换的,如果:

  • ...
  • 一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有非静态数据成员,则该对象的任何基类子对象 ([class.内存]),或
  • 存在一个对象 c,使得 a 和 c 是指针可互转换的,而 c 和 b 是指针可互转换的。

如果两个对象是指针可互转换的,则它们具有相同的地址,并且可以通过 reinterpret_cast 从指向另一个的指针获得指向一个的指针。

由此,我们可以传递地知道,在标准布局类中,第一个成员之前实际上不能填充。

[expr.sizeof]

... 当应用于一个类时,结果是该类的对象中的字节数,包括将该类型的对象放置在数组中所需的任何填充。 ...当应用于数组时,结果是数组中的总字节数。这意味着一个包含 n 个元素的数组的大小是一个元素大小的 n 倍。

这意味着 integer_arrayid_array 或任何数组都没有在元素之前(或之间或之后)填充。

鉴于在 int 子对象之前缺少填充,您的第二个断言将是一个合理的假设,除非一个对象可以在一个上下文中具有一种表示形式而在另一种上下文中具有另一种表示形式(自由与子对象,或子对象)不同封闭类型的对象)。例如,一个是 big endian,另一个是 little endian。我找不到禁止这样做的标准,但我也无法想象这种实现在实践中如何工作,因为编译器实际上无法始终知道特定的泛左值是否是子对象(以及在哪个封闭对象中)。

鉴于上述假设,第一个断言归结为“标准布局类是否可以在唯一成员之后有填充?实际上,如果有 alignas 或某些布局影响所涉及的语言扩展,但如果情况并非如此,我们可以假设否定吗?标准并没有说太多,而且我认为这对于语言实现在实践中添加一些填充甚至是不可能的 - 只是不是非常有用。

关于对象表示的一些小标准:

[basic.types.general]

T 类型对象的对象表示是 T 类型对象占用的 N 个 unsigned char 对象的序列,其中 N 等于 sizeof(T)。 T 类型对象的值表示是参与表示 T 类型值的一组位。 对象表示中不属于值表示的位是填充位。 对于可简单复制的类型,值表示是确定值的对象表示中的一组位,该值是实现定义的值集的一个离散元素。 35

35) 目的是使 C++ 的内存模型与 ISO/IEC 9899 编程语言 C 的内存模型兼容。


关于 FooIdentifierBarIdentifier 是否保证彼此之间具有相同表示的一点点。

[class.mem.general]

两个标准布局结构 ([class.prop]) 类型的公共初始序列是声明顺序中最长的非静态数据成员和位域序列,从每个结构中的第一个这样的实体开始,这样对应的实体具有布局兼容的类型,要么两个实体都用 no_unique_address 属性([dcl.attr.nouniqueaddr])声明,要么都不是,并且两个实体都是具有相同宽度的位字段,或者都不是位-字段。

如果两个标准布局结构 ([class.prop]) 类型的公共初始序列包含两个类 ([basic.types]) 的所有成员和位域,则它们是布局兼容的类。

[基本化合物]

...指向布局兼容类型的指针应具有相同的值表示和对齐要求

这些类是布局兼容的,作为描述听起来很有希望,但对语言规则几乎没有影响。

,

不,不能保证。简单的反例:

#include <cstdio>
struct S {
    int s;
} __attribute__ ((aligned (8)));
int main() { printf("%d %d\n",sizeof(S),sizeof(int)); }

在我的机器上打印 8 和 4。 __attribute__ 是非标准语法,但不能保证 gcc 将来不会默认更改为八字节对齐。

编辑:假设结构体和整数总是相同的大小,那么确实可以保证相同的二进制布局。至少在任何最不明智的实现中都是如此。

,

挑战 eerorika 的回答,我相信你可以保证二进制兼容性。为此,我将参考 C++11 规范。

关键部分: [class/7] 这定义了一个标准布局类。很明显,我们都同意这些是标准布局。

[intro.object/5][intro.object/6]

一个可简单复制或标准布局的对象 类型 (3.9) 应占用连续的存储字节。

除非对象是位域或零大小的基类子对象,否则该对象的地址是 它占用的第一个字节。

这限制了标准布局对象可以具有的形状,并指定了我们可以称之为对象“地址”的内容。

[class.mem/20]

一个指向标准布局结构对象的指针,使用 reinterpret_cast 适当转换,指向它的 初始成员(或者如果该成员是位字段,则为它所在的单元),反之亦然。 [ 笔记: 因此,标准布局结构对象中可能存在未命名的填充,但不是在其开头, 必要时实现适当的对齐。 ——结尾说明]

这表示我们至少可以通过重新解释转换将 ID_t* 转换为 int*

现在,您断言 sizeof(ID_t) == sizeof(int)。这是个好消息,因为它限制了您的选择。 int* someIdAsInt = reinterpret_cast<int*>(&someId) 保证会成功,它会指向每个 class.mem 的第一个成员。那么问题来了,可以返回的可能地址有哪些?显然,只有一个地址可能是sizeof(int)字节的第一个字节,当然也就是someId的地址。

所以我们可以确定 &someIdsomeIdAsInt 指的是同一个地址。而且,特别是,someIdAsInt 必须指向每个 class.mem 的初始成员。

如果我做*someIdAsInt = 43,结果一定和我做someId.raw_id = 43一样,因为someIdAsInt指向someId.raw_id。不管我用这个指针做什么来掩盖它,这个陈述一定是真的。

这表示 *someIdAsIntsomeId 要么必须具有相同的布局(允许赋值),要么编译器必须跟踪 someIdAsInt 的值,将其与普通 { 区别对待{1}}。这就是为什么我离开 eerorika 的答案。无法在带有类型标记的类型系统中处理此信息(它会强制编译器能够跟踪标记,即使您在线程之间执行了诸如传递 int* 之类的残酷操作)。因此,任何信息标记都必须被烘焙到形成 int* 值的字节中。 C++ 规范没有说明指针值的格式。

但是,int* 的不同程度是有限的,一般来说,undisputed。关键是我可以使用 int* 将一个 std::memcpy 的字节复制到另一个,并且生成的整数必须是相同的值。据我所知,这实际上并没有写入规范,但是(基本上?)所有程序员都接受它作为 C 和 C++ 的共同法则规则。实际上,C++20 中包含 int 进一步强调了这种事情。有两种无法通过字节区分的整数格式会破坏各种事情。

因此,如果您在语言律师论证中接受此普通法裁决,则 std::bit_cast 的布局必须与 ID_t if int 的布局相同。如果不接受普通法裁决,那么......好吧......我只想说一些自我反省=D

请注意,这意味着您可以安全地走另一条路。如果您有一个 sizeof(ID_t) == sizeof(int) 数组,则无法将其转换为 int 并访问它们。这将违反严格别名,因为该内存地址中从来没有ID_t*。但是,由于它们是相同的布局,因此使用 ID_tstd::memcpy 转换为具有等效位模式的 std::bit_cast 仍然是公平的游戏。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。