C++ 是否保证具有单个普通成员的 "trivial" 结构具有相同的二进制布局?
Does C++ guarantee identical binary layout for "trivial" structs with a single trivial member?
我们的项目中有一些严格类型的整数类型:
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 or similar to things used in a 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 ???
不,不能保证。简单反例:
#include <cstdio>
struct S {
int s;
} __attribute__ ((aligned (8)));
int main() { printf("%d %d\n", sizeof(S), sizeof(int)); }
在我的机器上打印 8 和 4。 __attribute__
是非标准语法,但不保证 gcc 将来不会默认更改为八字节对齐。
编辑: 前提是struct 和int 的大小始终相同,因此确实可以保证相同的二进制布局。至少在任何最不明智的实现中。
TL;DR 不,标准似乎不能保证(据我所知)。从技术上讲,您必须依赖于拥有健全的 ABI。
你可能要放弃对ds9k的支持了
标准没有明确保证布局。充其量我们可以根据我们确实拥有的保证对实际实现可以做什么做出一些合理的假设。
[basic.compound]
Two objects a and b are pointer-interconvertible if:
- ...
- one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, any base class subobject of that object ([class.mem]), or
- there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.
If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_cast.
据此,我们可以知道在标准布局中实际上不能填充 class 在 第一个成员之前。
[expr.sizeof]
... When applied to a class, the result is the number of bytes in an object of that class including any padding required for placing objects of that type in an array. ... When applied to an array, the result is the total number of bytes in the array. This implies that the size of an array of n elements is n times the size of an element.
这意味着 integer_array
和 id_array
以及任何数组在元素之前(之间或之后)都没有填充。
鉴于 int
子对象之前缺少填充,你的第二个断言将是一个合理的假设,除非一个对象可以在一个上下文中有一个表示,而在另一个上下文中有另一个表示(自由与子对象,或不同封闭类型的子对象)。例如,一个是大端,另一个是小端。我找不到禁止这样做的标准,但我也无法想象这样的实现在实践中如何工作,因为编译器实际上不能总是知道特定的泛左值是否是一个子对象(以及在哪个封闭对象中)。
鉴于上述假设,第一个断言可归结为“标准布局 class 是否可以在唯一成员 after 中填充 ?实际上,如果有 alignas
或一些影响语言扩展的布局,但如果不是这种情况,我们可以假设是否定的吗?标准没有说太多,我认为这对于语言实现来说甚至是不可能的在实践中添加一些填充 - 只是不是很有用。
关于对象表示的小标准:
[basic.types.general]
The object representation of an object of type T is the sequence of N unsigned char objects taken up by the object of type T, where N equals sizeof(T).
The value representation of an object of type T is the set of bits that participate in representing a value of type T.
Bits in the object representation that are not part of the value representation are padding bits.
For trivially copyable types, the value representation is a set of bits in the object representation that determines a value, which is one discrete element of an implementation-defined set of values. 35
35) The intent is that the memory model of C++ is compatible with that of ISO/IEC 9899 Programming Language C.
关于 FooIdentifier
和 BarIdentifier
是否保证彼此之间具有相同表示的一点点。
[class.mem.general]
The common initial sequence of two standard-layout struct ([class.prop]) types is the longest sequence of non-static data members and bit-fields in declaration order, starting with the first such entity in each of the structs, such that corresponding entities have layout-compatible types, either both entities are declared with the no_unique_address attribute ([dcl.attr.nouniqueaddr]) or neither is, and either both entities are bit-fields with the same width or neither is a bit-field.
Two standard-layout struct ([class.prop]) types are layout-compatible classes if their common initial sequence comprises all members and bit-fields of both classes ([basic.types]).
[basic.compound]
... Pointers to layout-compatible types shall have the same value representation and alignment requirements
classes 是布局兼容的,这听起来很有希望,但对语言规则影响不大。
挑战eerorika的回答,我相信你保证二进制兼容性。为此,我将参考 C++11 规范。
关键部分:
[class/7] 这定义了一个标准布局 class。很明显,我们都同意这些是标准布局。
[介绍。object/5]和[介绍。object/6]
An object of trivially copyable or standard-layout
type (3.9) shall occupy contiguous bytes of storage.
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of
the first byte it occupies.
这限制了标准布局对象可以具有的形状,并指定了我们所谓的对象“地址”。
[class.mem/20]
A pointer to a standard-layout struct object, suitably converted using a reinterpret_cast, points to its
initial member (or if that member is a bit-field, then to the unit in which it resides) and vice versa. [ Note:
There might therefore be unnamed padding within a standard-layout struct object, but not at its beginning,
as necessary to achieve appropriate alignment. —end note ]
这表示我们至少可以通过重新解释转换将 ID_t*
转换为 int*
。
现在,您断言 sizeof(ID_t) == sizeof(int)
。这是个好消息,因为它限制了您的选择。 int* someIdAsInt = reinterpret_cast<int*>(&someId)
保证成功,它将指向第一个成员,根据 class.mem。那么问题来了,可能返回的地址有哪些呢?显然,只有一个地址可能是sizeof(int)
字节的第一个字节,当然是someId
.
的地址
所以我们可以确定&someId
和someIdAsInt
指的是同一个地址。并且,特别是 someIdAsInt
必须 指向每个 class.mem.
的初始成员
如果我要执行 *someIdAsInt = 43
,结果必须与我执行 someId.raw_id = 43
时的结果相同,因为 someIdAsInt
指向 someId.raw_id
。无论我用这个指针做什么来掩盖它,这个陈述都必须是真实的。
这表示 *someIdAsInt
和 someId
要么必须具有相同的布局(允许赋值),要么编译器必须跟踪 someIdAsInt
的值,将其与正常int*
。这就是为什么我不同意 eerorika 的回答。该信息无法在带有类型标记的类型系统中处理(它会强制编译器能够跟踪标记,即使您做了诸如在线程之间传递 int*
之类的残酷事情)。因此,任何信息标记都必须烘焙到构成 int*
值的字节中。 C++ 规范没有说明指针值的格式。
但是,int*
的差异是有限制的,一般来说,。关键一个是我可以用std::memcpy
将一个int
的字节复制到另一个int
中,得到的整数必须是相同的值。据我所知,这实际上并没有写入规范,但它被(基本上?)所有程序员接受为 C 和 C++ 的普通法规则。事实上,在 C++20 中包含 std::bit_cast
进一步强调了这类事情。拥有两种无法通过字节区分的整数格式会破坏各种各样的事情。
因此,如果您在语言律师争论中接受这项普通法裁决,那么您的 ID_t
的布局必须与 int
的布局相同,如果 sizeof(ID_t) == sizeof(int)
。如果那个普通法裁决不被接受,那么......好吧......我只想说一些自我反省是为了=D
请注意,这 不 意味着您可以安全地走另一条路。如果你有一个 int
数组,你不能将它转换为 ID_t*
然后访问它们。这将违反严格的别名,因为首先在该内存地址中从来没有 ID_t
。但是,因为它们是相同的布局,使用 std::memcpy
或 std::bit_cast
转换为具有等效位模式的 ID_t
仍然是公平的游戏。
我们的项目中有一些严格类型的整数类型:
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 or similar to things used in a 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 ???
不,不能保证。简单反例:
#include <cstdio>
struct S {
int s;
} __attribute__ ((aligned (8)));
int main() { printf("%d %d\n", sizeof(S), sizeof(int)); }
在我的机器上打印 8 和 4。 __attribute__
是非标准语法,但不保证 gcc 将来不会默认更改为八字节对齐。
编辑: 前提是struct 和int 的大小始终相同,因此确实可以保证相同的二进制布局。至少在任何最不明智的实现中。
TL;DR 不,标准似乎不能保证(据我所知)。从技术上讲,您必须依赖于拥有健全的 ABI。
你可能要放弃对ds9k的支持了
标准没有明确保证布局。充其量我们可以根据我们确实拥有的保证对实际实现可以做什么做出一些合理的假设。
[basic.compound]
Two objects a and b are pointer-interconvertible if:
- ...
- one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, any base class subobject of that object ([class.mem]), or
- there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.
If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_cast.
据此,我们可以知道在标准布局中实际上不能填充 class 在 第一个成员之前。
[expr.sizeof]
... When applied to a class, the result is the number of bytes in an object of that class including any padding required for placing objects of that type in an array. ... When applied to an array, the result is the total number of bytes in the array. This implies that the size of an array of n elements is n times the size of an element.
这意味着 integer_array
和 id_array
以及任何数组在元素之前(之间或之后)都没有填充。
鉴于 int
子对象之前缺少填充,你的第二个断言将是一个合理的假设,除非一个对象可以在一个上下文中有一个表示,而在另一个上下文中有另一个表示(自由与子对象,或不同封闭类型的子对象)。例如,一个是大端,另一个是小端。我找不到禁止这样做的标准,但我也无法想象这样的实现在实践中如何工作,因为编译器实际上不能总是知道特定的泛左值是否是一个子对象(以及在哪个封闭对象中)。
鉴于上述假设,第一个断言可归结为“标准布局 class 是否可以在唯一成员 after 中填充 ?实际上,如果有 alignas
或一些影响语言扩展的布局,但如果不是这种情况,我们可以假设是否定的吗?标准没有说太多,我认为这对于语言实现来说甚至是不可能的在实践中添加一些填充 - 只是不是很有用。
关于对象表示的小标准:
[basic.types.general]
The object representation of an object of type T is the sequence of N unsigned char objects taken up by the object of type T, where N equals sizeof(T). The value representation of an object of type T is the set of bits that participate in representing a value of type T. Bits in the object representation that are not part of the value representation are padding bits. For trivially copyable types, the value representation is a set of bits in the object representation that determines a value, which is one discrete element of an implementation-defined set of values. 35
35) The intent is that the memory model of C++ is compatible with that of ISO/IEC 9899 Programming Language C.
关于 FooIdentifier
和 BarIdentifier
是否保证彼此之间具有相同表示的一点点。
[class.mem.general]
The common initial sequence of two standard-layout struct ([class.prop]) types is the longest sequence of non-static data members and bit-fields in declaration order, starting with the first such entity in each of the structs, such that corresponding entities have layout-compatible types, either both entities are declared with the no_unique_address attribute ([dcl.attr.nouniqueaddr]) or neither is, and either both entities are bit-fields with the same width or neither is a bit-field.
Two standard-layout struct ([class.prop]) types are layout-compatible classes if their common initial sequence comprises all members and bit-fields of both classes ([basic.types]).
[basic.compound]
... Pointers to layout-compatible types shall have the same value representation and alignment requirements
classes 是布局兼容的,这听起来很有希望,但对语言规则影响不大。
挑战eerorika的回答,我相信你保证二进制兼容性。为此,我将参考 C++11 规范。
关键部分: [class/7] 这定义了一个标准布局 class。很明显,我们都同意这些是标准布局。
[介绍。object/5]和[介绍。object/6]
An object of trivially copyable or standard-layout type (3.9) shall occupy contiguous bytes of storage.
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies.
这限制了标准布局对象可以具有的形状,并指定了我们所谓的对象“地址”。
[class.mem/20]
A pointer to a standard-layout struct object, suitably converted using a reinterpret_cast, points to its initial member (or if that member is a bit-field, then to the unit in which it resides) and vice versa. [ Note: There might therefore be unnamed padding within a standard-layout struct object, but not at its beginning, as necessary to achieve appropriate alignment. —end note ]
这表示我们至少可以通过重新解释转换将 ID_t*
转换为 int*
。
现在,您断言 sizeof(ID_t) == sizeof(int)
。这是个好消息,因为它限制了您的选择。 int* someIdAsInt = reinterpret_cast<int*>(&someId)
保证成功,它将指向第一个成员,根据 class.mem。那么问题来了,可能返回的地址有哪些呢?显然,只有一个地址可能是sizeof(int)
字节的第一个字节,当然是someId
.
所以我们可以确定&someId
和someIdAsInt
指的是同一个地址。并且,特别是 someIdAsInt
必须 指向每个 class.mem.
如果我要执行 *someIdAsInt = 43
,结果必须与我执行 someId.raw_id = 43
时的结果相同,因为 someIdAsInt
指向 someId.raw_id
。无论我用这个指针做什么来掩盖它,这个陈述都必须是真实的。
这表示 *someIdAsInt
和 someId
要么必须具有相同的布局(允许赋值),要么编译器必须跟踪 someIdAsInt
的值,将其与正常int*
。这就是为什么我不同意 eerorika 的回答。该信息无法在带有类型标记的类型系统中处理(它会强制编译器能够跟踪标记,即使您做了诸如在线程之间传递 int*
之类的残酷事情)。因此,任何信息标记都必须烘焙到构成 int*
值的字节中。 C++ 规范没有说明指针值的格式。
但是,int*
的差异是有限制的,一般来说,std::memcpy
将一个int
的字节复制到另一个int
中,得到的整数必须是相同的值。据我所知,这实际上并没有写入规范,但它被(基本上?)所有程序员接受为 C 和 C++ 的普通法规则。事实上,在 C++20 中包含 std::bit_cast
进一步强调了这类事情。拥有两种无法通过字节区分的整数格式会破坏各种各样的事情。
因此,如果您在语言律师争论中接受这项普通法裁决,那么您的 ID_t
的布局必须与 int
的布局相同,如果 sizeof(ID_t) == sizeof(int)
。如果那个普通法裁决不被接受,那么......好吧......我只想说一些自我反省是为了=D
请注意,这 不 意味着您可以安全地走另一条路。如果你有一个 int
数组,你不能将它转换为 ID_t*
然后访问它们。这将违反严格的别名,因为首先在该内存地址中从来没有 ID_t
。但是,因为它们是相同的布局,使用 std::memcpy
或 std::bit_cast
转换为具有等效位模式的 ID_t
仍然是公平的游戏。