使用联合体在两个具有共同初始序列的结构体之间进行转换是否是合法且定义良好的行为(见示例)?

17

我有一个API,其中包含一个公共结构A和一个内部结构B,并且需要能够将结构B转换为结构A。以下代码是否在C99(和VS 2010/C89)和C++03/C++11中合法且具有良好定义的行为?如果是,请解释什么使它具有良好的定义。如果不是,则转换这两个结构之间最有效和跨平台的方法是什么?

struct A {
  uint32_t x;
  uint32_t y;
  uint32_t z;
};

struct B {
  uint32_t x;
  uint32_t y;
  uint32_t z;
  uint64_t c;
};

union U {
  struct A a;
  struct B b;
};

int main(int argc, char* argv[]) {
  U u;
  u.b.x = 1;
  u.b.y = 2;
  u.b.z = 3;
  u.b.c = 64;

  /* Is it legal and well defined behavior when accessing the non-write member of a union in this case? */
  DoSomething(u.a.x, u.a.y, u.a.z);

  return 0;
}


更新

我简化了示例并编写了两个不同的应用程序。其中一个基于memcpy,另一个使用union。


Union:

struct A {
  int x;
  int y;
  int z;
};

struct B {
  int x;
  int y;
  int z;
  long c;
};

union U {
  struct A a;
  struct B b;
};

int main(int argc, char* argv[]) {
  U u;
  u.b.x = 1;
  u.b.y = 2;
  u.b.z = 3;
  u.b.c = 64;
  const A* a = &u.a;
  return 0;
}


memcpy:

#include <string.h>

struct A {
  int x;
  int y;
  int z;
};

struct B {
  int x;
  int y;
  int z;
  long c;
};

int main(int argc, char* argv[]) {
  B b;
  b.x = 1;
  b.y = 2;
  b.z = 3;
  b.c = 64;
  A a;
  memcpy(&a, &b, sizeof(a));
  return 0;
}



调试模式下的程序集剖析 [DEBUG] (Xcode 6.4, 默认C++编译器):

以下是调试模式下程序集中相关的差异。当我对发布版本进行剖析时,程序集没有差异。


Union:

movq     %rcx, -48(%rbp)


memcpy:

memcpy()函数:将源地址所指向的一段内存区域拷贝到目标地址所指向的内存区域,大小为指定的字节数。
movq    -40(%rbp), %rsi
movq    %rsi, -56(%rbp)
movl    -32(%rbp), %edi
movl    %edi, -48(%rbp)



注意:

基于union的示例代码会产生一个警告,指出变量'a'未被使用。由于分析的汇编代码来自调试模式,我不知道是否有任何影响。


1
我认为那甚至无法编译通过。struct C是一个不完整的类型,这使得struct B也成为了不完整的类型。这又导致union U也是不完整的类型。在我所知道的范围内,没有办法将U/B暴露出来而隐藏C。你只能拥有指向不完整类型的指针。 - kaylum
2
我猜你会收到很多理论性的答案,比如“C++中未定义的行为”、“在C99中有效”等等。然而,我非常确定你的代码在实践中会正常工作,我看不出编译器出问题的任何原因。 - stgatilov
1
@stgatilov:我可以想象优化会破坏这个,除非你声明volatile struct...的联合 - 对一个结构体的更改被缓存在寄存器中,不会刷新到RAM,然后通过“另一侧”读取(旧的)RAM值。 - SF.
1
@SF。我指出您对volatile修饰符的理解是错误的,因为我不希望其他读者被您的评论所迷惑。 - Coder
1
@SF。我只是想说,你建议使用“volatile”来确保原子行为是不正确的。请参见:https://dev59.com/4mw15IYBdhLWcg3wbLDU。那里是未来评论的更好场所。 - Coder
显示剩余22条评论
2个回答

11

这是可以的,因为您访问的成员是共同初始序列的元素。

C11(6.5.2.3 结构和联合成员; 语义):

[...] 如果一个联合包含多个结构体,它们共享一个公共初始序列(见下文),并且如果联合对象当前包含其中之一,则允许在可见联合的已完成类型声明的任何位置检查其中任何一个的公共初始部分。如果对应成员具有兼容类型(对于位域,具有相同的宽度)用于一系列或多个初始成员,则两个结构体共享一个公共初始序列

C++03 ([class.mem]/16):

如果POD-联合包含两个或更多共享一个公共初始序列的POD结构,并且如果POD-联合对象当前包含其中之一,则允许检查其中任何一个的公共初始部分。如果对应成员具有布局兼容类型(对于位域,具有相同的宽度)用于一系列或多个初始成员,则两个POD结构共享一个公共初始序列

两个标准的其他版本使用类似的语言; 自C ++ 11以来,使用的术语是标准布局而不是 POD


我认为混淆可能是因为C允许通过联合进行类型拼接(别名不同类型的成员),而C ++不允许;这是确保C / C ++兼容性的主要情况下,您必须使用 memcpy 。但在您的情况下,您正在访问具有相同类型并且前面是具有兼容类型的成员,因此类型拼接规则不相关。


你能提供你的来源链接吗? - Coder
@Codorilla 请查看 https://dev59.com/wnVD5IYBdhLWcg3wHnyd - ecatmur
1
个人而言,我会将memcpy保留给在不同类型成员之间进行转换的情况;如果在允许使用联合方法的代码中看到memcpy,我会感到惊讶。正如你所说,优化编译器可以“看穿”memcpy,但并不总是运行优化代码——当在调试版本中单步执行时,它会妨碍调试过程。 - ecatmur
1
还有一个问题是,联合的行为是明确定义的,而使用memcpy是否高效则是实现定义的。在调试模式下进行分析汇编可以清楚地看到这一点。在这种情况下使用联合可以跨平台提供更强的性能保证。 - Coder
1
@underscore_d 我想它允许更积极的优化;C 可以假设函数参数 S* sT* t 不会别名,即使它们共享一个公共初始序列,只要没有 union { S; T; } 在视野中,而 C++ 只能在链接时做出这种假设。也许值得就这种差异提出一个单独的问题。 - ecatmur
显示剩余36条评论

5

在C和C++中都是合法的

例如,在C99(6.5.2.3/5)和C11(6.5.2.3/6)中:

为了简化联合的使用,提供了一个特殊的保证:如果一个联合包含多个共享公共初始序列的结构(下面详细介绍),并且如果联合对象当前包含其中之一,那么允许在可见联合的完整类型声明的任何位置检查它们任何一个的公共初始部分。如果相应成员具有兼容类型(对于位字段,具有相同的宽度)的初始成员序列,则两个结构共享一个公共初始序列。

C++11和C++14中也存在类似的规定(措辞不同,意义相同)。


尽管C++并没有明确规定“联合体的完整类型声明必须可见”,但这可能会有重大影响。 - underscore_d
@underscore_d:即使完整的联合声明可见,gcc也不会保证指向联合成员类型的指针。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接