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 ???
}
missing field 'k' initializer
. If it weren't, this would be quite blaspheme. – RapperShapeWithCenter
then it would largely (but not fully) be obvious what is happening. Now, I agree that the difference between brace initialization and the ostensibly intended constructor is still dangerous, but that is not new. – TismanD
andE
adds no new members toB
. I could have usedB
everywhere, or I could have usedusing D = B
andusing E = B
. If I did that then yes, it should be possible to initialize anE
from aD
. But I do NOT want this to be possible, so I am using the type system to create separate types. Before C++17 this was a good way of caching mistakes. But now it is not. – PlasticizerManager
andAssistant
fromEmployee
. Same thing as my example. So if I am using the language wrong, at least I am in good company. I just don't think it should be possible to silently convert anAssistant
into aManager
. – PlasticizerManager
andAssistant
have an "is a" relationship toEmployee
than it does forcircle
andrectangle
withpoint
. Circles have points but they are not points; you cannot use them interchangeably with points. Whereas a manager cannot be a manager without also being an employee. I would also wager that none of Stroustrup's types are aggregates, so that wouldn't matter. – KristiankristiansandD d = e;
. That's not allowed. What you're doing is initializing an object; that's what a braced-init-list is for.D d = {e};
is not supposed to be the same thing asD d = e;
. – KristiankristiansandE f();
, I save the return value in a variable usingE e{ f() };
and notE e = f();
. But as I illustrated, as of C++17, I can now writeD d{ f() }
, and the compiler will silently accept what is almost certainly an error. The same goes forstruct F { F(D d) : e{d} {} E e; };
I think most people will be very surprised that that now compiles. – Plasticizer