New Standard, new ways to initialize objects!

With C++20, we get a handy way of initializing data members. The new feature is called designated initializers and might be familiar to C programmers.

Let’s have a look at this small feature:

The basics

Designated Initialization is a form of Aggregate Initialization.

As of C++20, an Aggregate type::

  • is an array type or,
  • is a class type that:
    • has no private or protected direct non-static data members
    • has no user-declared or inherited constructors
    • has no virtual, private, or protected base classes
    • has no virtual member functions

In a basic form in C++20, you can write:

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

For example:

struct Point { 
    double x { 0.0 };
    double y { 0.0 };
};

const Point p { .x = 10.0, .y = 20.0 };
 
const Point offset { .x { 100.0 }, .y { -100.0 } };

// mix also possible:
const Point translation { .x = 50.0, .y { -40.0 } };

Play @Compiler Explorer

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

This article started as preview for Patreon. If you want to get elusive content, early previews, bonus materials and access to Discord server, join
the C++ Stories Premium membership.
Special deals in Nov 2021!

Why are designated initializers handy?

One of the main reasons to use this new kind of initialization is to increase readability.

It’s easier to read:

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

Date inFuture { .year = 2050, .month = 4, .day = 10 };

Than:

Date inFuture { 2050, 4, 10 };

In the case of the date class, it might not be clear what’s the order of days/month or month/days. With designated initializers, it’s effortless to see the order.

Or have a look at some Configuration class:

struct ScreenConfig {
    bool autoScale { false };
    bool fullscreen { false };
    int bits { 24 };
    int planes { 2 };
};

// hmmmm.... ?
ScreenConfig cfg { true, false, 8, 1 }; 

// better?
ScreenConfig playbackCfg {
    .autoScale = true, .fullscreen = false, .bits = 8, .planes = 1
};

Rules for designated initializers

The following rules apply to designated initializers:

  • Designated initializers work only for aggregate initialization
  • Designators can only refer to non-static data members.
  • Designators in the initialization expression must have the same order of data members in a class declaration.
  • Not all data members must be specified in the expression.
  • You cannot mix regular initialization with designaters.
  • There can be only one designator for a data member.
  • You cannot nest designators

For example, the following lines won’t compile:

struct Date {
    int year;
    int month;
    int day;
    MinAndHour mh;

    static int mode;
};

Date d1 { .mode = 10; }             // err, mode is static!
Date d2 { .day = 1, .year = 2010 }; // err, out of order!
Date d3 { 2050, .month = 12 };      // err, mix!
Date d4 { .mh.min = 55 };           // err, nested!

Advantages of designated initialization

  • Readability. A designator points out 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 initialization form (although even more relaxed). With the C++20 feature, it’s possible to have a 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

Let’s have a look at some examples:

#include <iostream>
#include <string>

struct Product {
    std::string name_;
    bool inStock_ { false };
    double price_ = 0.0;
};

void Print(const Product& p) {
  std::cout << "name: " << p.name_ << ", in stock: "
            << std::boolalpha << p.inStock_ << ", price: " 
            << p.price_ << '\n';
}

struct Time { int hour; int minute; };
struct Date { Time t; int year; int month; int day; };

int main() {
  Product p { .name_ = "box", .inStock_ {true }};
  Print(p);
  
  Date d { 
      .t { .hour = 10, .minute = 35 }, 
      .year = 2050, .month = 5, .day = 10 
  };

  // pass to a function:
  Print({.name_ = "tv", .inStock_ {true }, .price_{100.0}});

  // not all members used:
  Print({.name_ = "car", .price_{2000.0}});
}

Play @Compiler Explorer

It’s also interesting that we can also use designated initialization inside another designated initialization, for example:

struct Time { int hour; int minute; };
struct Date { Time t; int year; int month; int day; };

Date d { 
    .t { .hour = 10, .minute = 35 }, 
    .year = 2050, .month = 5, .day = 10 
};

But we can’t use “nested” ones like:

Date d { 
    .t.hour = 10, .t.minute = 35, .year = 2050, .month = 5, .day = 10 
};

The syntax .t.hour won’t work.

Summary

As you can see, with designated initializers, we got a 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.

More in the paper P0329 and wording in P0329R4 and @CppReference.

The feature is available in GCC 8.0, Clang 10.0, and MSVC 2019 16.1

Have you tried Designated Initializers?