Why How to Not Brace Initialize a Struct Derived from Another Struct

Why can I not brace initialize a struct derived from another struct?

Answer for C++ standard versions before C++17:

Your problem has to do with aggregate initialization: struct X is an aggregate while struct Y is not. Here is the standard quote about aggregates (8.5.1):

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equal-initializers for non-static data members (9.2), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

This clause specifies that if a class has a base class, then it's not an aggregate. Here, struct Y has struct X as a base class and thus cannot be an aggregate type.

Concerning the particular problem you have, take the following clause from the standard:

When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list are taken as initializers for the members of the aggregate, in increasing subscript or member order. Each member is copy-initialized from the corresponding initializer-clause. If the initializer-clause is an expression and a narrowing conversion (8.5.4) is required to convert the expression, the program is ill-formed.

When you do X x = {0}, aggregate initialization is used to initialize a to 0. However, when you do Y y = {0}, since struct Y is not an aggregate type, the compiler will look for an appropriate constructor. Since none of the implicitely generated constructors (default, copy and move) can do anything with a single integer, the compiler rejects your code.


Concerning this constructors lookup, the error messages from clang++ are a little bit more explicit about what the compiler is actually trying to do (online example):

Y Y = {0};
^ ~~~

main.cpp:5:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'int' to 'const Y &' for 1st argument

struct Y : public X {};
^

main.cpp:5:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'int' to 'Y &&' for 1st argument

struct Y : public X {};
^

main.cpp:5:8: note: candidate constructor (the implicit default constructor) not viable: requires 0 arguments, but 1 was provided

Note that there is a proposal to extend aggregate initialization to support your use case, and it made it into C++17. If I read it correctly, it makes your example valid with the semantics you expect. So... you only have to wait for a C++17-compliant compiler.

Struct initialization of derived struct with a templated base type

You can only do aggregate initialization on aggregates. An aggregate is, from [dcl.init.aggr]:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or
protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

MoveEvent is not an aggregate. Thus, you will have to add a constructor:

template <class Derived>
struct Event {
};

struct MoveEvent: Event<MoveEvent> {
MoveEvent(int x, int y) : x(x), y(y) { }
int x, y;
};

int main() {
MoveEvent event{5, 4}; // NOW this is fine
}

Can't initialize a struct using an initializer list if it inherits?

It works for C because it is an aggregate and therefore it is using aggregate initialization but D is not an aggregate because it has a base class. The obvious work-around as you mention is to write a constructor.

This is covered in the draft C++ standard section 8.5.1 Aggregates [dcl.init.aggr] with emphasis mine:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or
protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

There is a proposal: Extension to aggregate initialization to remove that restriction. As chris points out this was accepted by Evolution Working Group but as far as I understand now needs to accepted by Core as well.

Using initializer list for a struct with inheritance

With inheritance, each base class subobject of the aggregate is initialized like a member. So to aggregate initialization, Child has two subobjects: Parent, and i. So you need two initializers in your braced-init-list:

Child c{ {}, 1 };

Also, in order for Child to be an aggregate, all subobjects must be public. So you can't have private base classes.

Of course, this assumes that Visual Studio implements the feature correctly. VS2017 15.5 is not C++17 compliant, but 15.7 supports this.

initializer list with struct and inheritance

Your code would work with C++17. Since C++17 both L and Y are considered as aggregate type:

no virtual, private, or protected (since C++17) base classes

Then brace elision is allowed; i.e. the braces around nested initializer lists for subaggregate could be elided and then you can just write L l {1, 2}; and Y y {1, 2}; (instead of L l {{1, 2}}; and Y y {{1}, 2};).

If the aggregate initialization uses copy- (until C++14)list-initialization syntax (T a = {args..} or T a {args..} (since C++14)), the braces around the nested initializer lists may be elided (omitted), in which case as many initializer clauses as necessary are used to initialize every member or element of the corresponding subaggregate, and the subsequent initializer clauses are used to initialize the following members of the object.

LIVE

Before C++17 it doesn't work because list initialization is performed instead, the appropriate constructors are tried to be used and fails.

Why is brace-initialization not needed with array of struct?

6.7.9 Initialization/20 states how such struct elements are initialized:

[..] If the initializer of a subaggregate or contained union begins
with a left brace, the initializers enclosed by that brace and its
matching right brace initialize the elements or members of the
subaggregate or the contained union. Otherwise, only enough
initializers from the list are taken to account for the elements or
members of the subaggregate or the first member of the contained
union; any remaining initializers are left to initialize the next
element or member of the aggregate of which the current subaggregate
or contained union is a part.

(emphasis mine)

So it's valid. And thus

ar[] = {
1,"asd",
2, "qwe",
3, "poi"
};

is equivalent to:

 ar[] = {
{1,"asd"},
{2, "qwe"},
{3, "poi"}
};

and ar contains 3 elements.

Brace Initialize struct with virtual functions

There isn't a way. like in this text you mentioned

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

this means that a class/struct or an array that has

  • no user-provided constructors
  • no private or protected non-static data members
  • no base classes
  • and no virtual functions

is an aggregate. and only an aggregate can have brace initialization. so in this case, your struct has a virtual function that violates one of the laws above and makes it a non-aggregate.

I don't know why this is the case.

I guess that if your struct is similar to the struct in c then your struct would work.

so like Hassan's answer, you should use a parameterized constructor instead.

Initializer list for struct deriving from class (C++)

It does not work because Derived is not an aggregate since it has a base class with virtual members.

The code compiles and runs when removing the virtual destructor in Base and using C++17.

If Base requires a virtual destructor, Derived can implement a custom constructor allowing initialization using curly brackets.

class Base {
public:
virtual ~Base() = default;
};

struct Derived : public Base {
Derived(int m): m1(m) {}
int m1;
};

int main() {
Derived d{ 1 };
return 0;
}

Why can an aggreggate struct be brace-initialized, but not emplaced using the same list of arguments as in the brace initialization?

Is this an oversight in the Standard?

It is considered a defect in the standard, tracked as LWG #2089, which was resolved by C++20. There, constructor syntax can perform aggregate initialization on an aggregate type, so long as the expressions provided wouldn't have called the copy/move/default constructors. Since all forms of indirect initialization (push_back, in_place, make_*, etc) uses constructor syntax explicitly, they can now initialize aggregates.

Pre-C++20, a good solution to it was elusive.

The fundamental problem comes from the fact that you cannot just use braced-init-lists willy-nilly. List initialization of types with constructors can actually hide constructors, such that certain constructors can be impossible to call through list initialization. This is the vector<int> v{1, 2}; problem. That creates a 2-element vector, not a 1-element vector whose only element is 2.

Because of this, you cannot use list initialization in generic contexts like allocator::construct.

Which brings us to:

I would think there's be a SFINAE trick to do that if possible, else resort to brace init that also works for aggregates.

That would require using the is_aggregate type trait from C++17. But there's a problem with that: you would then have to propagate this SFINAE trick into all of the places where indirect initialization is used. This includes any/variant/optional's in_place constructors and emplacements, make_shared/unique calls, and so forth, none of which use allocator::construct.

And that doesn't count user code where such indirect initialization is needed. If users don't do the same initialization that the C++ standard library does, people will be upset.

This is a sticky problem to solve in a way that doesn't bifurcate indirect initialization APIs into groups that allow aggregates and groups that don't. There are many possible solutions, and none of them are ideal.

The language solution is the best of the bunch.



Related Topics



Leave a reply



Submit