1

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));
Tony Yuen
  • 13
  • 4
  • In my experience, all compilers are happy with `float data[0]` in the base struct. – o11c Apr 28 '22 at 21:01
  • 1
    @o11c Fun, but that isn't where the OP is going. The OP wants to know if he can pass a `test_ext_t` instance by-address to something that expects a `test_base_t` instance by-address and the flexible member in the latter will magically align with the inflexible member of the former. – WhozCraig Apr 28 '22 at 21:03
  • 1
    Tony Yue, If _alignment_ requirements of `test_base_t` differ from `float`, then `float data[3];`, even though it provides the memory for at least 2 `float` in `float data[];`, they may not overlap well such that access one will not be the same as accessing the other. It that case `ext.base.data != ext.data`. Now maybe the alignment requirements cannot differ here - hmmm. – chux - Reinstate Monica Apr 28 '22 at 21:56
  • 1
    Note: `ext.base.data == ext.data` --> that may fail if padding existed between `.base` and `.data`. – chux - Reinstate Monica Apr 28 '22 at 22:43
  • @WhozCraig Theoretical oddities allowed by the standard don't matter. Only real and potential implementations do, and no sane implementation would break in either case (the only difference between a flexible array member and a zero-sized trailing array member is warnings, and the only sane alignment algorithm forces the addresses to be identical) – o11c Apr 29 '22 at 02:08
  • 2
    @Eric: 6.7.2.1p3: "A structure or union shall not contain a member with incomplete or function type (hence, a structure shall not contain an instance of itself, but may contain a pointer to an instance of itself), except that the last member of a structure with more than one named member may have incomplete array type; **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**." – rici Apr 29 '22 at 04:36
  • [N2083](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2083.htm) proposes allowing the last member of a struct (recursively) to be a struct containing a FAM, but I don't believe there is yet a resolution to that change request. Anyway, it wouldn't apply in this case. – rici Apr 29 '22 at 04:40
  • 1
    Does https://stackoverflow.com/questions/68175878/how-to-allocate-a-struct-with-a-flexible-array-member-on-the-stack answer your question? – KamilCuk Apr 29 '22 at 06:43
  • @rici: Thanks, I missed that because I searched for “flexible,” and that paragraph does not use the word even though it is discussing flexible array members. I will write up an answer. – Eric Postpischil Apr 29 '22 at 10:59

2 Answers2

1

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.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
1

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;
}
chqrlie
  • 131,814
  • 10
  • 121
  • 189
  • Agreed since the standard doesn't allow embedding structure with flexible array inside another structure, but GCC does have an extension to allow you to statically define flexible array, which i think is really useful! https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html, but the downside is now your code is GCC C extension dependent – Tony Yuen Apr 29 '22 at 20:34
  • @TonyYuen: Using extensions is problematic: they are often poorly specified and are of course not portable. The gcc documentation shows an example of an initialized flexible array `struct f1 { int x; int y[]; } f1 = { 1, { 2, 3, 4 } };` which seems interesting, but beware that `sizeof(f1)` is `4`, not `16` and this syntax is only supported for static objects. Automatic objects and compound literals such as `(struct f1){ 1, { 2, 3, 4 } }` produce an error. – chqrlie Apr 29 '22 at 21:21