通过结构别名阵列

时间:2015-01-12 18:41:20

标签: c arrays struct language-lawyer strict-aliasing

我正在阅读ISO / IEC 9899:TC2中第6.5段的第7段。

它通过以下方式宽恕对对象的左值访问:

  

包含上述之一的聚合或联合类型   其成员之间的类型(包括,递归地,成员   subaggregate或contains union),

请参阅文件,了解上述'类型是但它们肯定包括对象的有效类型。

在以下标记为:

的部分中
  

此列表的目的是指定其中的一种情况   对象可能有也可能没有别名。

我读到这句话时说(例如)以下内容定义明确:

#include <stdlib.h>
#include <stdio.h>

typedef struct {
    unsigned int x;
} s;

int main(void){
    unsigned int array[3] = {73,74,75};

   s* sp=(s*)&array; 

   sp->x=80;

   printf("%d\n",array[0]);

   return EXIT_SUCCESS;
}

该程序应输出80。

我并不主张这是一个好的(或非常有用的)想法,并承认我在某种程度上以这种方式解释它,因为我无法想到其他意味着什么,并且可以&#39;相信这是一个毫无意义的句子!

那就是说,我看不出有理由禁止它。我们所知道的是该位置的对齐和内存内容与sp->x兼容,为什么不呢?

似乎可以说,如果我在结构的末尾添加{say} double y;,我仍然可以通过这种方式访问​​array[0] sp->x。< / p>

然而,即使数组大于sizeof(s),任何访问sp->y的尝试都会被拒绝。未定义的行为。

我可能会礼貌地要求人们说出那句话容忍的内容,而不是进入一个扁平的旋转喊叫严格混淆UB严格别名UB&#39;似乎经常是这些事情的方式。

3 个答案:

答案 0 :(得分:7)

这个问题的答案在提案中有所涉及:Fixing the rules for type-based aliasing我们将会看到,遗憾的是,2010年提出的提案未在Hedquist, Bativa, November 2010 minutes中得到解决。因此,C11不包含N1520的分辨率,因此这是一个未解决的问题:

  

似乎没有任何办法可以解决这个问题   在这次会议上解决了。提议方法的每个线程   导致更多问题。 2010年11月4日,星期四,上午1点48分。

     

行动 - 克拉克在

中做更多工作

N1520打开说(强调我的前进):

  

Richard Hansen指出了基于类型的别名问题   规则如下:

     

我的问题涉及6.5p7的子弹5的措辞(别名,因为它适用于工会/聚合)。除非我理解   有效类型不正确,似乎是union / aggregate   条件应适用于有效类型,而不是左值类型。

     

以下是一些更多细节:

     

以下面的代码片段为例:

union {int a; double b;} u;
u.a = 5;
     

根据我对有效类型(6.5p6)的定义的理解,位置&amp; u处对象的有效类型是union {int a;   双b;}。正在访问的左值表达式的类型   在&amp; u(在第二行)的对象是int。

     

根据我对兼容类型(6.2.7)的定义的理解,int与union {int a;不兼容;双b;},所以   6.5p7的子弹1和2不适用。 int不是签名或   联合类型的无符号类型,因此子弹3和4不适用。 INT   不是字符类型,因此子弹6不适用。

     

留下子弹5.但是,int不是聚合或联合   类型,所以子弹也不适用。这意味着以上   代码违反了别名规则,显然不应该这样做。

     

我认为应该重新说明子弹5,以表明如果   有效类型(不是左值类型)是聚合类型或联合类型   那么包含一个类型与lvalue类型兼容的成员   可以访问该对象。

     

实际上,他指出的是规则是不对称的   关于结构/联盟成员资格。我已经意识到这一点   情况,并认为这是一个(非紧急)问题,相当一些   时间。一系列示例将更好地说明问题。 (这些   例子最初是在Santa Cruz会议上提出的。)

     

根据我对有关别名是否有效的问题的经验   在类型限制方面,问题总是用语言来表达   循环不变性。这样的例子使问题变得非常尖锐   对焦。

适用于这种情况的相关示例是3,如下所示:

struct S { int a, b; };
void f3(int *pi, struct S *ps1, struct S const *ps2)
{
  for (*pi = 0; *pi < 10; ++*pi) {
      *ps1++ = *ps2;
  }
}
     

这里的问题是是否可以访问对象* ps2(和   特别修改)通过分配左值* pi - 如果是,   标准是否实际上是这样说的。可以说这是   不属于6.5p7的第五颗子弹,因为* pi没有   聚合类型。

     

也许意图是应该扭转这个问题:是   它允许通过左值* ps2访问对象* pi的值。   显然,这个案例将由第五个子弹涵盖。

     

关于这种解释,我只能说它从未发生过   直到圣克鲁斯会议,我才有可能,即使我已经   在整个过程中深入思考这些规则   很多年。即使这种情况可能被认为是由   现有的措辞,我建议可能值得寻找   不透明的配方。

以下讨论和建议的解决方案非常漫长且难以总结,但似乎最终删除了上述第五条,并通过调整6.5的其他部分来解决问题。但如上所述,所涉及的问题无法解决,我也没有看到后续提案。

因此,似乎标准的措辞确实似乎允许OP演示的情景,尽管我的理解是这是无意的,因此我会避免它,并且它可能在后来的标准中有可能改变为不符合。< / p>

答案 1 :(得分:0)

我认为此文不适用:

  

聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),

sp->x的类型unsigned int不是聚合或联合类型。

在您的代码中没有严格的别名违规:可以将unsigned int视为unsigned int

结构可能对数组有不同的对齐要求,但除此之外没有问题。

通过“聚合或联合类型”访问将是:

s t = *sp;

