use*_*550 7 c strict-aliasing language-lawyer
以下示例是否违反了严格的别名规则?
在文件ac中
extern func_takes_word(uint32_t word);
void func(void *obj, size_t size_in_words)
{
for (int i = 0; i < size_in_words; i++)
func_takes_word(*(((uint32_t *)obj)+i)); // <--- Here
}
Run Code Online (Sandbox Code Playgroud)
在文件bc中
struct some_struct
{
uint32_t num_0;
uint32_t num_1;
uint32_t num_2;
};
extern void func(void *obj, size_t size_in_words);
void some_func(void)
{
struct some_struct stc = {0, 1, 59}; // assume no padding
func((void *)&stc, sizeof(struct some_struct)/sizeof(uint32_t));
}
Run Code Online (Sandbox Code Playgroud)
人们可以说存在违规,因为我发送了一个struct some_struct指针, func该指针将其转换为uint32_t指针而不是访问该值。
但是,由于func需要一个void指针,并且由于func与调用者位于不同的编译单元中,因此编译器无法“看到”这种违规。
那么下面的例子呢,根据我的理解,没有违规,并且完全符合严格的别名要求:
extern func_takes_word(uint32_t word);
void func(void *obj, size_t size_in_words)
{
uint32_t word;
for (int i = 0; i < size_in_words; i++)
{
// instead of calling memcpy, (or using union type punning)
// for learning purpose
*(char *)&word = *((char *)obj+ (sizeof(uint32_t) * i));
*(((char *)&word) + 1) = *((char *)obj+ (sizeof(uint32_t) * i)+1);
*(((char *)&word) + 2) = *((char *)obj+ (sizeof(uint32_t) * i)+2);
*(((char *)&word) + 3) = *((char *)obj+ (sizeof(uint32_t) * i)+3);
func_takes_word(word);
}
}
Run Code Online (Sandbox Code Playgroud)
我对么?
这主要是一个重复的问题,但无论如何我都会写出一个答案,因为我找不到更早的答案来讨论通过转换的具体问题void *和单独编译的具体问题。
首先,让我们想象一个更简单的代码版本:
#include <stddef.h>
#include <stdint.h>
extern void func_takes_word(uint32_t word);
struct __attribute__((packed, aligned(_Alignof(uint32_t)))) some_struct
{
uint32_t num_0;
uint32_t num_1;
uint32_t num_2;
};
void some_func(void)
{
struct some_struct stc = {0, 1, 59};
for (size_t i = 0; i < sizeof(struct some_struct) / sizeof(uint32_t); i++)
func_takes_word(*(((uint32_t *)&stc) + i));
}
Run Code Online (Sandbox Code Playgroud)
(GCC__attribute__((packed, aligned(...)))注释的存在只是为了排除由于填充或未对齐而导致任何问题的可能性。如果您将其删除,我下面所说的一切仍然是正确的。)
根据对 C2011 最直接的解释,此代码确实违反了“严格别名”规则(N1570:6.2.7和6.5p6,7)。该类型struct some_struct与该类型不兼容uint32_t。因此,获取具有声明的 type 的对象的地址struct some_struct,将结果指针转换为 type uint32_t *,添加非零偏移量,并取消引用转换指针,具有未定义的行为。确实就是这么简单。(编辑:如果指针没有偏移,则取消引用具有明确定义的行为,因为隐藏在第 6.7.2p15 节中的特殊情况规则我完全忘记了。感谢 dbush 指出了这一点。)
许多人愤怒地抵制对标准的这种解释,并坚持认为委员会一定有其他含义,因为有数百万行(如果不是数十亿行)“遗留”C 代码完全执行上述操作并期望它能够工作。更不用说还不清楚如何offsetof在这种解释下做任何有用的事情。但文本确实这么说了,没有其他合理的解释,并且自 1989 年最初的 ANSI C 以来,标准相关部分的措辞基本上没有变化。我认为我们必须假设委员会对改变标准缺乏兴趣。三十年来,尽管多次正式要求澄清或更正,文本仍然意味着它表达了他们想要表达的内容。
现在,关于强制转换void *和/或拆分操作,以便对象的原始“有效类型”对于执行取消引用的代码不可见:这些没有区别。 您的原始翻译单元对仍然具有未定义的行为。
强制转换void *没有任何区别,因为第 6.5.p6 节中的规则没有提及中间强制转换。他们只讨论内存中实际对象的“有效类型”,以及用于访问该对象的左值表达式的类型。因此,在获取对象地址和取消引用指针之间,指针的类型并不重要(只要没有任何强制转换会破坏信息,就保证不会发生这种情况)用于从对象类型转换为void *)。
拆分操作,使对象的原始“有效类型”对于执行解引用的代码不可见(静态),没有任何区别,因为 C 标准对编译器分析的复杂程度没有任何限制在决定是否允许访问之前允许执行。特别是,用“有效类型”标记内存的每个字节并对每个解引用执行运行时检查的实现已得到委员会的明确认可(不是在标准文本中,而是在 DR 响应中,我不这么认为)不记得这是多久以前的事了,WG14 的网站不太好搜索)。在翻译阶段 8(“链接时优化”)和阶段 7 期间,还允许实现进行任意激进的内联和过程间分析。将原始程序折叠成我的“更简单的版本”完全在当前的能力范围之内。生成整个程序优化编译器。
正如该问题的评论中所指出的,您可能能够依靠特定实现的优化器的复杂程度的知识,或者依赖于实现的明显扩展(例如__attribute__((noinline)))来控制您是否获得按预期行为的机器代码,尽管未定义的行为。C 标准甚至通过定义“一致程序”和“严格一致程序”之间的区别,明确许可您执行此操作(N1570:第 4 节)。依赖于某个特定实现对未定义行为的处理的程序仍然可以是一致的,但不是严格一致的,并且其作者必须意识到,当移植到不同的实现(可能包括相同的较新版本)时,它可能会崩溃。编译器)。