12. Aggregates and Designated Initializers in C++20

Across the book, you’ve seen a lot of cases for intuitively simple structures with all public data members. Such types, along with arrays, are called Aggregates. In this chapter, we’ll look at some C++20 changes and new ways to initialize such objects.

Aggregates in C++20

To sum up, as of C++20, here’s the definition of an aggregate type from the C++ Standard: dcl.init.aggr.

Here are some examples of aggregates:

Ex 13.1. Aggregate classes, several examples. Run @Compiler Explorer
struct Base { int x {42}; };
struct Derived : Base { int y; };

struct Param { 
    std::string name; 
    int val; 
    void Parse();  // member functions allowed
};

int main() {
    Derived d {100, 1000};    
    std::cout << "d.x " << d.x << ", d.y " << d.y << '\n';
    Derived d2 { 1 };
    std::cout << "d2.x " << d2.x << ", d2.y " << d2.y << '\n';
    Param p {"value", 10};
    std::cout << "p.name " << p.name << ", p.val " << p.val << '\n';

    double arr[] { 1.1, 2.2, 3.3, 4.4};
    std::cout << "arr[0] " << arr[0] << '\n';
    Param params[] {{"val", 10}, {"name", 42}};
    std::cout << "params[0].name " << params[0].name << '\n';
}

In C++20, in some limited cases, you can also use parens X(args...) to initialize an aggregate:

// C++20 and parens:
Point pt (1, 2);
// Point pt = (1, 2); // doesn't work

double params[] (9.81, 3.14, 1.44);
// double paramsDeduced[] = (9.81, 3.14, 1.44); // won't deduce
int arrX[10]  (1, 2, 3, 4); // rest is 0

Such improvement helps, especially in a generic template code where you want to work with various types of objects. For example, the following code wasn’t possible until C++20:

Ex 13.2. Aggregates and parens for make_unique. Run @Compiler Explorer
struct Point { int x; int y; };

int main() {
    auto ptr = std::make_unique<Point>(10, 20);
}

make_unique takes a variable number of arguments and passes them to a constructor. This function uses parens to call the constructor. Since aggregates has no user-declared constructors, then such syntax generated errors. With the C++20 change, the code works fine now. Suppose you want to dig more into this topic. In that case, I highly recommend reading C++20’s parenthesized aggregate initialization has some downsides – Arthur O’Dwyer, which discusses pros and cons of this new initialization syntax.

The basics of Designated Initializers

The C++20 Standard also gives us another handy way to initialize data members. The new feature is called designated initializers, which might be familiar to C programmers.

As of C++20, to initialize an aggregate object, you can write the following:

Type obj = { .designator = val, .designator { val2 }, ... };

For example:

struct Point { double x; double y; };
Point p { .x = 10.0, .y = 20.0 };

Designator points to a name of a non-static data member from our class, like .x or .y.

One of the main reasons to use this new kind of initialization is to increase readability. Compare the following initialization forms:

struct Date {
    int year;
    int month;
    int day;
};

// new
Date inFutureCpp20 { .year = 2050, .month = 4, .day = 10 };
// old
Date inFutureOld   { 2050, 4, 10 };

In the case of the Date class, it might be unclear what the order of days/month or month/days is. With designated initializers (inFutureCpp20), it’s very easy to see the order of data members.

Rules

Content available in the full version of the book.

Advantages of designated initialization

  • Readability: A designator points to the specific data member, so it’s impossible to make mistakes here.
  • Flexibility: You can skip some data members and rely on default values for others.
  • Compatibility with C: In C99, it’s popular to use a similar form of initialization (although even more relaxed). With the C++20 feature, it’s possible to have very similar code and share it.
  • Standardization: Some compilers, like GCC or Clang, already had some extensions for this feature, so it’s a natural step to enable it in all compilers.

Examples

Content available in the full version of the book.

Summary

As you can see, designated initializers are handy and usually more readable way of initializing aggregate types. The new technique is also common in other programming languages, like C or Python, so having it in C++ makes the programming experience even better.

The summary example

Content available in the full version of the book.

Compiler support

Here’s a table with support for the features we discussed:

Content available in the full version of the book.