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.
And here are various ways to make a type not an aggregate.
Following that, aggregate initialisation is when you use list-initialisation, i.e. {}
to initialise aggregates. Typically something like the following.
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.
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.
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:
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.
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).
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.