'Why is value-initialization specified as not calling trivial default constructors?

To value-initialize an object of type T means:

...

— if T is a (possibly cv-qualified) class type without a user-provided or deleted default constructor, then the object is zero-initialized and the semantic constraints for default-initialization are checked, and if T has a non-trivial default constructor, the object is default-initialized;

I understand the intent here: if the user has either not declared the default constructor for T, or has explicitly defaulted it on its first declaration, then the zero-initialization pass of value-initialization will ensure that certain direct members of the object (such as those of fundamental type) are not left with indeterminate values.

What I don't understand is why the second pass was specified as "the semantic constraints for default-initialization are checked, and if T has a non-trivial default constructor, the object is default-initialized". To me, this is the same as just saying "the object is default-initialized" (regardless of whether the default constructor is trivial or not). If the constructor is actually trivial, calling it should be the same as not calling it. The standard shouldn't need to tell the compiler not to generate the call, since such an optimization would be allowed under the as-if rule, and any good compiler would do it.

Am I missing something? In the value-initialization context, could calling a trivial default constructor ever be different from not calling it?



Solution 1:[1]

I haven't been able to track down an authoritative answer to my question. However, looking at the C++03 standard and the CWG issues list, I have a rough idea.

Let's review value-initialization in C++03. It is defined for a type T as follows:

  • If T is a class type with a user-declared constructor, then the default constructor is called;
  • If T is a non-union class type without a user-declared constructor [which implies that the compiler implicitly declares a default constructor], then each base class subobject and member subobject is value-initialized;
  • [...]

The second bullet point ensures that direct subobjects of scalar type are zero-initialized and is thus "stronger" than simply calling the implicit default constructor. This was the intent behind value-initialization.

So the wording in C++03 makes sense and (without needing to make any special exception) leads to the following result: when the default constructor is trivial, no function calls actually occur.

In C++11, the definition of zero-initialization was changed so that it would also initialize padding bits to zero (CWG 694). My guess is that the wording around value-initialization was changed in order to guarantee that value-initialization would also be guaranteed to zero out padding bits in the case of a trivial default constructor. So in C++11, when T (a type without a user-provided default constructor) is value-initialized, what happens is stronger than just value-initializing all direct subobjects of T. Rather, the entire T object is zero-initialized first (to ensure padding is zeroed out) and then the default constructor is called.

But again, our question is why C++11 carves out a special exception in the case where the default constructor is trivial, preventing it from being called. The paper, N2762, that made the wording change did not explain why that exception was put in, but we can now see that it preserves the C++03 behaviour wherein a trivial default constructor was not called at all. My guess is that the authors put in the wording intentionally to preserve this behaviour, but their motivations are unclear.

One possible motivation is that trivial default constructors were, in general, not constexpr in C++11, except in the case of empty classes: they would leave scalar members uninitialized, which was not allowed. Therefore, omitting the call to the default constructor during value-initialization makes it possible to possible to value-initialize objects that are trivially default constructible, which is desirable to allow; see CWG 644. However, there is no indication of whether the authors of N2762 intended to allow this. (Aside: for subsequent developments related to CWG 644, see CWG 1452.) Note that in C++20, such trivial default constructors became constexpr. See relevant SO question.

I find it more likely that the authors of N2762 were simply being cautious: in other words, they probably wanted to preserve the C++03 behaviour of not calling the trivial default constructor just in case changing this would cause problems they didn't anticipate. (It probably doesn't have anything to do with performance, though; the compiler can optimize out a call to a trivial default constructor.)

Still, we should observe that the C++11 behaviour isn't quite the same as the C++03 behaviour plus zero-initialization of padding. Let's say we have a type like this:

struct T {
    struct U { U() {} } u;
    int x;
};

In C++03, value-initialization of T means that U::U is called and then x is zero-initialized. In C++11, it means that the entire T object is zero-initialized and then T::T is called, which in turn calls U::U. So C++11 has an extra function call here compared to C++03. So despite the authors' best efforts, the behaviour is not the same. As far as I can tell though, this difference doesn't break any code.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1