Designated Initializers and Compound Literals in C99: A Practical Guide to Order-Independent Initialization
C99 designated initializers let you name struct fields and array indices in any order, with omitted members guaranteed zero. How the mechanism works, where compound literals fit, and the mistakes to avoid.
11 Apr 2026, 23:40 UTC

If you have ever reordered fields in a C struct and then hunted down every positional initializer that silently broke, designated initializers are the fix. Since C99, you can name the fields you set, in any order, and every field you omit is zero-initialized. That single guarantee turns "zeroed struct with a few fields set" from a memset plus assignments into one readable declaration.
The core mechanism
A designated initializer names the member or array index being set:
struct point { int x; int y; };
struct point p = { .y = 2, .x = 1 }; /* order no longer matters */
struct point origin = { 0 }; /* classic all-zero */
struct point unit = { .x = 1 }; /* y is zero-initialized */
int table[10] = { [3] = 5, [7] = 9 }; /* everything else is 0 */
The important semantic rule: in any aggregate initializer, members you do not mention are initialized as if the object had static storage duration — that means zero for scalars, null for pointers, and recursively zero for nested aggregates. This is a language guarantee, not a compiler nicety, so partial initialization is a safe idiom, not a shortcut that leaves garbage behind.
A worked example: configuration structs
The most common engineering use is a large configuration or state struct where most fields want defaults:
struct server_config {
const char *bind_addr;
int port;
int max_connections;
int timeout_ms;
int use_tls;
const char *cert_path;
};
struct server_config cfg = {
.bind_addr = "0.0.0.0",
.port = 8080,
.max_connections = 256,
/* timeout_ms, use_tls, cert_path default to 0 / NULL */
};
Two practical wins follow. First, diffs stay clean: adding a field to the struct never forces you to touch existing initializers. Second, reordering struct members — for example to pack the struct better — cannot silently swap two int fields the way it can with positional initializers, where { 8080, 256 } still compiles but means something different after the reorder.
Sparse lookup tables get the same benefit at array scope:
static const int http_status_class[600] = {
[200] = 2, [201] = 2, [204] = 2,
[301] = 3, [302] = 3,
[400] = 4, [401] = 4, [403] = 4, [404] = 4,
[500] = 5, [502] = 5, [503] = 5,
};
All unlisted entries are zero. No memset, no loop, and the table reads as a direct mapping.
Compound literals: unnamed objects on the spot
A compound literal creates an unnamed object inline, and you can combine it with designators:
draw_rect(&(struct rect){ .w = 10, .h = 20 });
The critical detail is lifetime. At block scope, a compound literal has automatic storage duration: the object lives until the end of the enclosing block. Passing its address into a call that uses it synchronously is fine. Storing that pointer somewhere that outlives the block — including returning it from the function — is undefined behavior once the block exits. At file scope, a compound literal has static storage duration, so taking its address is always safe.
struct rect *bad(void)
{
return &(struct rect){ .w = 1 }; /* dangling after return: UB */
}
static const struct rect *good =
&(struct rect){ .w = 1 }; /* file scope: static, fine */
Limits and common mistakes
- Order still matters within one initializer. Designators for the same object must appear in declaration order, and if you mix positional and designated forms, the positional element continues from the last designator.
{ .y = 2, 5 }setsythen continues at the next member — surprising to read, so avoid mixing. - Padding bytes are not zeroed. Omitted members are zeroed, but padding between members is unspecified. Never
memcmptwo such structs for equality; compare field by field. - Shared C/C++ headers. C++ only gained a restricted form in C++20: designators must match declaration order and array designators are not supported. A header compiled as both C and C++ cannot rely on full C99 semantics.
- Language standard. These constructs require C99 or later. Strict C90 modes and very old compilers reject them. Check your build flags before adopting.
Verifying on your toolchain
Compile a small probe with warnings enabled. Run this in your project environment with your normal compiler (no special permissions needed):
gcc -std=c99 -Wall -Wextra -pedantic probe.c -o probe
The probe should compile with no warnings and assert the zero-initialization guarantee:
#include <assert.h>
struct cfg { int a; int b; const char *s; };
int main(void)
{
struct cfg c = { .b = 7 };
assert(c.a == 0 && c.s == 0);
return 0;
}
If headers must also build as C++, recompile with g++ -std=c++20 to see which forms are accepted. Expected result: ordered scalar designators pass; out-of-order or array designators fail.
When to adopt
Use designated initializers for config and state structs, sparse tables, and any struct with more than a couple of fields. Reserve compound literals for arguments consumed within the same block. The combination removes a whole class of reordering bugs and makes initialization intent explicit at the cost of requiring a C99-or-later build, which nearly every current toolchain already defaults to.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.