Why is a member not getting zero-initialized in this example?
Asked Answered
A

3

22

This is specifically regarding C++11:

#include <iostream>
struct A {
    A(){}
    int i;
};
struct B : public A {
    int j;
};
int main() {
    B b = {};
    std::cout << b.i << b.j << std::endl;
}

Compiling with g++ 8.2.1:

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:25:25: warning: ‘b.B::<anonymous>.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << b.i << " " << b.j << std::endl

gcc is detecting b.i as uninitialized, but I would think it should be getting zero-initialized along with b.j.

What I believe is happening (C++11 specifically, from the ISO/IEC working draft N3337, emphasis mine):

  • B is not an aggregate because it has a base class. Public base classes were only allowed in aggregates in C++17.
  • A is not an aggregate because it has a user-provided constructor

Section 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).

  • b is getting list initialized with an empty braced-init-list

Section 8.5.4

List-initialization of an object or reference of type T is defined as follows:
If the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.
— Otherwise, if T is an aggregate, aggregate initialization is performed (8.5.1).

  • This means b gets value-initialized
  • B has an implicitly-defined default constructor, so b value-initialization invokes zero-initialization
  • b.B::A gets zero-initialized, which zero-initalizes b.B::A.i, and then b.B::j gets zero-initialized.

Section 8.5

To zero-initialize an object or reference of type T means:
...
— if T is a (possibly cv-qualified) non-union class type, each non-static data member and each base-class subobject is zero-initialized and padding is initialized to zero bits;

...

To value-initialize an object of type T means:
— if T is a (possibly cv-qualified) class type (Clause 9) with a user-provided constructor (12.1), then the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
if T is a (possibly cv-qualified) non-union class type without a user-provided constructor, then the object is zero-initialized and, if T’s implicitly-declared default constructor is non-trivial, that constructor is called.

However, it looks like gcc is saying only b.B::j is going to get zero-initialized. Why is this?

One reason I can think of is if B is being treated as an aggregate, which would initialize b.B::A with an empty list. B is certainly not an aggregate, though, because gcc rightly errors if we try to use aggregate initialization.

// ... as in the above example
int main() {
    B b = {A{}, 1};
    std::cout << b.i << " " << b.j << std::endl;
}

Compiling with C++11

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:10:18: error: could not convert ‘{A(), 1}’ from ‘<brace-enclosed initializer list>’ to ‘B’
     B b = {A{}, 1};

Compiling with C++17

g++ -std=c++17 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:11:25: warning: ‘b.B::<anonymous>.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << b.i << " " << b.j << std::endl;

And we can see that b.i is uninitialized because B is an aggregate, and b.B::A is getting initialized by an expression that itself leaves A::i uninitialized.

So it's not an aggregate. Another reason is if b.B::j is getting zero-initialized, and b.B::A is getting value-initialized, but I don't see that anywhere in the specs.

The last reason is if an older version of the standard was getting invoked. From cppreference:

2) if T is a non-union class type without any user-provided constructors, every non-static data member and base-class component of T is value-initialized; (until C++11)

In this case, both b.B::i and b.B::A would be value-initialized, which would cause this behavior, but that is marked as "(until C++11)".

Assurbanipal answered 3/1, 2019 at 19:47 Comment(5)
You defined a constructor and the compiler is using it. A() = default; Or just don't define it at all since you don't have any user-defined types members.Heathenish
@Heathenish b is getting zero-initialized. This should zero-initialize all bases and non-static members. Unless it's not getting zero-initialized...Assurbanipal
Problem is that the compiler is not "zero-initializing" the bases, it is actually calling the default constructor of it. Since you defined a constructor for A, B's constructor will call that A constructor you defined. You just have to simply remove A(){} or change it to A() = default;Heathenish
@MikeLui If you want an answer that has references from the standard to back them up you should add the language-lawyer tag.Morsel
Compiler bug. (But it is probably unnecessarily brittle to rely on this anyway.)Giles
W
6

I'd also go with compiler bug.

  • I think we can all agree that b gets value-initialized (8.5.4)
  • Using

    value-initialize an object of type T means:
    — if T is a (possibly cv-qualified) non-union class type without a user-provided constructor, then the object is zero-initialized and, if T’s implicitly-declared default constructor is non-trivial, that constructor is called.

    So what should happen is first zero-initialization, then default ctors may be called

  • And with the definition:

    To zero-initialize an object or reference of type T means:
    — if T is a (possibly cv-qualified) non-union class type, each non-static data member and each base-class subobject is zero-initialized and padding is initialized to zero bits;

Hence the following should happen:

  1. Fill sizeof(B) with zeroes
  2. Call constructor of subobject A which does nothing.

I assume this is a bug in the optimization. Compare the output of -O0 to -O1: https://godbolt.org/z/20QBoR. Without optimization the behaviour is correct. Clang on the other hand is correct in both: https://godbolt.org/z/7uhlIi

This "bug" is still present with newer standard flags in GCC: https://godbolt.org/z/ivkE5K

However I assume in C++20 B is an "aggregate" so the behavior becomes standard.

Winou answered 7/1, 2019 at 13:35 Comment(0)
C
8

For any class, if there is a single user-defined constructor it must be used, and A(){} does not initialise i.

Charily answered 3/1, 2019 at 19:51 Comment(3)
There's always a constructor. Depending on how it is defined (user-provided or not), the object may get zero-initialized beforehand. I believe in this situation there should be zero-initialization. The presence of a constructor in a base class does not preclude zero-initialization, as I have seen it defined in the standard. C++11 working draft 8.5: "To zero-initialize an object or reference of type T means: ... if T is a (possibly cv-qualified) non-union class type, each non-static data member and each base-class subobject is zero-initialized and padding is initialized to zero bits;"Assurbanipal
The section on zero initialization seems to disagree with this.Morsel
Added "user-defined".Charily
W
6

I'd also go with compiler bug.

  • I think we can all agree that b gets value-initialized (8.5.4)
  • Using

    value-initialize an object of type T means:
    — if T is a (possibly cv-qualified) non-union class type without a user-provided constructor, then the object is zero-initialized and, if T’s implicitly-declared default constructor is non-trivial, that constructor is called.

    So what should happen is first zero-initialization, then default ctors may be called

  • And with the definition:

    To zero-initialize an object or reference of type T means:
    — if T is a (possibly cv-qualified) non-union class type, each non-static data member and each base-class subobject is zero-initialized and padding is initialized to zero bits;

Hence the following should happen:

  1. Fill sizeof(B) with zeroes
  2. Call constructor of subobject A which does nothing.

I assume this is a bug in the optimization. Compare the output of -O0 to -O1: https://godbolt.org/z/20QBoR. Without optimization the behaviour is correct. Clang on the other hand is correct in both: https://godbolt.org/z/7uhlIi

This "bug" is still present with newer standard flags in GCC: https://godbolt.org/z/ivkE5K

However I assume in C++20 B is an "aggregate" so the behavior becomes standard.

Winou answered 7/1, 2019 at 13:35 Comment(0)
P
-1

Nothing is initializing i. It doesn't happen automatically. You need to either initialize it in-class or in the initialization list of the class constructor. Or remove your non-trivial/user-defined constructor (or = default it, which makes it trivial).

The compiler is using the constructor you provided and that ctor does not initialize i.

Parasitism answered 3/1, 2019 at 19:51 Comment(1)
Or remove the user-defined default constructor.Theodore

© 2022 - 2024 — McMap. All rights reserved.