This is the first post in my C tips for C++ developers series. I spend most of my time in C++, but lately I’ve been working on an embedded project where part of the codebase is pure C. Along the way I stumbled into a feature that made me pause and think “wait, you can do that in C?” I was writing a function to validate BLE advertising parameters and realized I could return a fully initialized struct in a single expression, almost exactly the way I would in C++. That caused me to gather more C features I had either forgotten or never learned properly into this series.

This part covers three of those: designated initializers, compound literals, and how to do forward declarations correctly. The first two landed in C99, so they have been part of the language for nearly 30 years, yet they still surprise C++ developers who encounter them for the first time. Forward declarations work differently in C than in C++, and mixing them with typedef makes things messier than you might expect.

Designated initializers

In C++ (before C++20) you initialize a struct by position:

struct Coordinates
{
    int32_t x;
    int32_t y;
};

struct Coordinates c = {1, 2};

That works, but you have to remember the field order. Add a field in the middle of the struct, or do re-ordering for padding purposes, and every initializer silently shifts. Two fields is manageable, but scale that up to a configuration struct with eight fields, yeah… good luck maintaining it.

C99 fixed this with designated initializers 1. You name the fields:

struct Coordinates c = {.x = 1, .y = 2};

Now the code documents itself. You can read what each value means without counting positions. Even better, any field you leave out is zero-initialized, so you only specify what matters:

// y is automatically 0
struct Coordinates c = {.x = 1};

In C you can also reorder the fields freely:

// valid C99, y comes before x
struct Coordinates c = {.y = 2, .x = 1};

This gets more interesting with nested structs. Say you have a Point2D that contains a Coordinates member and a precomputed squared length:

struct Point2D
{
    int64_t            sqlen;
    struct Coordinates coord;
};

You can reach into the nested struct with dot notation:

struct Point2D p = {.coord.x = 3, .coord.y = 4, .sqlen = 25};

No need to initialize the inner struct separately or use a temporary. The intent is clear at a glance.

C++20 finally brought designated initializers to C++ (via P0329R4 2), but with restrictions. Fields must appear in declaration order, you cannot mix designated and positional initializers, and you cannot skip a field and come back to it later. C99 has none of those limits.

Do note that the zero-initialization of omitted fields is a real safety net. In embedded code you often deal with large configuration structs where most fields should be zero or a default. Designated initializers give you that for free instead of relying on a memset or hoping the caller filled everything in.

Compound literals

Without compound literals or any initializer syntax, you end up declaring empty structs and assigning each field one by one:

struct Point2D get_initialized_point(int32_t x, int32_t y)
{
    struct Coordinates tmp_coord;
    tmp_coord.x = x;
    tmp_coord.y = y;

    int64_t tmp_sqlen = len2_i(&tmp_coord);

    struct Point2D result;
    result.coord = tmp_coord;
    result.sqlen = tmp_sqlen;

    return result;
}

Three named temporary variables, six assignment lines, and nothing stops you from accidentally using result before all fields are set. It gets the job done, but it is noisy. Of course initializers help a bit, but that does not fix the named temporary variables issue.

C99 introduced compound literals 3, which let you create an unnamed object of a given type inline:

struct Coordinates c = (struct Coordinates){.x = 1, .y = 2};

That (struct Coordinates){...} is the compound literal. It looks like a cast, but it is not. It creates a real object. On its own this might not seem like much, just another way to initialize a variable. The real power shows up when you combine it with function calls and return statements.

Say you have a function that computes the squared length of a coordinate vector. It takes a pointer to avoid copying the struct:

int64_t len2_i(const struct Coordinates *v)
{
    return (int64_t) v->x * v->x + (int64_t) v->y * v->y;
}

You can take the address of a compound literal and pass it directly, no temporary variable needed:

int64_t result = len2_i(&(const struct Coordinates){3, 4});

That &(const struct Coordinates){3, 4} creates a Coordinates on the stack and hands its address to the function. The compound literal lives for the duration of the enclosing block, so the pointer is valid for the entire call.

And here is the part that surprised me the most: you can return a compound literal from a function. A function that returns an empty point is just:

struct Point2D get_empty_point(void)
{
    return (struct Point2D){0};
}

The {0} zero-initializes every field. Clean and readable. Now combine that with designated initializers and function calls for a fully initialized return by updating the get_initialized_point function:

struct Point2D get_initialized_point(int32_t x, int32_t y)
{
    return (struct Point2D){.coord.x = x,
                            .coord.y = y,
                            .sqlen   = len2_i(&(const struct Coordinates){x, y})};
}

There is a lot going on in that single return statement. The outer compound literal creates a Point2D with designated initializers for each field. The .sqlen field calls len2_i with the address of an inner compound literal Coordinates. No temporary variables, no partial initialization, everything in one expression, much preferable to the original get_initialized_point function.

Coming from C++, I always assumed this kind of “return a constructed object” pattern required constructors or at least C++ brace initialization. Turns out C99 had it covered all along.

The real-world example

The code that originally sent me down this path was a validation function for BLE advertising parameters. The struct has eight fields, most of them needing their own validation:

