Handmade Hero » Forums » Code » Field Array Implementation (Union of fields and array)
rationalcoder
Blake Martin
1 posts
#15509 Field Array Implementation (Union of fields and array)
3 months, 2 weeks ago Edited by Blake Martin on June 7, 2018, 3:21 p.m.

Recently, I ran into a common problem faced on HMH. Sometimes you want a struct with some fields that you want to access both individually and as an array, like so:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
union v3
{
    struct
    {
        f32 x;
        f32 y;
        f32 z;
    };
    f32 e[3];
};

This is fine for things like a `v3` that don't change. However, like something on HMH (I forgot what Casey was trying to do and when, so I can't link to it), sometimes you want a struct with fields that you know will be moved around, added to, etc. all the time. Often, the code that accesses the fields as an array can stay the same, so it's annoying to have to change it.

I thought the problem could be solved by using offsetof() and friends inside the struct as array bounds, but you can't use offsetof() on the same struct that you are defining the array in (at least in MSVC). I've come up with a simple macro that allows you to modify the fields all you want while still producing a correctly sized array:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#define CONCAT(a, b) a##b

#define FIELD_ARRAY_IMPL(type, name, structDefinition, counter)\
typedef struct structDefinition CONCAT(_anon_array, counter);\
union {\
    struct structDefinition;\
    type name[sizeof(CONCAT(_anon_array, counter))/sizeof(type)];\
};\
static_assert(sizeof(CONCAT(_anon_array, counter)) % sizeof(type) == 0,\
              "Field Array of type '" #type "' must be a multiple of sizeof(" #type ")")\

#define FIELD_ARRAY(type, structDefinition, name)\
FIELD_ARRAY_IMPL(type, name, structDefinition, __COUNTER__)

It works by just doing the dumb thing: it typedefs the same struct with a dummy type name and uses the size of it to determine the array bounds and generate a helpful static_assert().

Now, I can write code like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct Memory_Arena
{
    const char* tag;

    void* start;
    umm filled;
    umm size;
    umm max;
};

struct Game_Memory
{
    FIELD_ARRAY(Memory_Arena, {
        Memory_Arena perm;
        Memory_Arena temp;
        Memory_Arena file;
        // Later I might want add one for a network stack, break the file
        // arena into separate ones for things like textures and audio, etc.
    }, arenas);
};

// NOTE: the platform layer will fill out the actual memory fields 
// and pass it back to us in game_init().
inline Game_Memory
game_get_memory_requirements()
{
    Game_Memory request = {};

    Memory_Arena& perm = request.perm;
    perm.tag  = "Permanent Storage";
    perm.size = Kilobytes(16);
    perm.max  = Gigabytes(4);

    Memory_Arena& temp = request.temp;
    temp.tag  = "Temporary Storage";
    temp.size = Kilobytes(16);
    temp.max  = Megabytes(4);

    Memory_Arena& file = request.file;
    file.tag  = "File Storage";
    file.size = Megabytes(1);
    file.max  = Megabytes(8);

    return request;
}


Elsewhere, in the platform layer, I can write code like this and forget about it:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static inline void
win32_allocate_memory(Win32_State* state, Game_Memory* memory)
{
    // Add up the max sizes to determine the size of the contiguous region + guard pages.
    for (Memory_Arena& arena : memory->arenas) {
        // ...
    }

    // Reserve the full contiguous region.

    // Actually commit the memory for one chunk of each arena.
    for (Memory_Arena& arena : memory->arenas) {
        // ...
    }
}

The fields don't have to be of the same type. They just have to overlay exactly with an array of the correct type. For example:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// This works, and you might actually want to do something like this.
struct Game_Memory
{
    FIELD_ARRAY(Memory_Arena, {
        Memory_Arena perm;
        Memory_Arena temp;
        Memory_Arena file;
        char arenaBuffer[sizeof(Memory_Arena)];
    }, arenas);
};

// This _doesn't_ work, and is probably a mistake.
struct Game_Memory
{
    FIELD_ARRAY(Memory_Arena, {
        Memory_Arena perm;
        Memory_Arena temp;
        Memory_Arena file;
        int test;
    }, arenas);
};

If you mess it up, you will get something like:
error C2338: Field Array of type 'Memory_Arena' must be a multiple of sizeof(Memory_Arena)

As long as the field and array names are unique, you can have as many of these as you want and do all sorts of crazy stuff with other structs and unions.

You can also rearrange the macro parameters to get a different syntax if you prefer. For example:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    FIELD_ARRAY(Memory_Arena, arenas,
    {
        Memory_Arena perm;
        Memory_Arena temp;
        Memory_Arena file;
    });

    FIELD_ARRAY(Memory_Arena, arenas, {
        Memory_Arena perm;
        Memory_Arena temp;
        Memory_Arena file;
    });


Hopefully someone else will find this helpful!