Jonathan Boccara's blog

Extended Aggregate Initialisation in C++17

Published July 17, 2021 - 0 Comments

Daily C++

By upgrading a compiler to C++17, a certain piece of code that looked reasonable stopped compiling.

This code doesn’t use any deprecated feature such as std::auto_ptr or std::bind1st that were removed in C++ 17, but it stopped compiling nonetheless.

Understanding this compile error will let us better understand a new feature of C++17: extended aggregate initialisation.

The code in C++14

Consider the following code:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d{};
}

This code is a classical trick related to the CRTP, to avoid passing the wrong class to the CRTP base class.

Indeed, in C++14, the above code compiles, but a slightly modified version where the CRTP derived class doesn’t pass itself as a template parameter to the base class doesn’t compile even in C++14:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct X{};

struct Derived : Base<X> // passing the wrong class here
{
};

int main()
{
    Derived d{};
}

When trying to construct Derived, it needs to call the constructor of it base class Base but the latter is private and only friend with the template parameter. The template parameter has to be Derived for the code to compile.

Here is the compile error in C++14 for the second case (run the code):

<source>: In function 'int main()':
<source>:17:15: error: use of deleted function 'Derived::Derived()'
   17 |     Derived d{};
      |               ^
<source>:11:8: note: 'Derived::Derived()' is implicitly deleted because the default definition would be ill-formed:
   11 | struct Derived : Base<X>
      |        ^~~~~~~
<source>:11:8: error: 'Base<Derived>::Base() [with Derived = X]' is private within this context
<source>:5:5: note: declared private here
    5 |     Base(){};
      |     ^~~~

And in C++14, the first version compiles fine. All good.

The code in C++17

Let’s take again our first correct version that compiles in C++14:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d{};
}

If we try to compile it with C++17, we get the following error:

<source>: In function 'int main()':
<source>:15:15: error: 'Base<Derived>::Base() [with Derived = Derived]' is private within this context
   15 |     Derived d{};
      |               ^
<source>:5:5: note: declared private here
    5 |     Base(){};
      |     ^~~~

Base is still friend with Derived, how come the compiler won’t accept to construct a Derived object?

Can you see the problem?

 

 

Take a few moments to look at the code…

 

 

If you don’t see why this doesn’t compile, it will be all the more instructive if you have spent time thinking about it…

 

 

Found it yet?

 

 

Ok let’s see what’s going on here.

Extended aggregate initialisation

One of the features that C++17 brings is that it extends aggregate initialisation.

Aggregate initialisation is when a call site constructs an objects by initialising its members without using an explicitly defined constructor. Here is an example:

struct X
{
    int a;
    int b;
    int c;
};

We can then construct X the following way:

X x{1, 2, 3};

The call site initialises a, b and c with 1, 2 and 3, without any constructor for X. This is allowed since C++11.

However the rules for this to work are pretty strict: the class cannot have private members, base classes, virtual functions, and plenty other things.

In C++17 one of those rules got relaxed: we can perform aggregate initialisation even if the class has a base class. The call site then has to initialise the base class.

For example, consider the following code:

struct X
{
    int a;
    int b;
    int c;
};

struct Y : X
{
    int d;
};

Y inherits from X. In C++14, this disqualifies Y from aggregate initialisation. But in C++17 we can construct an Y like this:

Y y{1, 2, 3, 4};

or

Y y{ {1, 2, 3}, 4};

Both syntaxes initialise a, b, c and d to 1, 2, 3 and 4 respectively.

We can also write this:

Y y{ {}, 4 };

This initialises a, b and c to 0 and d to 4.

Note that this is not equivalent to this:

Y y{4};

As this initialises a (not d) to 4, and b, c and d to 0.

We can also specify a part of the attributes in X:

Y y{ {1}, 4};

This initialises a to 1, b and c to 0, and d to 4.

Now that we’re familiar with extended aggregate initialisation, let’s go back to our initial code.

Why our code stopped compiling

Here was our code that compiled fine in C++14 and stopped compiling in C++17:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d{};
}

Notice the braces at call site of the construction of Derived? In C++17, they trigger aggregate initialisation, and they try to instantiate Base, which has a private constructor. This is why it stops compiling.

What is interesting to note is that it is the call site of the constructor that constructs the base class, and not the constructor itself. Indeed, if we modify the Base class for it to be friend with the call site of the constructor, the code compiles fine in C++17 too:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend int main(); // this makes the code compile
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d{};
}

Of course, we’re not going to keep code that way, with a friend to each call site! This change was just to illustrate the fact that the call site directly calls the constructor of the base class.

To fix the code we can… remove the braces:

template<typename Derived>
struct Base
{
private:
    Base(){};
    friend Derived;
};

struct Derived : Base<Derived>
{
};

int main()
{
    Derived d;
}

And it compiles ok again.

Note though that we no longer benefit from value initialisation. If Derived or class were to contain data members, we’d need to make sure to initialise them in explicitly declared constructors or when declaring those members in the class.

This example let us better understand how aggregate initialisation works and how it changed in C++17. Funny how much removing two characters can teach us!

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin