使用联合体代替强制类型转换是否定义良好?

16

今天早上我和同事讨论了一下一个用于检测字节序的“编程技巧”的正确性。

这个技巧是:

bool is_big_endian()
{
  union
  {
    int i;
    char c[sizeof(int)];
  } foo;


  foo.i = 1;
  return (foo.c[0] == 1);
}

在我看来,这种使用 union 的方式是不正确的,因为设置联合体的一个成员并读取另一个成员是不明确定义的。但我必须承认,这只是一种感觉,我缺乏实际证据来支持我的观点。

这个技巧是正确的吗?谁是正确的?


5
至少gcc开发人员推荐这种方法用于类型转换,而不是将其强制转换为另一种类型,因为后者的定义更加模糊不清 :-) - Gunther Piez
@drhirsch:我同意。但如果它只是一个伪装的转换,那么它不应该也是一样糟糕的吗? - ereOn
1
它不仅仅是一个伪装的转换。与简单的转换相比,它在联合体元素的内存位置方面具有更精确的语义。关于这个问题,可以查看http://gcc.gnu.org/onlinedocs/gcc-4.6.0/gcc/Optimize-Options.html#index-fstrict_002daliasing-824 - Gunther Piez
确实,对于这种无意义的东西,你永远不会得到一个完全定义的解决方案 :P - Lightness Races in Orbit
“#define BIG_ENDIAN” 不是比这个更容易吗?在大多数系统上,您仍然需要检测操作系统和其他一些东西。 - Bo Persson
@Bo Persson:不幸的是,在一些架构上(IBM),你必须在运行时检测字节序才能实现可靠的工作。 - ereOn
5个回答

13

你的代码不具备可移植性。它可能适用于某些编译器,也可能不适用。

你关于尝试访问联合体的非活动成员的行为是未定义的[就像给出的代码一样]是正确的

$9.5/1

在联合体中,最多只能有一个数据成员在任何时候处于活动状态,也就是说,在任何时候,联合体中最多只能存储一个数据成员的值。

因此,foo.c[0] == 1 是不正确的,因为此时c并不是活动成员。如果您认为我错了,请随意纠正我。


2
我在C++0x标准中找不到这个引用,但我找到了一个部分解除此限制的引用,例如 struct A { int a; char b; }; struct B { int a; double c; }; union U { A a; B b; }; 并且指出访问 u.a.au.b.a 总是可行的(因为它是A和B的公共前缀序列),但没有关于未定义性的说明 :/ - Matthieu M.
6
我认为在这个上下文中相关的是 "$9.5 在一个联合体中,任何时候最多只能有一个数据成员处于活动状态,也就是说,在联合体中最多只能存储一个数据成员的值。" 尝试访问未激活的联合体成员将导致未定义的行为。 - Prasoon Saurav
5
“@drhirsch: 有明确定义,也有明确定义。就标准而言,行为是未定义的。当然,个别编译器可能会提供额外的保证。” - Dennis Zickefoose
2
MISRA-C标准禁止使用联合体,因为存在各种可移植性问题,主要涉及对齐和填充。例如,它们禁止使用“变体”。然而,他们对规则做了一个例外,并允许使用联合体进行数据打包,其中一个char数组的大小与联合体中的其他数据类型相同。这是有道理的,因为在C/C++语言中,数据打包很可能是联合关键字的唯一合理用途。如果这是未定义行为,那么联合将变得无用。如果是这种情况,它们应该从语言中删除。 - Lundin
1
@Lundin: 或者你可以使用 boost::variant,它构建了一个类型安全的联合,并且在任何时候都知道哪些数据是活动的...并防止您访问其他数据。如果程序的逻辑条件是在任何时候恰好有N个字段是活动的,则 boost::variant 是此属性的逻辑表达式。 - Matthieu M.
显示剩余8条评论

7

提醒自己:这里有很多其他方法 https://dev59.com/_XNA5IYBdhLWcg3wVcT6 - moooeeeep
特别适用于嵌入式环境!! - Craig.Feied

2

你说得没错,那段代码没有明确定义的行为。以下是如何实现可移植性:

#include <cstring>

bool is_big_endian()
{
    static unsigned const i = 1u;
    char c[sizeof(unsigned)] = { };
    std::memcpy(c, &i, sizeof(c));
    return !c[0];
}

// or, alternatively

bool is_big_endian()
{
    static unsigned const i = 1u;
    return !*static_cast<char const*>(static_cast<void const*>(&i));
}

1
我认为即使这样也不是在所有情况下都可移植的。在以中位结束的架构上,它会返回 true,这将导致第三个字节中出现 1。那么为什么还要费心呢?我仍然会使用 union,知道它可能不完美,但至少不会在错误的假设下让我的代码在所有情况下都能正常工作。 - Gunther Piez

0

这个函数应该被命名为is_little_endian。我认为你可以使用这个联合技巧,或者也可以将其转换为char。


0

代码存在未定义的行为,尽管一些(大多数?)编译器会定义它,至少在有限的情况下。

标准的意图是使用reinterpret_cast来实现这个功能。然而,这个意图并没有很好地表达出来,因为标准无法真正定义行为;当硬件不支持它时(例如由于对齐问题),没有必要定义它。同时,也很明显你不能仅仅通过reinterpret_cast在两个任意类型之间进行转换并期望它能够工作。

从实现质量的角度来看,如果unionreinterpret_cast在同一个函数块中,我希望两种方法都能够工作,如果编译器可以看到最终类型是一个union的话,union应该能够工作(尽管我曾经使用过这种情况不成立的编译器)。


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