【问题标题】:Using union / structs to create generalized list objects in pure C使用 union / structs 在纯 C 中创建广义列表对象
【发布时间】:2017-04-04 18:24:27
【问题描述】:

链表是解决许多编程问题的根本有用的构建块。以下内容应该看起来很熟悉:

typedef struct _list {
  struct _list *_next;
} list_t;

#define NULL_LIST ((list_t *)NULL)

void list_push(list_t *element, list_t **list_head);
list_t *list_pop(list_t **list_head);
list_t *list_next(list *list);
int list_length(list_t *list);

我想使用上述例程来操作其他类型的对象,假设这些对象的第一个槽是list_t * 链接。也就是说,我想在定义的其他事物上使用list_push()list_pop()list_length() 函数,例如:

typedef struct _linked_buffer {
  list_t *_next;
  uint8_t _bufffer[80];
} linked_buffer_t;

typedef struct _pair {
  list_t *_next;
  void *_head;
} pair_t;

...等等。我保证所有这些其他“类”在第一个位置共享一个 _link 插槽。

所以我的问题是:有没有办法确保 C 编译器 linked_buffer_tpair_t 是“一种”list_t,这样我就不必每次想使用时都使用强制转换列出对它们的操作?

我可以创建隐藏投射的宏:

#define LIST_PUSH(el, head) list_push((list_t *)el, (list_t **)head)

但我想知道是否有更好的方法巧妙地使用 unionstruct 构造,从而消除对所有内容进行强制转换的需要。

【问题讨论】:

  • 在文件范围内以下划线开头的名称始终保留用于所有名称空间中的实现。在用户代码中使用它们会调用未定义的行为。
  • 如果您对扩展持开放态度,请查看 gcc -fplan9-extensions。宏不是一个好主意,它们会使调试复杂化。
  • #define LIST_PUSH(el, head) list_push((list_t *)el, (list_t **)head) 与使用 void list_push(void *element, void **list_head); 并在内部进行转换没有什么不同。
  • 一种方法是定义 push pop 等的标题以使用 void* 而不是 list_t* 和 list_t**。由于指向任何类型的指针都被隐式转换为 void*,因此您可以消除转换的需要。类似的回报。
  • “纯 C”是一个非术语。

标签: c gcc struct casting unions


【解决方案1】:

只要其中包含的所有结构都包含相同的初始字段集,union 将允许一定程度的类型双关。

例如:

typedef struct _generic_list {
  struct _generic_list *_next;
  char type;
} generic_list_t;

typedef struct _linked_buffer {
  generic_list_t *_next;
  char type;
  uint8_t _bufffer[80];
} linked_buffer_t;

typedef struct _pair {
  generic_list_t *_next;
  char type;
  void *_head;
} pair_t;

typedef union _list {
    generic_list_t generic_list;
    linked_buffer_t buffer;
    pair_t pair;
}

因为联合体的所有成员都将generic_list_t * 作为第一个元素,将char 作为第二个元素,所以您可以在联合体的任何成员中安全地访问这些元素。

type 字段是一个标志,可让您知道哪些联合成员包含对其其他字段有意义的数据。您需要按照惯例设置该字段。

【讨论】:

  • 还有其他方法可以在不添加类型字段的情况下实现运行时类型,例如所有pair_t 都可以从一个已知的内存块分配,而所有linked_buffer_t 可以从另一个内存块分配。 (我有没有提到这是针对没有 malloc() 的嵌入式系统?我的错...)除了那个 nit,很好的答案!
【解决方案2】:

假设这种通用链表的每个实例将只存储一种“类”,您可以使用指向 void void* 的指针作为列表结构的第二个成员。

typedef struct _list  
{
    struct _list *_next;
    void * pData; 
} list_t;

链表上的典型操作不需要了解存储类型。任何引用链表中对象的操作都返回指向 list_t 的指针或指向 void 的指针。
任何客户端都知道它希望存储在链表中的类型,并且可以在读取时转换为正确的类型,或者在写入时转换为 void 指针。

