Designated Initializers in Modern C++

This article provides a comprehensive overview of designated initializers in modern C++, a feature introduced in C++20 to make aggregate initialization clearer, safer, and less error-prone. You will learn the definition of designated initializers, how they are implemented, the specific syntax rules governed by the C++ standard, and how they differ from their counterparts in C.

What Are Designated Initializers?

Designated initializers allow you to initialize the specific members of an aggregate type by name rather than relying purely on member order. Before C++20, aggregate initialization required values to be provided strictly by positional order, which could lead to subtle bugs if two adjacent fields shared the same type or if the struct definition changed.

Designated initializers solve this by using the . member-designator syntax:

struct Configuration {
    int timeout_ms = 1000;
    int max_retries = 3;
    bool enable_logging = false;
};

// Using designated initializers
Configuration config{
    .timeout_ms = 5000,
    .enable_logging = true
};

In the example above, max_retries is omitted and falls back to its default member initializer (or value-initialization if no default is provided).

Requirements for Use

To use designated initializers, the target type must be an aggregate type. An aggregate type in C++ generally means:

  • It has no user-declared or inherited constructors (except default or deleted constructors in certain standard revisions).
  • It has no private or protected non-static data members.
  • It has no virtual member functions.
  • It has no virtual, private, or protected base classes.

Key Rules and How They Work in C++

While C++ adopted designated initializers inspired by C99, C++ applies stricter rules to ensure type safety, avoid ambiguities, and preserve destructor ordering semantics.

1. Declaration Order Is Mandatory

Unlike C99, C++ requires that designated initializers appear in the exact order in which members are declared in the class or struct definition.

struct Vec3 {
    float x;
    float y;
    float z;
};

// Valid: Matches the declaration order
Vec3 v1{.x = 1.0f, .y = 2.0f, .z = 3.0f};

// Valid: 'y' is skipped, but 'x' and 'z' are in relative order
Vec3 v2{.x = 1.0f, .z = 3.0f};

// Compilation Error: 'y' cannot appear after 'z'
Vec3 v3{.z = 3.0f, .y = 2.0f}; 

This restriction ensures that initialization and destruction happen in a predictable, consistent order without hidden runtime overhead.

2. Omitted Fields

Any field not explicitly named in the initializer list will:

  1. Use its default member initializer (e.g., int x = 10;), if one exists.
  2. Be value-initialized (set to zero for primitives, or default-constructed) if no default initializer is specified.

3. No Mixing of Designated and Non-Designated Elements

You cannot mix regular positional initializers with designated initializers in the same initialization clause:

// Compilation Error: Cannot mix positional and designated initializers
Vec3 v4{1.0f, .y = 2.0f, 3.0f}; 

4. Nested Designators Are Not Supported Directly

C99 permits nested designated initializers such as .sub.member = value. C++20 does not support this syntax directly. Instead, you must nest the braces:

struct Window {
    Vec3 size;
    const char* title;
};

// Valid in C++20:
Window win{
    .size = {.x = 1920.0f, .y = 1080.0f},
    .title = "Main Window"
};

// Invalid in C++20:
// Window win{.size.x = 1920.0f, .title = "Main Window"};

5. No Array Designators

C99 permits designating array indices (such as int arr[5] = {[0] = 1, [4] = 5};). C++ does not support designated initializers for arrays; they are strictly reserved for class and struct members.

Benefits of Designated Initializers

  • Enhanced Readability: Code becomes self-documenting. Readers immediately know which value corresponds to which field without inspecting the struct declaration.
  • Safety Against Member Reordering: Swapping the types or names of variables in the underlying struct often results in a compile-time failure rather than silently assigning wrong values.
  • Reduced Boilerplate: You no longer need to write custom builder patterns or explicit constructors for simple data transfer objects.