I don't understand what happens regarding the zero initialization of structs that has default values for its members.
If I have these structs:
struct A {
int *a;
int b;
};
struct B {
int *a;
int b;
B() : b(3) {}
};
struct C {
int *a;
int b = 3;
};
What we can say without a doubt is:
A a; leaves all fields uninitialized
A a{}; is {nullptr, 0}
B b; and B b{}; both are {garbage, 3} (the constructor is called)
Now it's unclear what happens when I do the following, here are the results using gcc:
C c; // {garbage, 3}
C c{}; // {nullptr, 3}
The question is: does C c{}; guarantees that C::a is initialized to nullptr, in other words, does having default members like in C still zero initialize the other members if I explicitly construct the object like C c{};?
Because it's not what happens if I have a constructor that does the same thing than C (like in B), the other members are not zero initialized, but why? What is the difference between B and C?
As of C++14, C is an aggregate (like A), and C c{} syntax performs aggregate initialization. This includes, in part:
[dcl.init.aggr]/8 If there are fewer initializer-clauses in the list than there are elements in a non-union aggregate, then each element not explicitly initialized is initialized as follows:
(8.1) — If the element has a default member initializer (12.2), the element is initialized from that initializer.
(8.2) — Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list (11.6.4).
(8.3) — Otherwise, the program is ill-formed.
So C c{}; is equivalent to C c{{}, 3};. Initializing an int* member with empty list causes it to be zero-initialized.
In C++11, C is not an aggregate (having a default member initializer was disqualifying), and C c{}; calls an implicitly-defined constructor that leaves c.a member uninitialized.
In all versions of the standard, B is not an aggregate due to the user-defined constructor. B b{}; calls that constructor, which explicitly initializes b member and chooses to leave a uninitialized.
Aggregate initialization - cppreference.com
If the number of initializer clauses is less than the number of members or initializer list is completely empty, the remaining members are value-initialized. If a member of a reference type is one of these remaining members, the program is ill-formed.
(until C++11)
If the number of initializer clauses is less than the number of members and bases (since C++17) or initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default member initializers, if provided in the class definition, and otherwise (since C++14) copy-initialized from empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates). If a member of a reference type is one of these remaining members, the program is ill-formed.
(since C++11)
So A a{}; all members are default initialized
Related
I am trying to migrate an old C++03 codebase to C++11. But I fail to understand what gcc is warning me about in the following case:
% g++ -std=c++03 t.cxx
% g++ -std=c++11 t.cxx
t.cxx: In function ‘int main()’:
t.cxx:8:21: warning: converting to ‘A’ from initializer list would use explicit constructor ‘A::A(int)’
8 | int main() { B b = {}; }
| ^
t.cxx:8:21: note: in C++11 and above a default constructor can be explicit
struct A {
explicit A(int i = 42) {}
};
struct B {
A a;
};
int main() {
B b = {};
return 0;
}
All I am trying to do here is a basic zero initialization. It seems to be legal for C++03, but I fail to understand how to express the equivalent in C++11.
For reference, I am using:
% g++ --version
g++ (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
The given program is ill-formed for the reason(s) explained below.
C++20
B is an aggregate. Since you're not explicitly initializing a, dcl.init.aggr#5 applies:
For a non-union aggregate, each element that is not an explicitly initialized element is initialized as follows:
5.2 Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).
This means that a is copy initialized from an empty initializer list. In other word, it is as if we're writing:
A a = {}; // not valid see reason below
Note also that list-initialization in a copy initialization context is called copy-list-initialization which is the case here. And from over.match.list#1.2:
In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
Essentially, the reason for the failure is that A a = {}; is ill-formed.
C++11
Since B is an aggregate and there are fewer initializer-clauses in the list than there are members in the aggregate, and from aggregate initialization documentation:
The effects of aggregate initialization are:
If the number of initializer clauses is less than the number of members and bases (since C++17) or initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default member initializers, if provided in the class definition, and otherwise (since C++14) copy-initialized from empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates).
This again means that a is copy initialized from an empty list {}. That is, it is as if we're writing:
A a = {}; // not valid see reason below
But from over.match.funcs:
In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
So again we face the same problem i.e., A a = {}; is not valid.
Solution
To solve this, we can pass A{} or A{0} as the initializer inside the list as shown below:
B b = { A{} }; //ok now
B c = { A{0} }; //also ok
Working demo.
Note
Note that writing A a{}; on the other hand is well-formed as this is a direct-initialization context and so it is direct-list-initialization.
In C++17, consider a case where S is a struct with a deleted default constructor and a float member, when S is initialized with empty braces, is the float member guaranteed by the standard to be zero-initialized?
struct A {
int x{};
};
struct S
{
S() = delete;
A a;
float b;
};
int main()
{
auto s = S{}; // Is s.b guaranteed to be zero?
}
In my opinion, cppreference.com is not clear, saying both that:
If the number of initializer clauses is less than the number of members and basesor initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default member initializers, if provided in the class definition, and otherwise (since C++14) copy-initialized from empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates). If a member of a reference type is one of these remaining members, the program is ill-formed.
(from here), which implies that b is guaranteed to be zero
In all cases, if the empty pair of braces {} is used and T is an aggregate type, aggregate-initialization is performed instead of value-initialization.
(from here)
which implies that b is not guaranteed to be zero.
There is also a discussion that seems to imply that while not guaranteed, all known compiler zero-initialize anyway:
The standard specifies that zero-initialization is not performed when the class has a user-provided or deleted default constructor, even if that default constructor is not selected by overload resolution. All known compilers performs additional zero-initialization if a non-deleted defaulted default constructor is selected.
Related to Why does aggregate initialization not work anymore since C++20 if a constructor is explicitly defaulted or deleted?
This is a quirk of C++ that is fixed in C++20. In the meantime you can add explicit to the deleted default constructor to force the struct to become non-aggregate, and make your code a guaranteed compile error:
struct A {
int x{};
};
struct S
{
explicit S() = delete;
const A a;
const float b;
};
int main()
{
auto s = S{}; // error: call to deleted constructor of 'S'
}
Because S is an aggregate, S{} will perform aggregate initialization. The rule in the standard about how members are initialized when there are no initializers in the list is basically what you cited:
If the element has a default member initializer ([class.mem]), the element is initialized from that initializer.
Otherwise, if the element is not a reference, the element is copy-initialized from an empty initializer list ([dcl.init.list]).
So for b, that's the equivalent of float b = {};. Per the rules of list initialization, we have to get all the way down to 3.10:
Otherwise, if the initializer list has no elements, the object is value-initialized.
And value initialization will initialize a float to 0.
Please consider the code with aggregate struct B having a field of class A with a private constructor:
class A { A(int){} friend struct B; };
struct B { A a{1}; };
int main()
{
B b; //ok everywhere, not aggregate initialization
//[[maybe_unused]] B x{1}; //error everywhere
[[maybe_unused]] B y{}; //ok in GCC and Clang, error in MSVC
}
My question is about aggregate initialization of B. Since the initialization takes place on behalf of the calling code (main function here), I expected that it must be denied by the compiler, since A's constructor is private. And indeed the construction B{1} fails in all compilers.
But to my surprise the construction B{} is accepted by both GCC and Clang, demo: https://gcc.godbolt.org/z/7851esv6Y
And only MSVC rejects it with the error error C2248: 'A::A': cannot access private member declared in class 'A'.
Is it a bug in GCC and Clang, or the standard permits them to accept this code?
... with aggregate struct B ...
For completeness, let's begin with noting that B is indeed an aggregate in C++14 through C++20, as per [dcl.init.aggr]/1 (N4861 (March 2020 post-Prague working draft/C++20 DIS)):
An aggregate is an array or a class ([class]) with
(1.1) no user-declared or inherited constructors ([class.ctor]),
(1.2) no private or protected direct non-static data members ([class.access]),
(1.3) no virtual functions ([class.virtual]), and
(1.4) no virtual, private, or protected base classes ([class.mi]).
whereas in C++11, B is disqualified as an aggregate due to violating no brace-or-equal-initializers for non-static data members, a requirement that was removed in C++14.
Thus, as per [dcl.init.list]/3 B x{1} and B y{} are both aggregate initialization:
List-initialization of an object or reference of type T is defined
as follows:
[...]
(3.4) Otherwise, if T is an aggregate, aggregate initialization is performed ([dcl.init.aggr]).
For the former case, B x{1}, the data member a of B is an explicitly initialized element of the aggregate, as per [dcl.init.aggr]/3. This means, as per [dcl.init.aggr]/4, particularly /4.2, that the data member is copy-initialized from the initializer-clause, which would require a temporary A object to be constructed in the context of the aggregate initialization, making the program ill-formed, as the matching constructor of A is private.
B x{1}; // needs A::A(int) to create an A temporary
// that in turn will be used to copy-initialize
// the data member a of B.
If we instead use an A object in the initializer-clause, there is no need to access the private constructor of A in the context of the aggregate initialization, and the program is well-formed.
class A {
public:
static A get() { return {42}; }
private:
A(int){}
friend struct B;
};
struct B { A a{1}; };
int main() {
auto a{A::get()};
[[maybe_unused]] B x{a}; // OK
}
For the latter case, B y{}, as per [dcl.init.aggr]/3.3, the data member a of B is no longer an explicitly initialized element of the aggregate, and as per [dcl.init.aggr]/5, particularly /5.1
For a non-union aggregate, each element that is not an explicitly
initialized element is initialized as follows:
(5.1) If the element has a default member initializer ([class.mem]), the element is initialized from that initializer.
[...]
and the data member a of B is initialized from its default member initializer, meaning the private constructor A::A(int) is no longer accessed from a context where it is not accessible.
Finally, the case of the private destructor
If we add private destructor to A then all compilers demonstrate it with the correct error:
is governed by [dcl.init.aggr]/8 [emphasis mine]:
The destructor for each element of class type is potentially invoked ([class.dtor]) from the context where the aggregate initialization occurs. [ Note: This provision ensures that destructors can be called for fully-constructed subobjects in case an exception is thrown ([except.ctor]). — end note ]
In my opinion, the GCC and CLANG are behaving correctly, but the MVSC is not, for the following reasons:
As you already mentioned struct B is an aggregate and so aggregate initialization takes place when using list-initialization for B (list initialization
Since the initializer list is empty default member initialization is used (aggregate initialization
Since B is a friend of A using the private constuctor of A for default member initialization is allowed
Just for the case it was not clear for you (at least it was not clear for me). The line B x{1}; results in a compiler error, because the compiler tries to find a way to convert the integer 1 in the initializer list into an instance of A before performing copy initialization of the member a of B. But there is no way to perform that conversion at that place, since the constructor of A is private. That's the reason why you get that compiler error
I'm debugging some code that essentially is identical to this:
struct Foo { int a; int b; };
struct Bar { Bar() {} Foo foo{0}; };
When I make an instance of Bar, it seems like both a and b are initialized to zero. Is this guaranteed? Where can I find that in the spec?
According to cppreference.com
If the number of initializer clauses is less than the number of members [and bases (since C++17)] or initializer list is completely empty, the remaining members [and bases (since C++17)] are initialized [by their default member initializers, if provided in the class definition, and otherwise (since C++14)] by empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates). If a member of a reference type is one of these remaining members, the program is ill-formed.
Foo has no default member initializers (int b{0};), so b will be initialized by list-initialization with an empty list, which means value-initialization for non-class types: b = int() // = 0.
By the rules of value initialization. Value initialization occurs:
1,5) when a nameless temporary object is created with the initializer
consisting of an empty pair of parentheses or braces (since C++11);
2,6) when an object with dynamic storage duration is created by a
new-expression with the initializer consisting of an empty pair of
parentheses or braces (since C++11);
3,7) when a non-static data
member or a base class is initialized using a member initializer with
an empty pair of parentheses or braces (since C++11);
4) when a named
variable (automatic, static, or thread-local) is declared with the
initializer consisting of a pair of braces.
Trivial example
struct A{
int i;
string s;
A(){};
};
A a{}
cout << a.i << endl // default initialized value
without explicitly declared constructor and left with defaulted default ctor // compiler generated one we get.
struct A{
int i;
string s;
};
A a{};
cout << a.i << endl // zero-initialized value
However using antoher struct.
struct A{
int i;
string s;
};
struct B{
A a;
int c;
};
B a{};
cout << a.a.i << endl // default initialized , even tho we did not , int struct B , declared A a{}.
The value of a.i is zero-initialized, even without using {} / () construct, which goes against rules ( if i am not mistaken ).
Using same logic on struct B:
struct A{
int i;
string s;
};
struct B{
A a;
int c;
};
B b;
cout << b.c << endl; // default inicialized
We get behavior according to rules.
The last example:
struct A
{
int i;
A() { }
};
struct B { A a; };
std::cout << B().a.i << endl;
B().a.i is also zero-initialized while we explicitly declared constructor and its not deleted.
Why are the these values getting zero-initialized? By rules stated here they should be default-initialized not zero-initialized.
Thanks for answers.
It's the difference between A being an aggregate or not.
[dcl.init.aggr] (Emphasis mine)
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),
So when A has no declared constructors, saying A a{} has the effect of aggregate initialization
which will construct each member with an empty initialization list:
If there are fewer initializer-clauses in the list than there are members in the aggregate, then each member
not explicitly initialized shall be initialized from an empty initializer list
So you get int{} and std::string{} which will value-initialize the integer member to zero.
When you do provide a default constructor, the aggregate property is lost and the int member remains uninitialized, so accessing it is considered undefined behavior.
To be specific:
This code is undefined behavior when accessing a.i because you provided a user-defined constructor, so the int i field remains uninitialized after construction:
struct A{
int i;
string s;
A(){};
};
A a{} ;
cout << a.i << endl;
And this code exhibits undefined behavior when accessing b.c because you did not perform list initialization on B b:
struct B{
A a;
int c;
};
B b;
cout << b.c << endl;
All the other code is okay, and will zero-initialize the integer fields. In the scenarios where you are using braces {}, you are performing aggregate-initialization.
The last example is a little tricky because you're performing value-initialization. Since B is an aggregate, it gets zero-initialized ([dcl.init]), in which:
each base-class
subobject is zero-initialized
So again you're okay accessing the integer member of the A subobject.
According to the rules of aggregate initialization, the members are indeed value initialized.
Value initialization:
In all cases, if the empty pair of braces {} is used and T is an aggregate type, aggregate-initialization is performed instead of value-initialization.
The effects of aggregate initialization are:
If the number of initializer clauses is less than the number of members and bases (since C++17) or initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default initializers, if provided in the class definition, and otherwise (since C++14) by empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates). If a member of a reference type is one of these remaining members, the program is ill-formed.