是否允许转换和取消引用 "compatible" 结构的结构指针?

Is casting and dereferencing struct pointers of "compatible" structs allowed?

假设我有这样的东西:

在list.h中:

//...
#include <stdlib.h>
typedef struct node_s{
    struct node_s *next;
    struct node_s *prev;

    char data[];
}node_t;

void* getDataFromNode(node_t *node){
    return(node->data);
}

node_t* newNode(size_t size){
    node_t *ret = malloc(sizeof(node_t));
    return(ret);
}
//...

在main.c中:

#include "list.h"
#include <stddef.h>
typedef struct float_node_s{
    struct foo_node_s *next;
    struct foo_node_s *prev;

    float someFloat;
}float_node_t;

int main(void){
    float *f;
    float_node_t *node;
    //1)
    node = (float_node_t*)newNode(sizeof(float_node_t));
    if(node == NULL){
        return(1);
    }
    //2)
    f = (float*)getDataFromNode((node_t*)node);
    return(0);
}

这是我在很多 C list/tree/etc 中看到的东西。实现。

我可以这样做吗?

具体来说,我可以将 node_t 指针转换为 float_node_t 指针并将其分配给 float_node_t 指针变量,如 1) 所示吗?如果我现在取消引用 float_node_t 指针以访问存储在其中的浮点数怎么办?我猜 2) 已经被禁止了。返回的指针指向 char 数组,它被转换为 float 指针。

C 标准规定指向不同结构的指针具有相同的表示和对齐要求,不会发生结构元素的重新排序,并且如果结构具有共同的初始序列,则这些结构的初始序列的布局将相等.

因此转换指针并取消引用以访问 prev/next 字段 似乎 没问题,但这是否已经违反了 C 的严格别名规则?

有很多关于在“兼容”结构之间转换指针的类似问题,但答案往往不一致甚至相互矛盾。有人说,通过任一指针访问公共初始序列的字段都很好,有人说你甚至不能取消引用已转换的指针。

So casting the pointers and dereferencing to access the prev/next fields seems fine, but isn't that already a violation of C's strict aliasing rules?

是的,即它确实违反了严格的别名规则;但问题是,它可能已经成为一种广泛使用的模式,因为它通常会编译成预期的形式。

不要在 sizeof 中仅使用 objects 中的类型,尤其是在这种代码中。使用类型容易出错。

在这种情况下,指针双关是 IMO 无效的(它违反了严格的别名规则)

我会这样做。

typedef struct node_s{
    struct node_s *next;
    struct node_s *prev;

    char data[];
}node_t;


typedef struct float_node_s{
    struct float_node_s *next;
    struct float_node_s *prev;
    
    float someFloat;
}float_node_t;

typedef union
{
    node_t node_c;
    float_node_t node_f;
}node_ut;

float getFloatDataFromNode(node_ut *node){
    return node -> node_f.someFloat;
}

void* newNode(size_t size){
    node_t *ret = malloc(sizeof(*ret) + size);
    return(ret);
}


int main(void){
    float f;
    node_ut *node;
    //1)
    node = newNode(sizeof(node -> node_f.someFloat));
    if(node == NULL){
        return(1);
    }
    //2)
    f = getFloatDataFromNode(node);
    return(0);
}

https://godbolt.org/z/c3csvoEeY

有特殊规定,见C17 6.5.2.3:

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. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.

因此,如果在同一个翻译单元中有一个 union 可见的两个结构,您应该能够检查每个结构的公共初始序列,无论其类型如何。然而,编译器对这个特定规则的支持不稳定,并且有关于它的 C 语言缺陷报告。

但值得注意的是,此特殊规则符合“严格别名规则”,该规则允许 struct/union 成员通过兼容类型进行左值访问,“包含上述之一的聚合或联合类型其成员之间的(兼容)类型。

但是,none 这允许在两个结构之间进行野生类型双关语,您可以在其中重新解释没有以不同方式共享的部分 - 这只是一个严格的别名违规和 UB。

尽管如此,我们不应该依赖于这些不稳定规则的语言律师来编写程序。这里的正确解法是:

typedef struct node
{
  struct node* next;
  struct node* prev;
} node_t;

typedef struct
{
  node_t parent;
  float  data;
} float_node_t;

typedef struct
{
  node_t parent;
  char   data[n];
} str_node_t;

这就是多态性在 C 中的工作方式 - 您现在可以使用 float_node_t* 强制转换为 node_t* 并将其传递给任何需要 node_t*.

的函数

如果您希望在 运行 时间内更改类型,您还可以选择在其中显示枚举以跟踪类型。你可以用函数指针做多态性。这是一个例子:

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

typedef struct node
{
  struct node* next;
  struct node* prev;
  void (*print)(struct node*);
} node_t;

typedef struct
{
  node_t parent;
  float  data;
} float_node_t;
    
typedef struct
{
  node_t parent;
  char   data[100];
} str_node_t;

node_t* float_node_create (float f);
node_t* str_node_create (const char* s);

void float_node_print (struct node* this);
void str_node_print (struct node* this);

#define node_create(data)                   \
  _Generic( (data),                         \
            float: float_node_create,       \
            char*: str_node_create )(data) \

int main (void)
{
  node_t* n1 = node_create(1.0f);
  node_t* n2 = node_create("hello world");
  n1->print(n1);
  n2->print(n2);
  
  free(n1);
  free(n2);
  return 0;   
}

void float_node_print (struct node* this)
{
  printf("%f\n", ((float_node_t*)this)->data );
}

void str_node_print (struct node* this)
{
  puts( ((str_node_t*)this)->data );
}

node_t* float_node_create (float f)
{
  float_node_t* obj = malloc(sizeof *obj);
  obj->data  = f;
  obj->parent.print = float_node_print;
  return (node_t*)obj;
}

node_t* str_node_create (const char* s)
{
  str_node_t* obj = malloc(sizeof *obj);
  strcpy(obj->data,s);
  obj->parent.print = str_node_print;
  return (node_t*)obj;
}

所有这些都是定义明确的行为和可移植的标准 C。这依赖于 6.7.2.1 中更为成熟和安全的语言规则:

Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.