Compared to many other C++20 additions this one is quite small, only needing a single box of explanation on cppreference. Despite this, I quite like the feature as it helps code that uses aggregate initialisation to be more readable and less error prone - both are well needed in that area!

Aggregate Initialisation

As you might know, designated initialisers is a feature that expands on aggregate initialisation, which as you might guess from the name is how you initialise aggregates. But what is an aggregate?

The full description of what an aggregate is can be read on cppreference, but in layman’s terms, aggregates are class types (includes struct and unions too) that are “POD style” in the sense that all member variables have to be public and it can not have any user-declared or inherited constructors. It also cannot use virtual member functions or virtual inheritance. Also, array types are always aggregates.

Here’s an example of an aggregate type.

struct Base
{
    int baseNumber;
};

struct Aggregate: Base //public inheritance is fine
{
    int number = 0; //default member initialisation is fine
    std::string text; //having non-trivial types is fine
    size_t text_length(); //member functions are fine
};

And here are various ways to make a type not an aggregate.

struct Base
{
    int baseNumber;
};

struct NotAggregate: private Base //private inheritance breaks it
{
    NotAggregate(std::string t); //user declared constructor breaks it
    int number;
    virtual size_t text_length(); //virtual member function breaks it
    private:
        std::string text; //private member variable breaks it
};

Following that, aggregate initialisation is when you use list-initialisation, i.e. {} to initialise aggregates. Typically something like the following.

struct S
{
    int number;
    std::string text;
};

S f()
{
    return S{0, "hi"};
}

Aggregate initialisation is of course very useful since without it you need to declare a temporary variable and set all the variables one by one. Look at the following.

//using S struct from above
S f()
{
    S s;
    s.number = 0;
    s.text = "hi";
    return S;
}

This way was in fact the only choice you had before C++11 if you didn’t want a constructor in S.

Pre-C++20 Aggregate Initialisation is Problematic

The problem with this approach is when our types are bit bigger and especially if they grow over time. Consider a kind of Settings struct.

struct Settings
{
    int rowCount = 0;
    int columnCount = 0;
    int bitdepth = 32;
    int framerate = 30;
    Color bgColor = Color::black();
    RenderMode renderMode = RenderMode::normal;
};

void f()
{
    Settings settings{128, 128, 32, 60, Color::black(), RenderMode::wireframe};
    //...use settings
}

Now where settings is initialised, readability is quite low since you just have a sequence of unnamed parameters. Furthermore, if we add/remove/change things to the Settings struct over time, we have to be very careful. Since the initialisation relies on ordering, in the worst case we might get incorrect behaviour and the compiler won’t even warn us. This would happen if we swapped the declaration order of bitdepth and framerate, for example:

As an extra annoyance, look also how we have to include all the parameters in the list, just because we wanted a non-default RenderMode at the end. Two of the parameters just repeat the default value, and if the default value is changes in the struct… oops, now we are accidentally using a different value.

Enter Designated Initialisers

The gist of this new feature is that it lets us specify the values that we initialise based on their name. For example:

struct S
{
    int a = 0;
    int b = 5;
    std::string c;
};

S f()
{
    return S{.a=3, .c="hi"}; //values are initialised based on name
}

Here we set the members a and c, while b is left to its default value of 5 which is clearer.

There are some restrictions on how it can be used. Most notably, all designators must come in the same order as they are declared in the type. So for example, we cannot have .a=3 at the end of the list. Skipping entries is fine however, like how we skipped b entirely in the example above.

Brace or Equals

There are two ways of setting values with this feature; brace initialisers or equal initialisers: S{.a{30}, .b=9}. The brace version is a bit more relaxed as it allows narrowing conversions. Prefer the equals one unless you have reason not to. Correction: None of these allow narrowing conversions.

A boon for readability and safety

All the problems mentioned above with the old-style initialisers are alleviated by using this new feature!

If we continue with the problematic Settings struct, without changing anything we can now instead initialise it like follows.

void f()
{
    Settings settings{.rowCount=128, .columnCount=128,
        .framerate=60, .renderMode=RenderMode::wireframe};
    //...use settings
}

Suddenly we can reason about what we are actually initialising without having to look at the definition of Settings. Much better.

Furthermore, we are also able to leave out the values that we are actually not interested in setting, such as the bitdepth in this example. It is also going to be more robust against errors, in the case of someone changing the Settings struct. For example, if the order of bitdepth and framerate is switched around, we’ll get a compiler error instead of faulty runtime behaviour.

For the same reasons, I think this feature is worth applying when you have those typical functions with very many parameters (even though this itself is a code-smell, sometimes we end up with them).

//instead of this
void crazyFunc(/*lots of parameters here*/);

//we can do
struct CrazyFuncParams
{
    /*lots of parameters here*/
};
void crazyFunc(CrazyFuncParams p);

//and our call site would benefit a lot:
void f()
{
    crazyFunc({.param1 = 45, .param6 = "blopp"});
}

I am for sure going to start using this feature as the default way to initialise my aggregates and I happily welcome features which reduce foot-shooting risks and aid readability through simply opting to use them.

Even though C++20 is still a while away, you can potentially already start using this feature since both GCC and Clang have supported some version of this for years already due to the feature existing in C. Beware though that there might be some discrepancies on how you’re allowed to use them and it’s not guaranteed to work on all compilers.