> This pattern was officially standardized in C99,

No it wasn't; the C99 flexible array uses [] not [1] or [0].

When using the [1] hack, you cannot use the sizeof the structure to get the offset, because it includes the [1] array.

When using C99, you also cannot use sizeof to get the offset of the [0] element of the flexible array; sizeof is allowed to pretend that there is padding as if the flexible member were not there.

>

  // The (N - 1) adjusts for the 1-element array in Payload struct
  Payload *item = malloc(sizeof(Payload) + (N - 1) * sizeof(char))
>

If you are in C++ you need a cast; the void * return value of malloc cannot implicitly convert to Payload *.

  Payload *item = static_cast<Payload *>(malloc(...));
Or of course a C cast if the code has to compile as C or C++:

  Payload *item = (Payload *) malloc(...);
Setting aside that issue for brevity, pretending we are in C, I would make the malloc expression:

  Payload *item = malloc(offsetof(Payload, elements) + N);
sizeof(char) is by definition 1, so we do not need to multiply N by it.

By taking the offset of the elements array, we don't need to subtract 1 from N to account for the [1] element being skipped by sizeof.

These kinds of little things take away complexity for something that must be carefully coded to avoid a memory safety issue. You really want the calculations around the memory to use the simplest possible formulas that are as easy as possible to reason about to convince yourself they are correct.

Also, when you do use sizeof in a malloc expression, the following pattern avoids repeating the type name for the size, and also lets a pair of parentheses be dropped since sizeof only requires parentheses when the operand is a type:

  Payload *item = malloc(sizeof *item);

Strictly speaking, in the C++ object model, malloc allocates storage but doesn't create objects. Accessing that memory as if it contains an object (even a trivial one like int) without properly giving it object lifetime is technically UB. For trivial types, this is rarely enforced in practice, but the standard says to use placement new or std::start_lifetime_as (C++23) to properly begin object lifetime.

> Strictly speaking, in the C++ object model, malloc allocates storage but doesn't create objects.

No - strictly speaking, it does create objects. https://en.cppreference.com/w/cpp/memory/c/malloc.html#:~:te...

It gets confusing (to say the least) if you start questioning the details, but the spec does formally intend the objects to be implicitly created.

I’m not a C++ dev … Does that mean calling constructors? So a default, parameter-less constructor must exist for the given type, and it will be called N times - right?

It's only legal for types that are sufficiently trivial, so the "called constructor" would be trivial. You'll want to follow the links in the page I sent you, it's explained: https://en.cppreference.com/w/cpp/language/classes.html#Impl...

Even better and simpler..

  Payload *item = (Payload *)malloc(offsetof(Payload, elements[N]));
The rest of the article does make me vary of a lot of other things that aren't done "per-spec" if you're making your own container and probably will cause unintended bugs in the future.

Unfortunately, ISO C requires offsetof to calculate a constant; moreover, the right operand is a "member designator", which elements[N] isn't.

However, the traditional (and pretty much the only sensible) ways of defining offsetof do make it work.

N3220 draft:

  offsetof(type, member-designator)
[expands] to an integer constant expression that has type size_t, the value of which is the offset in bytes, to the subobject (designated by member-designator), from the beginning of any object of type type. The type and member designator shall be such that given

  static type t;
then the expression &(t. member-designator) evaluates to an address constant. If the specified type name contains a comma not between matching parentheses or if the specified member is a bit-field, the behavior is undefined.

This doesn't mean I won't use it, but just something to be aware of.

It might not be a bad idea to propose to ISO C that offsetof(type, array[n]) should be required to work, and for non-constant n too.

In GCC, __builtint_offsetof explicitly supports the extended syntax for the member designator. when the offsetof macro uses __builtin_offsetof, the capability is a documented extension.

That's really great; it's easy to forget that the second argument of offsetof isn't simply a member name, but a designator expression.