Anonymous structs for aggregating allocations

November 8, 2020

Sometimes it's nice to be able to allocate a bunch of different linked structures in one go, instead of making separate allocations. An example of where this kind of thing can be useful is when implementing interface like structures to enable a degree of polymorphism:

typedef struct interface {
  void* ctx;
  void action(void* ctx);
} interface;

interface_act(interface* iface)

The idea here is that `interface` forms an opaque data type, which is only used with the interface function `interface_act`. A concrete `interface` might be typed a little differently - `ctx` might be some auxillary data structure.

struct ctx {
  FILE* out;
  char* prefix;

impl_action(interface *iface)
  const struct ctx* ctx = iface->ctx;
  fprintf(ctx->out, "%s: hello happy campers\n", ctx->prefix);

error_announcer_new(FILE* stream, char* prefix);

Ideally error_announcer_new allocates both an `interface` structure to return, and an auxillary struct ctx containing the required auxillary data, linking them together as needed.

One way to do this is with several allocations. But its also possible to allocate them in one go without doing pointer math by using anonymous structs:

error_announcer_new(FILE* stream, char* prefix)
  struct {
    interface impl;
    struct ctx ctx;
  } *aggregate = calloc(1, sizeof(*aggregate));

  if (!aggregate) {
    return NULL;

  aggregate->ctx.out = stream;
  aggreagte->ctx.prefix = prefix;

  interface *ret = (interface*)aggregate;
  ret->ctx = &aggregate->ctx;
  ret->action = impl_action;

  return ret;

One nice thing about this is it avoids the error prone alignment and size calculations of calculating the appopriate allocation size by hand, and it also survives updates a little better.

This also has the advantage (or disadvantage) that the lifetimes of the struct ctx and `interface` are coupled. In fact, the whole thing must be freed in one go 1

error_announcer_delete(interface* iface)


This is not the most type-safe way to do things. Each implementation of an interface can be given its own type e.g. struct error_announcer { interface impl; }; which is pointer-castable to an interface, or the appropriate destructor can be attached to the interface itself.

This kind of thing is particularly useful for one-off structs which use the global allocator (custom allocators are easier to make guarantees about), or when you want to guarantee contiguous allocation of data. Notice that it's also possible to fiddle with alignment in aggregate, or to add in scratch space, or padding.