Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Designated Initialization (C++20)

Understanding Aggregate Types

Before diving into designated initialization, we need to understand aggregate types, which are the only types that support this feature.

What is an Aggregate Type?

An aggregate type is a specific category of data structure in C++ (usually a struct, class, or array) that meets a strict set of criteria. Essentially, an aggregate is a plain, simple data container that has not implemented any “special” object-oriented features or access controls. Because they are simple data structures, they can be initialized using a straightforward brace-enclosed list of values (aggregate initialization).

C++20 Aggregate Definition

In C++20, the definition of an aggregate was slightly simplified and refined. A class type (struct, class, or union) is an aggregate if it satisfies all the following conditions:

ConditionDescriptionExample of Breaking the Rule
No User-Provided ConstructorsYou cannot explicitly define any constructors (even default or delete ones).struct A { A() {} };
No Private/Protected Non-Static Data MembersAll non-static data members must be public.struct A { private: int x; };
No Virtual FunctionsThe class cannot be part of a polymorphic hierarchy.struct A { virtual void f() {} };
No Virtual, Private, or Protected Base ClassesIt can have public base classes, but they must adhere to specific rules related to access.struct A : private B {};

Examples of Aggregate Types

// Valid aggregate - simple struct
struct Point {
    int x;
    int y;
};

// Valid aggregate - with default member initializers
struct Config {
    int timeout = 30;
    bool verbose = false;
    std::string mode = "auto";
};

// Valid aggregate - nested aggregates
struct Rectangle {
    Point topLeft;
    Point bottomRight;
};

// Valid aggregate - array
int numbers[5];

// Valid aggregate - with public base class (C++17+)
struct Base {
    int base_value;
};

struct Derived : Base {
    int derived_value;
};

Examples of Non-Aggregate Types

// NOT an aggregate - has user-provided constructor
struct WithConstructor {
    int x;
    WithConstructor() : x(0) {}
};

// NOT an aggregate - has private members
struct WithPrivate {
private:
    int x;
public:
    int y;
};

// NOT an aggregate - has virtual function
struct WithVirtual {
    int x;
    virtual void process() {}
};

// NOT an aggregate - has private base class
struct Base { int x; };
struct NotAggregate : private Base {
    int y;
};

// NOT an aggregate - has protected members
class WithProtected {
protected:
    int x;
public:
    int y;
};

Traditional Aggregate Initialization (Pre-C++20)

Aggregates have always supported list initialization, but members had to be initialized in order:

struct Point {
    int x;
    int y;
    int z;
};

// Traditional aggregate initialization
Point p1{10, 20, 30};           // All members
Point p2{10, 20};               // z gets default value (0)
Point p3{10};                   // y and z get default values
Point p4{};                     // All members get default values

// Problem: What does each number mean?
Point p5{100, 200, 300};        // Not self-documenting

The limitation? You couldn’t skip members or initialize them out of order, and the code wasn’t self-documenting.

Designated Initialization (C++20)

Designated initialization allows you to explicitly name which members you’re initializing, making code more readable, maintainable, and less error-prone.

Basic Syntax

struct Point {
    int x;
    int y;
    int z;
};

// Designated initialization - explicitly name members
Point p1{.x = 10, .y = 20, .z = 30};
Point p2{.x = 10, .z = 30};              // y gets default value (0)
Point p3{.z = 30};                        // x and y get default values

Rules and Constraints

Designated initialization has specific rules to maintain clarity and prevent ambiguity:

1. Must Follow Declaration Order

struct Data {
    int a;
    int b;
    int c;
};

// Correct - follows declaration order
Data d1{.a = 1, .b = 2, .c = 3};
Data d2{.a = 1, .c = 3};           // OK: skipping b

// Error - out of order
Data d3{.c = 3, .a = 1};           // Compilation error!
Data d4{.b = 2, .a = 1};           // Compilation error!

2. Cannot Mix Designated and Non-Designated Initializers

struct Point {
    int x;
    int y;
};

// All designated
Point p1{.x = 10, .y = 20};

// All non-designated
Point p2{10, 20};

// Error - cannot mix
Point p3{10, .y = 20};             // Compilation error!
Point p4{.x = 10, 20};             // Compilation error!

3. Each Member Can Only Be Initialized Once

struct Data {
    int value;
};

// Error - duplicate initialization
Data d{.value = 10, .value = 20};  // Compilation error!

Practical Examples

