这种未定义行为背后至少有(或者在C90中曾经有)两个动机。其一是编译器可以生成额外的代码来跟踪联合体中的内容,并在访问了错误的成员时生成信号。实际上,我不认为有人曾经这么做过(也许CenterLine公司有?)。另一个动机是优化的可能性,这也是被使用的。我用过的某些编译器会推迟写入,直到最后一刻,因为它可能是不必要的(因为变量超出范围了,或者后续写入了不同的值)。从逻辑上讲,人们本能地期望在联合体可见时关闭此优化,但在Microsoft C的早期版本中并没有这样做。
类型转换方面的问题比较复杂。C委员会(在1980年代末)基本上采取了这样一个立场,即应该使用强制转换(在C++中是reinterpret_cast),而不使用联合体,尽管当时两种技术都很常见。自那时以来,一些编译器(例如g++)已经持相反的观点,支持使用联合体,但不支持使用强制转换。实际上,如果此时不存在类型转换,那么这两种方法都无法正常工作。这可能是g++持此观点的动机。如果访问联合体成员,那么可能存在类型转换就会立即变得明显。但当然,如果有这样一个例子:
int f(const int* pi, double* pd)
{
int results = *pi;
*pd = 3.14159;
return results;
}
调用方式:
union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );
根据标准的严格规则,这是完全合法的,但在g ++(以及可能许多其他编译器)中会失败。当编译f时,编译器假设pi和pd不能别名,并重新排序对* pd的写入和对*pi的读取。(我认为从来没有想过保证这一点。但是标准的当前措辞确实保证了这一点。)
编辑:
由于其他答案认为其行为实际上已定义(主要基于引用非规范说明的论点,该说明被断章取义),因此正确答案是pablo1977的答案:标准并未尝试定义涉及类型转换时的行为。这样做的可能原因是没有便携式行为可以定义。这并不妨碍特定的实现定义它;尽管我不记得有关该问题的任何具体讨论,但我非常确定意图是实现定义某些内容(如果不是全部,则大部分都是)。
关于使用联合进行类型转换:当C委员会开发C90(在1980年代末期)时,很明显希望允许进行额外检查的调试实现(例如使用fat指针进行边界检查)。从当时的讨论中可以清楚地看出,意图是调试实现可能会缓存有关联合中最后一个初始化的值的信息,并在尝试访问其他任何内容时截获。这在§6.7.2.1 / 16中明确说明:“联合对象中最多可以存储一个成员的值。”访问不在那里的值是未定义行为;它可以被 assimilated 访问未初始化的变量。(当时有一些关于访问具有相同类型的不同成员是否合法的讨论。我不知道最终解决方案是什么;在大约1990年之后,我转向了C ++。)
关于从C89引用行为是实现定义的语句:在第3节(术语,定义和符号)中发现它似乎非常奇怪。我必须在家里查看我的C90副本;其已在后来版本的标准中删除的事实表明委员会认为它的存在是错误的。
标准支持的使用union的方式是模拟派生。可以定义:
struct NodeBase
{
enum NodeType type;
};
struct InnerNode
{
enum NodeType type;
NodeBase* left;
NodeBase* right;
};
struct ConstantNode
{
enum NodeType type;
double value;
};
union Node
{
struct NodeBase base;
struct InnerNode inner;
struct ConstantNode constant;
};
即使Node是通过inner
初始化的,也可以合法地访问base.type。(§6.5.2.3/6以“做出一个特殊保证...”开始,并明确允许这一点,这非常强烈地表明所有其他情况都应视为未定义行为。当然,§4/2中还有这样的声明:“本国际标准中的‘未定义行为’或未明确定义行为的省略否则表明未定义行为”,为了证明行为不是未定义的,您必须展示在标准中它在哪里被定义。)
最后,关于类型转换:所有(或至少我用过的所有)实现都以某种方式支持它。当时我的印象是,指针转换是实现支持它的方式;在C++标准中,甚至有(非规范性)文本建议对于熟悉底层架构的人来说,reinterpret_cast
的结果是“不令人惊讶的”。然而,在实践中,大多数实现支持使用union进行类型转换,前提是通过union成员进行访问。大多数实现(但不包括g++)还支持指针转换,前提是指针转换对编译器明显可见(对于某些未指定的指针转换定义)。而底层硬件的“标准化”意味着像这样的事情:
int
getExponent( double d )
{
return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}
实际上,它们相当具有可移植性(当然在大型机上不起作用)。
不起作用的是像我第一个例子那样的东西,其中别名对编译器是不可见的。(我非常确定这是标准中的缺陷。我记得甚至看到过一个关于此的DR。)