【讨论】:

  • 或者,您可以使用灵活的数组成员而不是 void *,以更紧密地将数据负载与主机结构相关联。
  • @John Bollinger 我看到 OP 希望使用纯 C(我读为“无 C++”)和“灵活数组”之间存在冲突。那是因为我模糊地将 STL 和其他 C++ 概念与它联系起来。您能否详细说明纯 C 中的灵活数组的概念?
  • 灵活的数组成员只是一个 C 特性(在 C99 中引入)。 C++ 没有它们。它们提供了一种标准替代方案,以替代先前广泛但非标准的做法,即将结构的最后一个成员声明为单字符数组,但分配更大的实例并有意超出数组边界。您可以阅读灵活数组成员的规范草案here
  • @John Bollinger 谢谢。我似乎落后于新的 C 标准。在我的工作中,我受限于旧标准。
【解决方案3】:

您可以将函数的原型更改为 -

void list_push(void *element, void *list_head);
void* list_pop(void *list_head);
void* list_next(void *list);
int list_length(void *list);

现在在函数内部,您可以将它们转换为 list_t* 和 list_t **

在调用它们时,您可以直接调用它们而无需任何转换。

唯一的问题是它会接受任何指针,你会失去错误检测。

【讨论】:

  • 你的意思是void **list_head,是吗?
  • 不,您可以使用 void*,因为指向指针的指针也可以在没有显式转换的情况下转换为 void*。
  • 我不确定 list_t** 是否可以隐式转换为 void**。我的猜测是不可能的。
【解决方案4】:

所以我的问题是:有没有办法确保 C 编译器 linked_buffer_tpair_t 是“一种”list_t,这样我就不必每次想使用时都使用强制转换列出对它们的操作?

您的linked_buffer_tpair_t 在任何意义上都不是C 语义所识别的list_t,所以不,C 没有提供任何方式来说明它们是。此外,您建议进行的那种指针转换将导致您的程序违反严格的别名规则,因此具有未定义的行为。但在实践中,铸造将在许多地方可靠地工作。例如,POSIX 在其套接字 API 中依赖于类似的指针转换,因此您可以相当确信该方法适用于此类系统。

尽管如此,还是有一些方法可以在 C 中提供通用列表。您可以使用基于联合的方法(例如建议的 @dbush)来衡量通用性。您可能还想检查基于宏的方法,例如 Troy Hanson 的 UTHash 包中的方法(尽管它仍在分发,但已不再维护)。

【讨论】:

  • @AjayBrahmakshatriya,是的,通过在不兼容的指针类型之间进行类型转换(在标准的“不兼容”意义上)违反了标准的要求,因此产生了 UB。这在标准的paragraph 6.5/7 中有明确描述,俗称“严格别名规则”。
  • 这似乎是正确的。题外话,但它是一种特殊情况,允许通过将变量的指针转换为 char* 来将变量读取为 char 吗?
  • 我问这个是因为这是所有字节复制的基础。
  • 是的,@AjayBrahmakshatriya,如果您阅读我为您链接的规定,您会发现允许将任何有效的对象指针转换为指向字符类型的指针,并读取字节对象通过该指针的表示。
  • 谢谢,更有意义!
【解决方案5】:

我们可以利用 C11 6.7.2.1p15:

一个指向结构对象的指针,经过适当的转换,指向它的初始成员(或者如果该成员是位域,则指向它所在的单元),反之亦然。结构对象中可能有未命名的填充,但不是在其开头。

所以,假设你有

typedef  struct linked_list  linked_list;
struct linked_list {
    struct linked_list *next;
};

所有链表类型都以linked_list common; 成员开头:

struct list1 {
    linked_list  common;
    double       value;
};

struct list2 {
    linked_list  common;
    size_t       size;
    char         data[];
};

struct list3 {
    linked_list  common;
    /* ... other members ... */
};

对于 GCC,我们可以额外使用 __typeof__ 运算符将返回值设置为正确的类型;对于其他编译器,通用列表函数将返回 void *:

#define  LIST_NONE              ((void *)0)

