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 a preview for Patrons months ago. If you want to get exlusive content, early previews, bonus materials and access to Discord server, join the C++ Stories Premium membership.

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?