如何使用同一个函数初始化相似的结构体?

4

我有一些以 void *buffer 开头的结构体,接下来的成员可以是不同的类型:

struct A {
    void *buffer;
    type_x *x;
    type_y *y;
};

struct B {
    void *buffer;
    type_w *w;
    type_y *y;
    type_z *z;
};

struct A的缓冲区将存储n个type_x类型的元素,后跟n个type_y类型的元素。另外两个成员type_x *xtype_y *y将分别指向这些数组。struct B同理。

我目前正在做的事情类似于:

void allocate(struct B **b, unsigned int n) {
    (*b)->buffer = calloc(n * (sizeof(type_w) + sizeof(type_y) + sizeof(type_z));

    (*b)->w = (type_w *) (*b)->buffer;
    (*b)->y = (type_y *) ((*b)->w + n);
    (*b)->z = (type_z *) ((*b)->y + n);
}

有没有办法创建一个函数来实现这个功能?该函数应接收指向这些结构体之一的指针(void *s)和一个整数作为参数,如下所示:

void allocate(void *s, unsigned int n) {
    // MAGIC
}

我考虑过的其他选项:

  1. 创建void allocate(void *buffer, int n, ...),并将其指针给予结构体的指针。但问题在于,我必须给它void *指针,因此我还必须给出每种类型的大小。

  2. 创建void *create_struct(StructType type)(其中StructType是一个枚举),但我仍然需要编写每个结构体的情况,并且我希望能够定义新的结构体而不必编写额外的代码。

我试图这样做是因为我将有许多结构体,因为allocate函数对于每个结构体基本上都执行相同的操作,所以我认为可能有一种“更清洁”的方法来完成它。

另外,我知道我可以直接删除缓冲区并直接为所有成员分配内存,但我想以这种方式进行,以便数据存储连续。


这通常是一个不好的想法...但是如何使用宏来传递大小呢?例如:#define alloc(ptr) __alloc((ptr), sizeof((ptr))) - user1551592
2
你的原始代码存在对齐问题。(n = 3type_w = chartype_y = double - Bryan Chen
@user9000:这个宏只会分配指针的大小——不太可能是 OP 想要的。而且以双下划线开头的标识符是保留给实现的,用户代码不应使用它们。 - too honest for this site
你正在寻找的魔法必须由你自己编写。你已经在做的可能是最干净的代码,而不需要诸如typeof GCC扩展这样的编译器扩展,它可以实现一些神奇的事情,比如Linux内核的container_of。否则,你需要处理单独的初始化函数,并且仍然需要将大小传递给函数以及n和初始化函数本身。 - user539810
@ChronoKitsune 我也考虑使用类似的东西(例如 offsetof,如果我没记错的话,它是用于 container_of 的),但我还没有找到可以用来解决这个特定问题的东西。 - vdrg
显示剩余5条评论
3个回答

1

没有通用的方法可以做到这一点,而不会对类型安全进行快速和宽松的处理。这意味着,从技术上讲,一个真正通用的解决方案将导致未定义的行为。如果我被迫实现这样的东西,我将不得不假设我可以将传入的结构指针视为指针数组。并且每种类型的大小都需要传递进来。忽略对齐问题,以下是一些未经测试的代码:

void allocate(void *sp, size_t n, ... /* terminate with 0 */) {
    void **sv = sp;
    size_t arg, total = 0;
    size_t args = 0;
    va_list ap;
    va_start(ap, n);
    while ((arg = va_arg(ap, size_t)) != 0) {
        total += arg;
        ++args;
    }
    va_end(ap);
    *sv = calloc(...);
    sv[1] = sv[0];
    va_start(ap, n);
    while (--args > 0) {
        ++sv;
        sv[1] = (char *)sv[0] + va_arg(ap, size_t);
    }
    va_end(ap);
}

allocate(a, n, sizeof(type_x), sizeof(type_y), (size_t)0);
allocate(b, n, sizeof(type_w), sizeof(type_y), sizeof(type_z), (size_t)0);

显然是一个不太优雅的hack。

更好的解决方案应该是为每种类型创建单独的分配器函数。但是,您可以创建一个宏来帮助自动生成分配器。以下是更多未经测试的代码:

#define CREATE_ALLOCATOR(Type, X_Fields) \
void allocate_##Type (struct Type *sp, size_t n) { \
    _Pragma("pop_macro(\"X\")") \
    size_t total = 0 \
        X_Fields \
        ; \
    void *p; \
    sp->buffer = calloc(sizeof(*sp) + total); \
    p = sp->buffer; \
    _Pragma("pop_macro(\"X\")") \
    X_Fields \
    ; \
}

#include "create_allocator_helper.h"
CREATE_ALLOCATOR(A, X(x) X(y))
#include "create_allocator_helper.h"
CREATE_ALLOCATOR(B, X(w) X(y) X(z))

助手头文件定义并推送一些由CREATE_ALLOCATOR宏使用的X宏定义:

#ifdef X
#undef X
#endif
#define X(A) ; sp->A = p; p = sp->A + n
#pragma push_macro("X")
#undef X
#define X(A) + sizeof(sp->A)
#pragma push_macro("X")
#undef X

在这个例子中,内联的 _Pragma 语法是特定于GCC的,但是MSVC有一种不完全相似的语法,拼写为 __pragma - jxh
如果您拥有完整的C.11支持,您可以使用新的_Generic功能来创建一个单一的allocator()宏,以分派到适当的类型特定分配器。 - jxh

1
如果您在编译时知道可能的n集合,您可以让每个(成员集合)/(数组大小)组合成为自己的类型,并使用方便的宏引用正确的类型。
#include <stddef.h>

/*  
We put a type marker at the beginning of each struct.  There won't be padding before the first member, and all the types
start with a struct Type, so we can do `(struct Type*)&unknown_structure` and be guaranteed to have a valid object that
tells us what type the rest of it is.

In practice, I'd probably use X-Macros to generate an enum containing all the types instead of using strings, to
make comparison faster 
*/

struct Type { char *type; size_t n; };

/* We define what types of arrays each structure contains. Since the struct contains the arrays themselves
instead of pointers to them, the memory will be contiguous, +/- a bit of padding. */

#define DECL_A(N) struct A_##N { struct Type type; char x[N]; double y[N]; }
#define DECL_B(N) struct B_##N { struct Type type; size_t n; int x[N]; float y[N]; char z[N]; }

/* 
Declare a struct and initialize the type and n members.  This one just
declares a local variable, but we could make a malloc version easily enough. 
*/
#define CREATE_STRUCT(NAME, TYPE, N) struct TYPE##_##N NAME = { .type = { #TYPE, N} }

/* We declare all struct type/size combinations we'll use */
DECL_A(42);
DECL_A(100);
DECL_B(30);

int main(void) {
    int i;

    CREATE_STRUCT(foo, A, 42);
    CREATE_STRUCT(bar, A, 100);
    CREATE_STRUCT(baz, B, 30);

    return 0;
}   

0

这里有一个替代方案,不需要在编译时知道 n

请注意,我并不完全确定这个方法的合法性,但我相信它是有效的。关键思想是,如果 p 指向类型为 T 的正确对齐内存块,则只要它位于已分配的块内,((char*)p) + sizeof(T) * N 也必须指向正确对齐的内存。 只要这是真的,我相当确定这必须是合法的,因为 bufferAlignment_Hack 的联合保证了所有类型的 buffer[0] 都被正确对齐。

即使它是合法的,它仍然有点像 hack,所以我不完全确定我会推荐它,但它是一个潜在的选择。

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

/* This MUST contain all types that might be stored in the arrays */
union Alignment_Hack {
    short hd;
    int d;
    char c;
    float hf;
    double f;
};

/* This struct is used for *all* structures.  The structure-specific details get specified later */
struct Variable_Structure {
    size_t num_arrays;
    void **arrays;
    union {
        union Alignment_Hack *hack;
        char *buffer;
    } u; //u.buffer[0] is guaranteed to be properly aligned for anything in the union
};

//Here's where the details for a specific struct (struct A {short x; double y; int z; }) are specified.
size_t sizes_A[] = { sizeof(short), sizeof(double), sizeof(int) };


void create_structure(struct Variable_Structure *p, const size_t array_count, size_t *sizes, unsigned nvars) {
    size_t offsets[nvars];//in bytes (NOTE: requires C99)
    unsigned i;

    offsets[0] = 0;
    for (i = 1; i < nvars; i++) {
        //offsets[i] must be an even multiple of sizes[i] and must also be past the end of the last array
        size_t min = offsets[i - 1] + sizes[i - 1] * array_count;
        size_t mod = min % sizes[i];

        //offsets[i] = min_p such that p >= min and p % sizes[i] == 0
        offsets[i] = (min - mod) + (mod ? sizes[i] : 0);// (min - mod) and (min - mod + sizes[i]) are the two possible starting points

        /* Visualization of the transition from TYPE_A[] to TYPE_B[], showing where everything's pointing:

                                      min (end of TYPE_A array)
                                       V
        ... | TYPE_A | TYPE_A | TYPE_A |
        ...   |  TYPE_B  |  TYPE_B  |  TYPE_B  |  TYPE_B  |
                                    ^          ^
                             min - mod       (min - mod) + sizes[i] */

        assert(offsets[i] >= min);//doesn't overlap previous array
        assert(offsets[i] <= min + sizes[i]);//doesn't include more padding than necessary
        assert(0 == offsets[i] % sizes[i]);//alignment correct
    }
    size_t bufsiz = offsets[nvars - 1] + sizes[nvars - 1] * array_count;

    //Skipping error checking for brevity
    p->num_arrays = nvars;
    p->u.buffer = malloc(bufsiz);
    p->arrays = malloc(sizeof(void*) * nvars);
    for (i = 0; i < nvars; i++) {
        p->arrays[i] = p->u.buffer + offsets[i];
    }
}

void free_structure(struct Variable_Structure *p) {
    free(p->arrays);
    free(p->u.buffer);
}

int main(void) {
    struct Variable_Structure a;

    size_t n = 42;
    create_structure(&a, n, sizes_A, sizeof(sizes_A)/sizeof(*sizes_A));

    unsigned i;
    for (i = 0; i < n; i++) {
        //We could always set up some macros or something so we could say, e.g., A(x, i) instead of ((short*)a.arrays[0])[i]
        ((short*)a.arrays[0])[i] = i;
        ((double*)a.arrays[1])[i] = i;
        ((int*)a.arrays[2])[i] = i;

        printf("%hd %f %d\n",
            ((short*)a.arrays[0])[i],
            ((double*)a.arrays[1])[i],
            ((int*)a.arrays[2])[i]);
    }

    printf("SIZES: %zu %zu %zu\n", sizeof(short), sizeof(double), sizeof(int));
    printf("OFFSETS: %p %p %p\n", a.arrays[0], a.arrays[1], a.arrays[2]);

    free_structure(&a);
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接