#if defined(__gnuc__)
#define  LIST_NEXT(list)        ((__typeof__(list))(list_next(&((list)->common), (void *)(list))))
#define  LIST_SIZE(list)        (list_size(&((list)->common), (void *)(list)))
#define  LIST_PUSH(list, item)  (list_push((linked_list **)(list), &(*(list))->common, &((item)->common)))
#define  LIST_POP(list)         ((__typeof__(*(list)))list_pop((linked_list **)(list), &(*(list))->common))
#else
#define  LIST_NEXT(list)        ((void *)(list_next(&((list)->common), (void *)(list))))
#define  LIST_SIZE(list)        (list_size(&((list)->common), (void *)(list)))
#define  LIST_PUSH(list, item)  (list_push((linked_list **)(list), &(*(list))->common, &((item)->common)))
#define  LIST_POP(list)         ((void *)list_pop((linked_list **)(list), &(*(list))->common))
#endif

LIST_NONE 是一个 POSIX NULL 宏。 (也就是说,它在 POSIXy 机器上等价于NULL。)

LIST_SIZE()LIST_NEXT() 宏将单个指针作为参数。该宏假定指针指向以linked_list common; 作为初始成员的结构类型,并将该成员的地址以及转换为void 指针的原始指针提供给相应的函数。基于 C11 6.7.2.1p15,两者应该相等。

如果指针指向的类型不是结构类型,或者结构类型缺少common 初始成员,或者该成员不是指向linked_list 的指针,编译器将发出不兼容的指针类型警告(如果启用)。

如果linked_list common; 不是结构中的初始成员,则提供给list_size()list_next() 函数的两个指针将不匹配。你可以使用例如assert() 来自 <assert.h> 检查:

linked_list *list_next(linked_list *list, void *copy)
{
    assert((void *)list == copy);
    return list->next;
}

size_t list_size(linked_list *list, void *copy)
{
    size_t result = 0;
    assert((void *)list == copy);
    while (list) {
        list = list->next;
        result++;
    }
    return result;
}

如果你觉得不需要检查common成员是否在结构体的开头,可以省略assert()检查,函数调用中的第二个参数(包括宏中)完全一致。

LIST_PUSH()LIST_POP() 宏是相似的,但在这种情况下,我们需要一个指向链表成员指针的指针。我们没有尝试拼凑某种地址的地址运算符,而是将指针指针转换为指针指针到linked_list,但也提供common成员的地址用于类型检查:

void list_push(linked_list **listptr, linked_list *list, linked_list *item)
{
    assert(list == *listptr);
    item->next = *listptr;
    *listptr = item;
}

linked_list *list_pop(linked_list **listptr, linked_list *list)
{
    assert(list == *listptr);
    if (*listptr) {
        linked_list *result = *listptr;
        *listptr = result->next;
        result->next = (linked_list *)0;
        return result;
    } else
        return (linked_list *)0;
}

在这种情况下,如果你觉得不需要验证common成员是否在链表结构类型的开头,可以省略assert()检查;但是,无论如何都不能省略list 参数,因为即使不使用它的值,编译器仍然会检查它的类型。

这是我用来验证上述内容的完整快速拼凑的测试程序:

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

typedef  struct linked_list  linked_list;
struct linked_list {
    struct linked_list *next;
};

struct list1 {
    linked_list  common;
    double       value;
};

struct list2 {
    linked_list  common;
    size_t       size;
    char         data[];
};

struct list3 {
    linked_list  common;
    /* ... other members ... */
};

static inline void list_push(linked_list **listptr, linked_list *list, linked_list *item)
{
    assert(list == *listptr);
    item->next = *listptr;
    *listptr = item;
}

static inline linked_list *list_pop(linked_list **listptr, linked_list *list)
{
    assert(list == *listptr);
    if (*listptr) {
        linked_list *result = *listptr;
        *listptr = result->next;
        result->next = (linked_list *)0;
        return result;
    } else
        return (linked_list *)0;
}

static inline linked_list *list_next(linked_list *list, void *copy)
{
    assert((void *)list == copy);
    return list->next;
}

static inline size_t list_size(linked_list *list, void *copy)
{
    size_t result = 0;
    assert((void *)list == copy);
    while (list) {
        list = list->next;
        result++;
    }
    return result;
}