struct BleAdvParams
{
    uint16_t            min_adv_interval_ms;
    uint16_t            max_adv_interval_ms;
    EBleAddressType     address_type;
    EBleAdvType         adv_type;
    EBleAdvChannel      adv_channel;
    EBleAdvFilterPolicy filter_policy;
    EBlePhy             primary_phy;
    EBlePhy             secondary_phy;
};

The validate function returns a fully populated struct in one expression:

static inline struct BleAdvParams validate_advertising_parameters(
    int** parameters)
{
    return (struct BleAdvParams)
        {.min_adv_interval_ms = (uint16_t) *parameters[0],
         .max_adv_interval_ms = (uint16_t) *parameters[1],
         .address_type        = ad_addr_type_validate(*parameters[2]),
         .adv_type            = ad_type_validate(*parameters[3]),
         .adv_channel         = ad_channel_validate(*parameters[4]),
         .filter_policy       = ad_filter_validate(*parameters[5]),
         .primary_phy         = ad_phy_validate(*parameters[6]),
         .secondary_phy       = ad_phy_validate(*parameters[7])};
}

Every field is named, each value goes through its own validator, and the whole thing is a single return statement. No temporary variable, no partial initialization, no chance of forgetting a field (the compiler warns about missing initializers if you ask it to).

How C++ does the same thing

In C++ you would typically take a const reference instead of a pointer, which lets you pass a temporary directly. With that, the C++20 equivalent looks almost identical, just without the compound literal syntax:

int64_t len2_i(const Coordinates& v);

struct Point2D get_initialized_point(int32_t x, int32_t y)
{
    return {.coord = {.x = x, .y = y},
            .sqlen = len2_i(Coordinates{x, y})};
}

C++ skips the (type) prefix because the return type is already known from the function signature. Do note that some C++ compilers (GCC, Clang) accept C-style compound literals as an extension, but it is not standard C++ 4.

A word on storage duration

Compound literals in function scope have automatic storage duration, just like local variables 3. When returning by value (as in the examples above) this is fine because the value is copied to the caller. But if you take the address of a compound literal and return that pointer, you get a dangling pointer the moment the function returns. Same rules as any stack variable.

Forward declarations

In C++ you can forward-declare a struct and use it by name without the struct keyword:

struct Coordinates; // forward declaration

int64_t len2_i(const Coordinates *v); // just "Coordinates", no struct keyword

C works differently. The struct keyword is always required when referring to a struct type, even after a forward declaration:

struct Coordinates; // forward declaration

int64_t len2_i(const struct Coordinates *v); // must write "struct Coordinates"

This is fine on its own. The forward declaration tells the compiler “this struct exists, I will define it elsewhere.” You can declare functions that accept or return the struct without including the full definition. The compiler only needs the complete definition at the point where you actually use the struct’s fields or size. Using a pointer here is key: a pointer is just an integer holding a memory address, so the compiler does not need to know the struct layout or size to handle one.

In my libctips library, the private header forward_declare.h uses exactly this pattern. It forward-declares struct Coordinates and declares the len2_i function without pulling in the full struct definition:

struct Coordinates;

int64_t len2_i(const struct Coordinates *v);

The actual struct definition lives in ctips.h, and only the .c files that need to access the fields include it. This keeps the internal header lightweight and avoids unnecessary coupling between translation units. All this is basic stuff, but the more interesting part comes next.

Why typedef makes this messy

A common C pattern is to typedef the struct so you do not have to write struct everywhere:

typedef struct Coordinates Coordinates;

Now you can write Coordinates instead of struct Coordinates. Sounds convenient. If the struct has a tag name, the typedef itself doubles as a forward declaration because it simultaneously introduces the struct tag and the alias:

// this works as a forward declaration
typedef struct Coordinates Coordinates;

The real problem shows up when you use an anonymous struct:

// coordinates.h
typedef struct
{
    int32_t x;
    int32_t y;
} Coordinates;

With an anonymous struct like this, there is no tag name to forward-declare. You must include the full header everywhere you need the type, even if all you want is a function prototype. You have lost the ability to forward-declare entirely.

Even with a tagged struct, if the typedef and definition live together in one header, every file that wants to use the short name has to pull in the full definition. Compare that to C++ where struct Coordinates; gives you both the forward declaration and the short name in one line.

This is not just my preference. The Linux kernel coding style guide 5 puts it bluntly: it is a mistake to typedef structures and pointers. Their reasoning is that struct virtual_container *a tells you exactly what you are dealing with, while vps_t a hides it. The guide lists a handful of exceptions (opaque types, integer width aliases like u32, sparse type-checking), but for regular structs the rule is clear: do not typedef them.

In short: skip the typedef entirely and just write struct everywhere, as the libctips library does. It is a few extra characters, but it keeps forward declarations clean, your headers lightweight, and the type visible at every use site.

Putting it all together

Designated initializers let you name struct fields at initialization (and even reach into nested structs with .member.field syntax). Compound literals let you create struct values inline without a separate variable. Forward declarations let you keep headers minimal by declaring struct types without their full definition. Together they give C a set of patterns that C++ developers already know in slightly different forms. For embedded C code full of configuration structs and layered headers, these features keep things readable and well-organized.

You can find the full example code in the libctips library on my examples repo. It forces C99 and C++11 so you can be sure that there is no modern trickery here!

-sorhanp

References