C flexible array define with another type instead of malloc

148 Views Asked by At

the general usage of flexible array is to use malloc to define the flexible array. I'm trying to explore defining the flexible array with another struct. An example

typedef struct {
    uint64_t header;
    size_t data_size;
    float data[];
} test_base_t;

typedef struct {
    test_base_t base;
    float data[3];
} test_t;

As I understand, flexible array needs to be defined at the end of a struct. And clangd will give the following warning. -Wgnu-variable-sized-type-not-at-end

I just wanted to ask if anybody has done this before and is it safe? Or is there a better way to define flexible array size without alloc?

You can then wrap the usage of the object in a macro to static assert ext.base.data == ext.data before casting and passing to a general API consumes test_base_t. This way you can have the memory required in compile instead of allocing.

Edit

There seem to be a confusion on how I wanted to consume it, here is an example to demonstrate

#define SUM_BASE(test) \
    sum_base(&test->base); \
    _Static_assert(test->data == test->base.data);

float sum_base(test_base_t *base)
{
  float sum = 0;
  for (size_t i = 0; i < base->data_size; i++)
  {
    sum += base->data[i];
  }
  return sum;
}

test_t test = { .base = { .data_size = 3, }, .data = { 1, 2, 3, }, };
SUM_BASE((&test));
2

There are 2 best solutions below

2
On BEST ANSWER

You cannot create actual instances of test_base_t with an initialized array, but you can create compound literals with an initialized array of a specified length and cast their address as test_base_t pointers. The layout and alignment of both structures should be compatible, given that they have exactly the same types, save for the flexible array length.

Here is an example:

#include <inttypes.h>
#include <stdio.h>
#include <stdint.h>

typedef struct {
    uint64_t header;
    size_t data_size;
    float data[];
} test_base_t;

#define TEST_ARRAY(n) (test_base_t*)&(struct { uint64_t header;  \
                                               size_t data_size; \
                                               float data[n]; })

float sum_base(const test_base_t *p) {
    float sum = 0.F;
    for (size_t i = 0; i < p->data_size; i++) {
        sum += p->data[i];
    }
    return sum;
}

void print_test(const test_base_t *p) {
    printf("%"PRIu64" %zu { ", p->header, p->data_size);
    if (p->data_size) {
        printf("%g", p->data[0]);
        for (size_t i = 1; i < p->data_size; i++) {
            printf(" %g", p->data[i]);
        }
    }
    printf(" } sum=%g\n", sum_base(p));
}

int main() {
    test_base_t *p1 = TEST_ARRAY(1){.data_size = 1, .data = {1}};
    test_base_t *p2 = TEST_ARRAY(2){.data_size = 2, .data = {1, 2}};
    print_test(p1);
    print_test(p2);
    print_test(TEST_ARRAY(3){.data_size = 3, .data = {1, 2, 3}});
    print_test(TEST_ARRAY(4){.data_size = 4, .data = {1, 3, 5, 7}});
    return 0;
}

Here is another approach, perhaps closer to your expectations, using a union with a base member with the flexible type and a parametric instance type with the appropriate array size:

#include <inttypes.h>
#include <stdio.h>
#include <stdint.h>

typedef struct {
    uint64_t header;
    size_t data_size;
    float data[];
} test_base_t;

/* parametric type template using C macros */
/* structures with a flexible array can be members of union types */
#define test_base_t(...) \
    union { \
        test_base_t base; \
        struct { \
            uint64_t header; \
            size_t data_size; \
            float data[__VA_ARGS__]; \
        }; \
    }

float sum_base(const test_base_t *p) {
    float sum = 0.F;
    for (size_t i = 0; i < p->data_size; i++) {
        sum += p->data[i];
    }
    return sum;
}

void print_test(const test_base_t *p) {
    printf("%"PRIu64" %zu { ", p->header, p->data_size);
    if (p->data_size) {
        printf("%g", p->data[0]);
        for (size_t i = 1; i < p->data_size; i++) {
            printf(" %g", p->data[i]);
        }
    }
    printf(" } sum=%g\n", sum_base(p));
}

int main() {
    test_base_t(1) t1 = { .data_size = 1, .data = {1} };
    test_base_t(2) t2 = { .data_size = 2, .data = {1, 2} };
    /* the print_test function can be called without casts */
    print_test(&t1.base);
    print_test(&t2.base);
    print_test(&((test_base_t(3)){.data_size = 3, .data = {1, 2, 3}}).base);
    print_test(&((test_base_t(4)){.data_size = 4, .data = {1, 3, 5, 7}}).base);
    return 0;
}
0
On

C 2018 6.7.2.1 3 says of a structure containing a flexible array member:

… such a structure (and any union containing, possibly recursively, a member that is such a structure) shall not be a member of a structure or an element of an array.

Thus, the test_t type in the question violates this “shall” requirement, and C 2018 4 2 says that makes the behavior not defined by the C standard. A compiler could reject this code. If the compiler accepts it, the behavior of the program is not defined by the C standard.

As an example of what could go wrong (in that the C standard allows it), consider this code:

test_t test = { .base = { .data_size = 3, }, .data = { 1, 2, 3, }, };
printf("%g\n", test.base.data[0]);

Since test.base.data[0] was never assigned a value through that expression, and the standard does not define test.data to alias test.base.data, the compiler may assume the value of test.base.data[0] is uninitialized and hence unspecified, and this printf may use any value of the float type, even if test.base.data[0] and test.data[0] nominally refer to the same memory.

And in this code:

test_t test = { .base = { .data_size = 3, } };
for (int i = 0; i < 4; ++i)
    test.base.data[i] = i+1;
test_t copy = test;

The compiler may assume that, since test.data was never initialized, it does not need to be copied to copy when initializing it from test.