C 联合类型双关数组

C union type punning arrays

鉴于以下代码,我有一些与类型双关相关的问题。我看不出有任何方式表明这没有违反严格的别名规则,但我无法指出具体的违规行为。我最好的猜测是将联合成员传递给函数违反了严格的别名。

以下代码在Compiler Explorer.

#include <stdint.h>

union my_type
{
    uint8_t m8[8];
    uint16_t m16[4];
    uint32_t m32[2];
    uint64_t m64;
};

int func(uint16_t *x, uint32_t *y)
{
    return *y += *x;
}

int main(int argc, char *argv[])
{
    union my_type mine = {.m64 = 1234567890};
    return func(mine.m16, mine.m32);
}

我的观察:

我的问题:

Passing m16 and m32 into func must violate something.

func(uint16_t *x, uint32_t *y) 可以自由假设 *x*y 不重叠,因为 x, y 是足够不同的指针类型。由于引用的数据确实在 OP 的代码中重叠,我们遇到了问题。

有关 unions 和别名的特殊问题不适用于 func() 的正文,因为调用的 union-ness代码丢失。

替代的“安全”代码可能是:

// Use volatile to prevent folding these 2 lines of code.
// The key is that even with optimized code, 
// the sum must be done before *y assignment.
volatile uint32_t sum = *y + *x;
*y = sum;

return (int) (*y);

What exactly am I violating by passing the pointers into func?

将指针传递给函数 func() 无需考虑的重叠数据。


Is type punning with arrays like this valid?

我不认为这是一个数组或 union 问题,只是将指针传递给函数 func() 没有义务考虑的重叠数据之一。

What other gotchas am I missing in this example?

轻微:int 可能是 16 位,可能导致在 uint32_tint 的转换中实现定义的行为。


考虑两者的区别

uint32_t fun1(uint32_t *a, uint32_t *b);
uint32_t fun2(uint32_t * restrict a, uint32_t * restrict b);

fun1() 必须考虑重叠的可能性。 fun2() 不会。

违反的规则是C 2018 6.5.16.1 3:

If the value being stored in an object is read from another object that overlaps in any way the storage of the first object, then the overlap shall be exact and the two objects shall have qualified or unqualified versions of a compatible type; otherwise, the behavior is undefined.

具体来说,在 *y += *x 中,存储在 ymine.m16 指向的对象中的值是从另一个重叠的对象 mine.m32 中读取的mine.m16 的存储,但重叠不准确,对象也没有兼容的类型,无论限定符如何。

请注意,此规则适用于简单赋值,如 E1 = E2,而代码具有组件赋值,E1 += E2。然而,复合赋值 E1 += E2 在 6.5.16.2 3 中被定义为等同于 E1 = E1 + E2 除了左值 E1 只计算一次。

Is type punning with arrays like this valid?

是的,C 标准允许通过联合成员使用别名;读取除最后一个存储的成员之外的成员将重新解释新类型中的字节。但是,如果程序的行为由 C 标准定义,这并不能免除程序遵守其他规则的责任,特别是上面引用的规则。

What exactly am I violating by passing the pointers into func?

传递指针不违反任何规则。如上所述,使用指针的赋值违反了规则。

What other gotchas am I missing in this example?

如果我们改变func

int func(uint16_t *x, uint32_t *y)
{
    *y += 1;
    *x += 1;
    return *y;
}

那么 6.5.16.1 3 中的规则不适用,因为没有涉及重叠对象的赋值。并且没有违反 6.5 7 中的别名规则,因为 *y 是定义为用于访问它的类型的对象,uint16_t*x 是定义为用于访问它的类型的对象访问它,uint32_t。然而,如果这个函数被孤立地翻译(没有 union 定义可见),编译器被允许假设 *x*y 不重叠,所以它可能缓存 *y*y += 1; 和 return 生成的缓存值,不知道 *x += 1; 更改 *y 的事实。这是C标准的缺陷。

My observations:

  • Assuming the arguments to func do not alias each other, func does not violate strict aliasing.

不可靠。 so-called strict-aliasing 规则以用于访问给定对象的左值表示,相对于该对象的有效类型。 func() 的两个参数不需要为 彼此别名 来执行 func() 以产生 strict-aliasing 违规。示例:

uint32_t x = 0, y = 1;
func((uint16_t *)&x, &y);
// func will violate strict aliasing when it dereferences its first parameter

围绕函数参数相互别名的问题将是 restrict 限定指针的领域,您没有使用它。


  • In C, it is permissible to use a union for type punning.

是的,前提是通过联合 对象执行双关语。这包含在 C17 6.5/7 中,上述 strict-aliasing 规则:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

[...]

  • an aggregate or union type that includes one of the aforementioned types among its members

请注意,这与被访问的存储实际上位于联合对象内部无关,而是与用于访问它的左值类型相对于被访问对象的实际(有效)类型有关。


  • Passing m16 and m32 into func must violate something.

确实如此,尽管语言规范可能比现在更清楚。但是,它确实说:

The value of at most one of the members can be stored in a union object at any time.

(C17 6.7.2.1/16)

在您的特定示例中,mine.m16mine.m32 在调用时都没有存储值,但在任何情况下,至多其中一个可以有一个值。当 func 然后尝试读取存储在这些对象中的值时,结果未定义(因为它们实际上没有存储值)。

第 6.5.2.3/6 段规范中包含的内容支持该解释:

One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible.

如果无论哪个成员实际存储了值,通常都可以访问随机联合成员,则不需要此类特殊规定。


My questions:

  • Is type punning with arrays like this valid?

不是那样的,不。规范还允许数组 type-punning 的其他变体。


  • What exactly am I violating by passing the pointers into func?

调用本身并没有违反任何内容。获取联合成员的地址是合法的,即使当前没有存储值的成员也是如此,将结果指针值传递给函数也是合法的。但是,当使用这些参数调用时,函数 在尝试取消引用一个或两个指针时会提交 strict-aliasing 违规,如上所述。


  • What other gotchas am I missing in this example?

与您的其他答案之一相反,所提供的代码并不 运行 与第 6.5.16.1/3 段相冲突。存储在 *y 中的值不是 从重叠对象 *x 中读取的,而是该值与 *y 原始值的总和.该总和是计算得出的,而不是从对象中读取的,因此 6.5.16.1/3 不适用。但是您可能没有注意到,如果 func() 执行简单赋值而不是加号,它 违反 6.5.16.1/3