c语言 - 为什么严格的别名规则不适用于 int* 和无符号*?



在 C 语言中,我们不能使用与该对象的有效类型不兼容的 lvalue 表达式访问对象,因为这会导致未定义的行为。基于这一事实,严格的别名规则规定,如果两个指针具有不兼容的类型,则它们不能相互别名(引用内存中的同一对象)。但是在 C11 标准的p6.2.4中,允许访问具有有符号版本右值的无符号有效类型,反之亦然。

由于最后一段,两个指针int *aunsigned *b可能会相互别名,其中一个指向的对象值的变化可能会导致另一个指向的对象的值发生变化(因为它是同一个对象)。

让我们在编译器级别演示一下:

int f (int *a, unsigned *b)
{
*a = 1;
*b = 2;
return *a;
}

上述函数的生成程序集在带有-O2的 GCC 6.3.0 上如下所示:

0000000000000000 <f>:
0:   movl   $0x1,(%rdi)
6:   movl   $0x2,(%rsi)
c:   mov    (%rdi),%eax
e:   retq  

这是意料之中的,因为 GCC 不会优化返回值,并且在写入*b后仍然*a读取值(因为*b的变化可能会导致*a的变化)。

但是有了这个其他功能:

int ga;
unsigned gb;
int *g (int **a, unsigned **b)
{
*a = &ga;
*b = &gb;
return *a;
}

生成的程序集非常令人惊讶(GCC -O2):

0000000000000010 <g>:
10:   lea    0x0(%rip),%rax        # 17 <g+0x7>
17:   lea    0x0(%rip),%rdx        # 1e <g+0xe>
1e:   mov    %rax,(%rdi)
21:   mov    %rdx,(%rsi)
24:   retq 

返回值经过优化,写入*b后不再读取。我知道int *aunsigned *b不是兼容的类型,但是第 P6.2.4段中的规则呢(允许使用带符号版本右值访问无符号有效类型,反之亦然)?为什么它不适用于这种情况?为什么编译器在这种情况下进行这种优化?

关于兼容类型和严格混叠的整个故事,我有些不明白。有人可以启发我们吗?(请解释为什么两个指针的类型不兼容,但可以相互别名,想想int *aunsigned *b)。

给定int **aunsigned **b*a的类型不是对应于有效*b类型的有符号或无符号类型,*b也不是对应于有效*a类型的有符号或无符号类型。因此,此规则允许通过相应的有符号或无符号类型进行别名不适用。由于没有其他允许混叠的规则适用,编译器有权假定写入*b不会修改*a,因此编译器在*a = &ga;中写入*a的值仍然存在于return *a;语句的*a中。

int *指向已签名int这一事实不会使其成为有符号类型。这是一个指针。int *unsigned *是指向不同类型的指针。即使它们被认为是有符号的或无符号的,它们也会是指向不同类型的有符号或无符号指针:如果int *是有符号指针,它将是指向int的有符号指针,而相应的无符号版本将是指向int的无符号指针,而不是指向unsigned的任何指针。

要了解有符号/无符号豁免的预期含义,必须首先了解这些类型的背景。 C 语言最初没有"无符号"整数类型,而是设计用于二进制补码机器,在溢出时具有安静的环绕。 虽然有一些操作,最明显的是关系运算符、除法、余数和右移,其中有符号和无符号行为会有所不同,但对有符号类型执行大多数操作将产生与对无符号类型执行相同操作相同的位模式,从而最大限度地减少对后者的需求。

尽管无符号类型即使在 quiet-wraparound two 的补码机器上也肯定很有用,但在不支持 quiet-wraparound two 的补码语义的平台上,它们是必不可少的。 然而,由于 C 最初不支持此类平台,因此许多逻辑上"应该"使用无符号类型的代码,如果它们更早存在,就会使用它们,因此被编写为使用有符号类型。 该标准的作者不希望类型访问规则在使用有符号类型的代码之间造成任何困难,因为无符号类型在编写时不可用,而使用无符号类型的代码是可用的,并且它们的使用是有意义的。

交替处理intunsigned的历史原因同样适用于允许使用unsigned*类型的左值访问int*类型的对象,反之亦然,int**使用unsigned**等进行访问。 虽然该标准没有明确规定应该允许任何此类用法,但它也忽略了其他一些显然应该被允许的用途,因此不能合理地被视为完全和完整地描述了实现应该支持的所有内容。

该标准未能区分涉及基于指针的类型双关语的两种情况 - 涉及混叠的情况和不涉及混叠的情况 - 除了一个非规范性的脚注,说明规则的目的是指示事物何时可能别名。 区别说明如下:

int *x;
unsigned thing;
int *usesAliasingUnlessXandPDisjoint(unsigned **p)
{
if (x)
*p = &thing;
return x;
}

如果x*p识别相同的存储,则*px之间会有混叠,因为p的创建和通过*p写入将被使用左值x对存储的冲突访问分开。 但是,给定如下内容:

unsigned thing;
unsigned writeUnsignedPtr(unsigned **p)
{ *p = &thing; }
int *x;
int *doesNotUseAliasing(void)
{
if (x)
writeUnsignedPtr((unsigned**)&x);
return x;
}

*p参数和x之间不会有混叠,因为在传递的指针p的生命周期内,x或任何其他不是从p派生的指针或左值都不能用于访问与*p相同的存储。 我认为很明显,标准的作者希望允许后一种模式。 我认为不太清楚他们是否想允许前者,即使是signedunsigned类型的左值 [而不是signed*unsigned*],或者没有意识到将规则的应用限制在实际涉及别名的情况下就足以允许后者。

gcc 和 clang 解释混叠规则的方式并没有将intunsigned之间的兼容性扩展到int*unsigned*——鉴于标准的措辞,这种限制是允许的,但至少在不涉及混叠的情况下,我认为这与标准声明的目的背道而驰。

您的特定示例确实涉及在*a*b重叠的情况下使用别名,因为a要么是首先创建的,并且在此类创建和最后一次使用*a之间发生了通过*b的冲突访问,或者首先创建了b,并且在此类创建和最后一次使用b之间发生了通过*a的冲突访问。 我不确定该标准的作者是否打算允许这种使用,但证明允许intunsigned的相同原因同样适用于int*unsigned*。 另一方面,gcc 和 clang 的行为似乎不是由标准作者想要表达的内容决定的,正如已发布的理由所表明的那样,而是由他们未能要求编译器做什么决定的。