Has the C++17 extension to aggregate initialization made brace initialization dangerous?

There seems to be a general consensus that brace initialization should be preferred over other forms of initialization, however since the introduction of the C++17 extension to aggregate initialization there seems to be a risk of unintended conversions. Consider the following code:

struct B { int i; };
struct D : B { char j; };
struct E : B { float k; };

void f( const D& d )
{
  E e1 = d;   // error C2440: 'initializing': cannot convert from 'D' to 'E'
  E e2( d );  // error C2440: 'initializing': cannot convert from 'D' to 'E'
  E e3{ d };  // OK in C++17 ???
}

struct F
{
  F( D d ) : e{ d } {}  // OK in C++17 ???
  E e;
};

In the code above struct D and struct E represent two completely unrelated types. So it is a surprise to me that as of C++17 you can "convert" from one type to another type without any warning if you use brace (aggregate) initialization.

What would you recommend to avoid these types of accidental conversions? Or am I missing something?

PS: The code above was tested in Clang, GCC and the latest VC++ - they are all the same.

Update: In response to the answer from Nicol. Consider a more practical example:

struct point { int x; int y; };
struct circle : point { int r; };
struct rectangle : point { int sx; int sy; };

void move( point& p );

void f( circle c )
{
  move( c ); // OK, makes sense
  rectangle r1( c );  // Error, as it should be
  rectangle r2{ c };  // OK ???
}

I can understand that you can view a circle as a point, because circle has point as base class, but the idea that you can silently convert from a circle to a rectangle, that to me is a problem.

Update 2: Because my poor choice of class name seems to be clouding the issue for some.

struct shape { int x; int y; };
struct circle : shape { int r; };
struct rectangle : shape { int sx; int sy; };

void move( shape& p );

void f( circle c )
{
  move( c ); // OK, makes sense
  rectangle r1( c );  // Error, as it should be
  rectangle r2{ c };  // OK ???
}

Solution 1:

struct D and struct E represent two completely unrelated types.

But they're not "completely unrelated" types. They both have the same base class type. This means that every D is implicitly convertible to a B. And therefore every D is a B. So doing E e{d}; is no different from E e{b}; in terms of the invoked operation.

You cannot turn off implicit conversion to base classes.

If this truly bothers you, the only solution is to prevent aggregate initialization by providing an appropriate constructor(s) that forwards the values to the members.

As for whether this makes aggregate initialization more dangerous, I don't think so. You could reproduce the above circumstances with these structs:

struct B { int i; };
struct D { B b; char j; operator B() {return b;} };
struct E { B b; float k; };

So something of this nature was always a possibility. I don't think that using implicit base class conversion makes it that much "worse".

A deeper question is why a user tried to initialize an E with a D to begin with.

the idea that you can silently convert from a circle to a rectangle, that to me is a problem.

You would have the same problem if you did this:

struct rectangle
{
  rectangle(point p);

  int sx; int sy;
  point p;
};

You can not only perform rectangle r{c}; but rectangle r(c).

Your problem is that you're using inheritance incorrectly. You're saying things about the relationship between circle, rectangle and point which you don't mean. And therefore, the compiler lets you do things you didn't mean to do.

If you had used containment instead of inheritance, this wouldn't be a problem:

struct point { int x; int y; };
struct circle { point center; int r; };
struct rectangle { point top_left; int sx; int sy; };

void move( point& p );

void f( circle c )
{
  move( c ); // Error, as it should, since a circle is not a point.
  rectangle r1( c );  // Error, as it should be
  rectangle r2{ c };  // Error, as it should be.
}

Either circle is always a point, or it is never a point. You're trying to make it a point sometimes and not others. That's logically incoherent. If you create logically incoherent types, then you can write logically incoherent code.


the idea that you can silently convert from a circle to a rectangle, that to me is a problem.

This brings up an important point. Conversion, strictly speaking, looks like this:

circle cr = ...
rectangle rect = cr;

That is ill-formed. When you do rectangle rect = {cr};, you're not doing a conversion. You are explicitly invoking list-initialization, which for an aggregate will usually provoke aggregate initialization.

Now, list-initialization certainly can perform a conversion. But given merely D d = {e};, one should not expect that this means you're performing conversion from an e to a D. You're list-initializing an object of type D with an e. That can perform conversion if E is convertible to D, but this initialization can still be valid if non-conversion list-initialization forms can work too.

So it is incorrect to say that this feature makes circle convertible to rectangle.

Solution 2:

This isn't new in C++17. Aggregate initialization always allowed you to leave off members (which would be initialized from an empty initializer list, C++11):

struct X {
    int i, j;
};

X x{42}; // ok in C++11

It's just now that there's more kinds of things that could be left off, since there's more kinds of things that can be included.


gcc and clang at least will provide a warning by way of -Wmissing-field-initializers (it's part of -Wextra) that will indicate that something is missing. If this is a huge concern, just compile with that warning enabled (and, possibly, upgraded to an error):

<source>: In function 'void f(const D&)':
<source>:9:11: warning: missing initializer for member 'E::k' [-Wmissing-field-initializers]
   E e3{ d };  // OK in C++17 ???
           ^
<source>: In constructor 'F::F(D)':
<source>:14:19: warning: missing initializer for member 'E::k' [-Wmissing-field-initializers]
   F( D d ) : e{ d } {}  // OK in C++17 ???
                   ^

More direct would be just to add a constructor to these types so that they cease to be aggregates. You don't have to use aggregate initialization, after all.