Example 1: Configuration Structures

struct ServerConfig {
    std::string host = "localhost";
    int port = 8080;
    int timeout = 30;
    bool ssl_enabled = false;
    int max_connections = 100;
};

// Clear and self-documenting
ServerConfig production{
    .host = "api.example.com",
    .port = 443,
    .ssl_enabled = true,
    .max_connections = 1000
    // timeout uses default value (30)
};

ServerConfig development{
    .port = 3000,
    .max_connections = 10
    // Other members use default values
};

Example 2: Nested Structures

struct Address {
    std::string street;
    std::string city;
    std::string zipcode;
};

struct Person {
    std::string name;
    int age;
    Address address;
};

// Nested designated initialization
Person person{
    .name = "Alice Smith",
    .age = 30,
    .address = {
        .street = "123 Main St",
        .city = "Springfield",
        .zipcode = "12345"
    }
};

Example 3: With Default Member Initializers

struct Options {
    bool verbose = false;
    bool debug = false;
    int log_level = 1;
    std::string output_file = "output.txt";
};

// Only override what you need
Options opts1{.verbose = true};
Options opts2{.debug = true, .log_level = 3};
Options opts3{.output_file = "custom.log"};

Example 4: Function Parameters

struct RenderOptions {
    int width = 800;
    int height = 600;
    bool fullscreen = false;
    int antialias = 4;
};

void render(const RenderOptions& options) {
    // Use options...
}

// Clean function calls
render({.width = 1920, .height = 1080, .fullscreen = true});
render({.antialias = 8});

Non-Aggregate Types: Designated Initialization Not Allowed

Designated initialization only works with aggregate types. Let’s see what happens when we try to use it with non-aggregates:

Example 1: Type with Constructor

struct WithConstructor {
    int x;
    int y;
    
    // User-provided constructor makes this NOT an aggregate
    WithConstructor(int a, int b) : x(a), y(b) {}
};

// Error - designated initialization not allowed
WithConstructor obj{.x = 10, .y = 20};  // Compilation error!

// Must use constructor
WithConstructor obj(10, 20);            // OK

Example 2: Type with Private Members

class WithPrivate {
private:
    int x;
    int y;
    
public:
    WithPrivate(int a, int b) : x(a), y(b) {}
    int getX() const { return x; }
    int getY() const { return y; }
};

// Error - not an aggregate due to private members
WithPrivate obj{.x = 10, .y = 20};      // Compilation error!

// Must use constructor
WithPrivate obj(10, 20);                // OK

Example 3: Type with Virtual Functions

struct WithVirtual {
    int x;
    int y;
    
    virtual void process() { /* ... */ }
};

// Error - not an aggregate due to virtual function
WithVirtual obj{.x = 10, .y = 20};      // Compilation error!

// Must use default initialization or constructor
WithVirtual obj;                        // OK (default initialization)
obj.x = 10;
obj.y = 20;

Why This Restriction?

The restriction to aggregate types makes sense because:

  1. Aggregates are simple data containers - No complex initialization logic or invariants to maintain
  2. Public members ensure visibility - You can only initialize what you can see
  3. No constructors means no conflicts - Designated initialization doesn’t compete with constructor overloading
  4. Predictable behavior - Simple, direct member initialization without side effects

Benefits of Designated Initialization

1. Self-Documenting Code

// Without designated initialization - unclear what each value means
ServerConfig config1{"example.com", 443, 60, true, 500};

// With designated initialization - crystal clear
ServerConfig config2{
    .host = "example.com",
    .port = 443,
    .timeout = 60,
    .ssl_enabled = true,
    .max_connections = 500
};

2. Partial Initialization Made Easy

struct Settings {
    int value_a = 10;
    int value_b = 20;
    int value_c = 30;
    int value_d = 40;
};

// Only override what you need, rest use defaults
Settings s1{.value_b = 100};
Settings s2{.value_a = 5, .value_d = 50};

3. Refactoring Safety

When you add new members to a struct, designated initialization is more resilient:

// Original struct
struct Point {
    int x;
    int y;
};

Point p{.x = 10, .y = 20};  // Designated initialization

// Later, add a new member
struct Point {
    int x;
    int y;
    int z = 0;  // New member with default
};

Point p{.x = 10, .y = 20};  // Still works! z gets default value

// Compare with traditional initialization
Point p1{10, 20};           // Also still works, but...
Point p2{10, 20, 30};       // New code must be updated everywhere

4. Reduced Errors

struct Color {
    int red;
    int green;
    int blue;
    int alpha = 255;
};

// Easy to mix up the order
Color c1{0, 128, 255};      // Which is which?
Color c2{255, 128, 0};      // Different color, but similarly confusing

// Designated initialization prevents mistakes
Color c3{.red = 0, .green = 128, .blue = 255};
Color c4{.red = 255, .green = 128, .blue = 0};

5. Better Default Handling

struct HTTPRequest {
    std::string url;
    std::string method = "GET";
    int timeout = 30;
    bool follow_redirects = true;
    int max_redirects = 5;
    std::map<std::string, std::string> headers = {};
};

// Only specify what differs from defaults
HTTPRequest req1{
    .url = "https://api.example.com/data"
};

HTTPRequest req2{
    .url = "https://api.example.com/upload",
    .method = "POST",
    .timeout = 60
};

6. Improved API Design

Designated initialization encourages cleaner API designs with option structs:

// Before: Multiple overloaded functions
void createWindow(int width, int height);
void createWindow(int width, int height, bool fullscreen);
void createWindow(int width, int height, bool fullscreen, int samples);

// After: Single function with options struct
struct WindowOptions {
    int width = 800;
    int height = 600;
    bool fullscreen = false;
    int samples = 1;
    bool vsync = true;
    std::string title = "Window";
};

void createWindow(const WindowOptions& options);

// Usage is much cleaner
createWindow({.width = 1920, .height = 1080, .fullscreen = true});
createWindow({.title = "My Game", .vsync = false});

Comparison with C Designated Initializers

C++20 designated initializers are inspired by C99, but with stricter rules:

C (C99) - More Flexible

struct Point {
    int x;
    int y;
    int z;
};

// C allows out-of-order
struct Point p1 = {.z = 30, .x = 10, .y = 20};  // OK in C

// C allows mixing
struct Point p2 = {.x = 10, 20, 30};             // OK in C

// C allows array designated initializers
int arr[10] = {[0] = 1, [5] = 2, [9] = 3};      // OK in C

C++ (C++20) - More Restrictive

struct Point {
    int x;
    int y;
    int z;
};

// C++ requires declaration order
Point p1{.z = 30, .x = 10};              // Error in C++

// C++ doesn't allow mixing
Point p2{.x = 10, 20, 30};               // Error in C++

// C++ doesn't support array designated initializers
int arr[10] = {[0] = 1, [5] = 2};        // Error in C++

Why stricter in C++? The restrictions maintain consistency with C++’s stronger type system and make the code more predictable and less error-prone.

Best Practices

1. Use for Configuration and Options

// Perfect use case
struct Config {
    std::string database_url = "localhost:5432";
    int pool_size = 10;
    bool enable_logging = true;
};

Config cfg{.database_url = "prod.db.com", .pool_size = 50};

2. Combine with Default Member Initializers

// Provides sensible defaults, easy to override
struct Settings {
    int value = 100;
    bool flag = false;
};

Settings s{.flag = true};  // value uses default

3. Prefer for Structs with Many Members

// When you have 5+ members, designated initialization shines
struct ComplexOptions {
    int opt1 = 0;
    int opt2 = 0;
    int opt3 = 0;
    int opt4 = 0;
    int opt5 = 0;
    int opt6 = 0;
};

// Much clearer than: ComplexOptions{0, 0, 5, 0, 0, 10}
ComplexOptions opts{.opt3 = 5, .opt6 = 10};

4. Avoid for Simple Coordinate-Like Types

struct Point { int x; int y; };

// Traditional initialization is fine here
Point p{10, 20};  // Clear enough

// Designated might be overkill
Point p{.x = 10, .y = 20};  // Also fine, but more verbose

Summary

Designated initialization is a powerful C++20 feature that makes code more readable, maintainable, and less error-prone. It works exclusively with aggregate types, which are simple data structures without user-provided constructors, private members, or virtual functions.

Key Takeaways:

  • Only works with aggregate types
  • Members must be initialized in declaration order
  • Cannot mix designated and non-designated initialization
  • Improves code clarity and reduces errors
  • Excellent for configuration structures and option objects
  • More restrictive than C designated initializers, but safer
  • Combines beautifully with default member initializers

Designated initialization represents a significant improvement in C++’s ability to write clear, self-documenting initialization code while maintaining type safety and predictability.