答案 2 :(得分:0)

我承认,我以这种方式在本地定义的数组上放置struct的想法是坦率的异国情调。 我仍然认为C99和所有后续标准都允许。 事实上,成员本身就是对象本身就是6.7.5中的第一个要点,这是非常有争议的:

  

与对象的有效类型兼容的类型

我认为这是M.M的观点。

从另一个方面来看问题,让我们注意到它是绝对合法的(在一个严格符合要求的环境中)将成员sp->x作为对象的别名。自己的权利。

在我的OP中的代码的上下文中考虑具有原型void doit(int* ip,s* sp);的函数,以下调用应该在逻辑上表现:

doit(&(sp->x),sp);

注意:程序逻辑可能(当然)可能无法按预期运行。例如,如果doit增加sp->x直到超过*ip,那么就会出现问题!但是,在一致的编译器中不允许的是,由于优化器忽略了混叠潜力,结果会被伪像破坏。

如果语言要求我编码,我认为C会更弱:

int temp=sp->x;
doit(&temp,sp);
sp->x=temp;

想象一下所有对任何函数的调用必须进行监管的情况,以便对传递的结构的任何部分进行潜在的别名访问。这种语言可能无法使用。

显然,如果doit()没有认识到ip可能是sp成员的别名,那么硬优化(即非兼容)编译器可能会生成sp->x的完整哈希值。 {1}}。 这与此讨论无关。

要说明编译器何时可以(并且不能)做出这样的假设,这被理解为标准需要围绕别名设置非常精确的参数的原因。这是为优化器提供一些可以忽略的条件。用低级语言,例如&#39; C&#39;可以合理地(甚至是期望的)说可以使用到可访问的有效位模式的适当对齐的指针来访问值。

绝对确定我的OP中的unsigned int指向一个保持有效doit()的正确对齐的位置。

智能问题在于编译器/优化器是否同意这是访问该位置的合法方式,或者可忽略为未定义的行为。

正如#include <stddef.h> #include <stdlib.h> #include <stdio.h> typedef enum { is_int, is_double //NB:TODO: support more types but this is a toy. } type_of; //This function allocates and 'builds' an array based on a provided set of types, offsets and sizes. //It's a stand-in for some function that (say) reads structures from a file and builds them according to a provided //recipe. int buildarray(void**array,const type_of* types,const size_t* offsets,size_t mems,size_t sz,size_t count){ const size_t asize=count*sz; char*const data=malloc(asize==0?1:asize); if(data==NULL){ return 1;//Allocation failure. } int input=1;//Dummy... const char*end=data+asize;//One past end. Make const for safety! for(char*curr=data;curr<end;curr+=sz){ for(size_t i=0;i<mems;++i){ char*mem=curr+offsets[i]; switch(types[i]){ case is_int: *((int*)mem)=input++;//Dummy...Populate from file... break; case is_double: *((double*)mem)=((double)input)+((double)input)/10.0;//Dummy...Populate from file... ++input; break; default: free(data);//Better than returning an incomplete array. Should not leak even on error conditions. return 2;//Invalid type! } } } if(array!=NULL){ *array=data; }else{ free(data);//Just for fun apparently... } return 0; } typedef struct { int a; int b; double c; } S; int main(void) { const type_of types[]={is_int,is_int,is_double}; const size_t offsets[]={offsetof(S,a),offsetof(S,b),offsetof(S,c)}; S* array=NULL; const size_t size=4; int err=buildarray((void **)&array,types,offsets,3,sizeof(S),size); if(err!=0){ return EXIT_FAILURE; } for(size_t i=0;i<size;++i){ printf("%zu: %d %d %f\n",i,array[i].a,array[i].b,array[i].c); } free(array); return EXIT_SUCCESS; } 示例所示,它绝对确定了一个结构可以被分解并被视为仅具有特殊关系的单个对象。

这个问题似乎是关于一群恰好具有这种特殊关系的成员可以拥有一种结构的情况。

我认为大多数人会同意这个答案底部的程序执行有效的,有价值的功能,如果与某些I / O库相关联,可以“抽象”。阅读和编写结构所需的大量工作。 您可能认为有更好的方法,但我并不期望很多人认为这不是一种不合理的方法。

它完全按照这种方式运行 - 它按成员构建一个结构成员,然后通过该结构访问它。

我怀疑一些反对OP中代码的人对此更加放松。 首先,它运行在从免费商店分配的内存中,作为“未键入的”内容。普遍对齐的存储。 其次,它构建了一个完整的结构。在OP中我指出规则(至少看起来允许)你可以排列结构的位,只要你只取消引用那些位,一切都没问题。

我有点赞同这种态度。我认为OP在标准的一个写得不好的角落里有点不正常和语言延伸。不适合穿衬衫。

但是,我绝对认为禁止下面的技术是错误的,因为它们排除了一种逻辑上非常有效的技术,可以识别结构可以从对象构建,就像分解它们一样。

然而,我会说这样的事情是我唯一能想出的方法,这种方法似乎值得。但另一方面,如果你不能将数据分开和/或将它们放在一起,那么你很快就会开始打破C结构的概念POD--它们各部分的可能填充的总和,仅此而已。< / p>

{{1}}

我认为这是一个有趣的紧张局势。 C旨在成为低级高级语言,并使程序员几乎可以直接访问机器操作和内存。 这意味着程序员可以满足硬件设备的任意需求并编写高效的代码。 但是,如果程序员被给予绝对控制权,例如我的观点,如果它适合它,那么就可以了。处理别名然后优化器让它的游戏变坏。 所以奇怪的是,值得保留一点性能,以便从优化器返回一个股息。

C99标准的第6.5节尝试(并且没有完全成功)设置该边界。