通过结构别名数组

Aliasing Arrays through structs

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

它允许通过以下方式对对象进行左值访问:

an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union),

'aforementioned' 类型请参考文档,但它们肯定包括对象的有效类型。

它位于标记为:

的部分中

The intent of this list is to specify those circumstances in which an object may or may not be aliased.

我读到的是(例如)以下定义明确:

#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。

我并不是在提倡这是一个好(或非常有用)的想法,我承认我在一定程度上是这样解释的,因为我想不出这还有什么意思,也不相信这是一个毫无意义的句子!

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

似乎说如果我在结构的末尾添加(比如)一个 double y; 我仍然可以通过这种方式通过 sp->x 访问 array[0] .

然而,即使数组大于 sizeof(s),任何访问 sp->y 的尝试都是 'all bets off' 未定义的行为。

我是否可以礼貌地请人们说出这句话宽恕的内容,而不是平淡地大喊大叫'strict aliasing UB strict aliasing UB',因为这些事情似乎太常见了。

我认为这段文字不适用:

an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union),

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

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

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

通过 "an aggregate or union type" 访问将是:

s t = *sp;

这个问题的答案包含在提案中:Fixing the rules for type-based aliasing which we will see, unfortunately was not resolved in 2010 when the proposal was made which is covered in Hedquist, Bativa, November 2010 minutes。因此 C11 不包含 N1520 的解决方案,因此这是一个悬而未决的问题:

There does not seem to be any way that this matter will be resolved at this meeting. Each thread of proposed approaches leads to more questions. 1 1:48 am, Thurs, Nov 4, 2010.

ACTION – Clark do more work in

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

Richard Hansen pointed out a problem in the type-based aliasing rules, as follows:

My question concerns the phrasing of bullet 5 of 6.5p7 (aliasing as it applies to unions/aggregates). Unless my understanding of effective type is incorrect, it seems like the union/aggregate condition should apply to the effective type, not the lvalue type.

Here are some more details:

Take the following code snippet as an example:

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

From my understanding of the definition of effective type (6.5p6), the effective type of the object at location &u is union {int a; double b;}. The type of the lvalue expression that is accessing the object at &u (in the second line) is int.

From my understanding of the definition of compatible type (6.2.7), int is not compatible with union {int a; double b;}, so bullets 1 and 2 of 6.5p7 do not apply. int is not the signed or unsigned type of the union type, so bullets 3 and 4 do not apply. int is not a character type, so bullet 6 does not apply.

That leaves bullet 5. However, int is not an aggregate or union type, so that bullet also does not apply. That means that the above code violates the aliasing rule, which it obviously should not.

I believe that bullet 5 should be rephrased to indicate that if the effective type (not the lvalue type) is an aggregate or union type that contains a member with type compatible with the lvalue type, then the object may be accessed.

Effectively, what he points out is that the rules are asymmetrical with respect to struct/union membership. I have been aware of this situation, and considered it a (non-urgent) problem, for quite some time. A series of examples will better illustrate the problem. (These examples were originally presented at the Santa Cruz meeting.)

In my experience with questions about whether aliasing is valid based on type constraints, the question is invariably phrased in terms of loop invariance. Such examples bring the problem into extremely sharp focus.

适用于这种情况的相关示例是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;
  }
}

The question here is whether the object *ps2 may be accessed (and especially modified) by assigning to the lvalue *pi — and if so, whether the standard actually says so. It could be argued that this is not covered by the fifth bullet of 6.5p7, since *pi does not have aggregate type at all.

**Perhaps the intention is that the question should be turned around: is it allowed to access the value of the object pi by the lvalue ps2. Obviously, this case would be covered by the fifth bullet.

All I can say about this interpretation is that it never occurred to me as a possibility until the Santa Cruz meeting, even though I've thought about these rules in considerable depth over the course of many years. Even if this case might be considered to be covered by the existing wording, I'd suggest that it might be worth looking for a less opaque formulation.

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

所以标准措辞似乎允许 OP 演示的场景,尽管我的理解是这是无意的,因此我会避免它,并且它可能会在以后的标准中改变为不符合标准。

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

a type compatible with the effective type of the object

我认为这是 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;

想象一下在所有情况下,必须对对任何函数的任何调用进行监管,以防止对传递的结构的任何部分进行潜在的别名访问。这样的语言估计没法用了。

显然,如果编译器无法识别 ip 可能是位于 sp。 这与本次讨论无关。

规定编译器何时可以(和不能)做出此类假设被理解为标准需要围绕别名设置非常精确的参数的原因。也就是给优化器一些条件打折。在诸如 'C' 之类的低级语言中,说可以使用指向可访问有效位模式的适当对齐的指针来访问值是合理的(甚至是可取的)。

绝对确定我的 OP 中的 sp->x 指向一个正确对齐的位置,其中包含一个有效的 unsigned int

聪明的问题是 compiler/optimizer 是否同意这是访问该位置的合法方式,或者作为未定义的行为可忽略。

doit() 示例所示,可以绝对确定结构可以分解并作为单独的对象处理,这些对象只是碰巧具有特殊关系。

这个问题似乎是关于碰巧具有这种特殊关系的一组成员可以具有结构 'laid over them' 的情况。

我想大多数人都会同意这个答案底部的程序执行有效的、有价值的功能,如果与某些 I/O 库相关联,可以 'abstract' 完成大量需要的工作读写结构。 您可能认为有更好的方法,但我并不期望很多人认为这不是不合理的方法。

它的运行方式完全相同 - 它逐个成员构建结构,然后通过该结构访问它。

我怀疑一些反对 OP 中代码的人对此比较放松。 首先,它对从自由存储分配的内存作为 'un-typed' 通用对齐存储进行操作。 其次,它构建了一个整体结构。在 OP 中,我指出了规则(至少看起来允许),你可以排列结构的位,只要你只取消引用这些位,一切都可以。

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

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

但是我要说的是,在这种方法似乎值得的地方,这是我唯一能想到的东西。但另一方面,如果你不能将数据分开 AND/OR 将它们放在一起,那么你很快就会开始打破 C 结构的概念是 POD - 它们各部分的可能填充总和,仅此而已。

#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;
}

我认为这是一种有趣的紧张关系。 C旨在成为低级高级语言,让程序员几乎可以直接访问机器操作和内存。 这意味着程序员可以满足硬件设备的任意需求并编写高效的代码。 然而,如果程序员被给予绝对控制权,例如我关于“如果适合就可以”的别名方法的观点,那么优化器就会破坏它的游戏。 奇怪的是,为了 return 优化器的红利,保留一点性能是值得的。

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