#if defined(__gnuc__)
#define  LIST_NEXT(list)        ((__typeof__(list))(list_next(&((list)->common), (void *)(list))))
#define  LIST_SIZE(list)        (list_size(&((list)->common), (void *)(list)))
#define  LIST_PUSH(list, item)  (list_push((linked_list **)(list), &(*(list))->common, &((item)->common)))
#define  LIST_POP(list)         ((__typeof__(*(list)))list_pop((linked_list **)(list), &(*(list))->common))
#else
#define  LIST_NEXT(list)        ((void *)(list_next(&((list)->common), (void *)(list))))
#define  LIST_SIZE(list)        (list_size(&((list)->common), (void *)(list)))
#define  LIST_PUSH(list, item)  (list_push((linked_list **)(list), &(*(list))->common, &((item)->common)))
#define  LIST_POP(list)         ((void *)list_pop((linked_list **)(list), &(*(list))->common))
#endif

void test_list1(void)
{
    struct list1  a, b, c, *root = NULL;
    size_t n;

    a.value = 1.0; LIST_PUSH(&root, &a);
    b.value = 2.0; LIST_PUSH(&root, &b);
    c.value = 3.0; LIST_PUSH(&root, &c);

    n = LIST_SIZE(root);
    if (n == 3)
        printf("test_list1(): list_size() OK\n");
    else
        printf("test_list1(): list_size() == %zu, ERROR\n", n);

    if (LIST_POP(&root) != &c)
        printf("test_list1(): list_pop() ERROR");
    else
    if (LIST_POP(&root) != &b)
        printf("test_list1(): list_pop() ERROR");
    else
    if (LIST_POP(&root) != &a)
        printf("test_list1(): list_pop() ERROR");
    else
    if (root)
        printf("test_list1(): list_pop() ERROR");
    else
        printf("test_list1(): list_pop() OK\n");
}

void test_list2(void)
{
    struct list2  a, b, c, *root = NULL;
    size_t n;

    a.size = 1; LIST_PUSH(&root, &a);
    b.size = 2; LIST_PUSH(&root, &b);
    c.size = 3; LIST_PUSH(&root, &c);

    n = LIST_SIZE(root);
    if (n == 3)
        printf("test_list2(): list_size() OK\n");
    else
        printf("test_list2(): list_size() == %zu, ERROR\n", n);

    if (LIST_POP(&root) != &c)
        printf("test_list2(): list_pop() ERROR");
    else
    if (LIST_POP(&root) != &b)
        printf("test_list2(): list_pop() ERROR");
    else
    if (LIST_POP(&root) != &a)
        printf("test_list2(): list_pop() ERROR");
    else
    if (root)
        printf("test_list2(): list_pop() ERROR");
    else
        printf("test_list2(): list_pop() OK\n");
}

void test_list3(void)
{
    struct list3 a, b, c, *root = NULL;
    size_t n;

    LIST_PUSH(&root, &a);
    LIST_PUSH(&root, &b);
    LIST_PUSH(&root, &c);

    n = LIST_SIZE(root);
    if (n == 3)
        printf("test_list3(): list_size() OK\n");
    else
        printf("test_list3(): list_size() == %zu, ERROR\n", n);

    if (LIST_POP(&root) != &c)
        printf("test_list3(): list_pop() ERROR");
    else
    if (LIST_POP(&root) != &b)
        printf("test_list3(): list_pop() ERROR");
    else
    if (LIST_POP(&root) != &a)
        printf("test_list3(): list_pop() ERROR");
    else
    if (root)
        printf("test_list3(): list_pop() ERROR");
    else
        printf("test_list3(): list_pop() OK\n");
}

int main(void)
{
    test_list1();
    test_list2();
    test_list3();
    return EXIT_SUCCESS;
}

请注意,由于我只使用这个单一编译单元中的函数,所以我将列表访问器函数标记为static inline。这应该让编译器优化掉额外的函数参数,并整体生成更好的代码。如果您在多个编译单元中使用这些函数,我可能只保留 static inline 并让编译器生成重复项,除非代码大小是一个问题。

由于我使用 GCC,我编译并运行了上面的 list.c 使用

gcc -Wall -std=c99 -pedantic list.c -o test && ./test

如果您发现上述错误,或者您认为我误解了 C11 标准的原因,请在 cmets 中告诉我,以便我进行验证。

【讨论】:

    猜你喜欢
    • 2019-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-19
    • 2013-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多