C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Impact of defaulted ctor on value initialization

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Tue, 11 Jul 2023 08:51:41 -0400
On Tue, Jul 11, 2023 at 7:18 AM Jonathan Wakely via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> On Tue, 11 Jul 2023 at 12:11, Jonathan Wakely <cxx_at_[hidden]> wrote:
>
>> On Tue, 11 Jul 2023 at 11:17, Ofek Shilon <ofekshilon_at_[hidden]> wrote:
>>
>>> On Tue, 11 Jul 2023 at 12:53, Jonathan Wakely <cxx_at_[hidden]> wrote:
>>>
>>>>
>>>> Why should Thingy3() = default; give you that behaviour? It's still
>>>> trivial and it is noexcept. Why should it prevent Thingy3() from zeroing
>>>> the members?
>>>>
>>> The phrasing of this question assumes zeroing the members is some
>>> natural default and deviating from it requires justification. I believe in
>>> C++ it's the other way around: you don't pay for what you don't use, so me
>>> & quite a few people I talked to would expect `= default` to consistently
>>> not initialize the members, in all initialization forms.
>>>
>>
>> But value-initialization zeros things, just like:
>> int i = int();
>>
>> If you don't want that, don't use value-init:
>> int i;
>> Thingy1 t1;
>>
> [...]
>
Your suggestion would **remove** functionality from the language. It would
>> no longer be possible to have a class with a user-declared copy/move/other
>> constructor and a trivial default constructor, so you would have to pay for
>> functionality (non-trivial default constructor) that you don't want.
>>
>
While I agree that this is a really tricky area and any modifications in
this area should be *assumed to be very difficult* and probably not worth
the cost... I also have a different viewpoint from Jonathan's here.

The root problem here IMHO is that C++ has two core *ideas*
("default-initialization" and "value-initialization") which are
distinguishable in the Platonic realm; but the *syntax* corresponding to
these ideas is all conflated together. Specifically two flaws:

(1) Some primitive types (like `int`) make default-initialization behave
differently from value-initialization. It is possible to emulate this
behavior in a user-defined class type, but only under very specific
restrictions that correspond to leaving specific fields uninitialized at
the machine level. So for example if I wanted to define a `class
TrappingInt` such that
    auto i1 = int(); // value-initialized to zero
    auto i2 = TrappingInt(); // value-initialized to zero
    int i1; // default-initialized to garbage
    TrappingInt i2; // default-initialized to *a trapping value*
well, there's no way to achieve that in today's C++. This is because the
syntax for defining a "default constructor" really does double duty: it
defines the default constructor and the, let's say, "value constructor,"
simultaneously, in an implicit and entangled way. This is very convenient
for most programmers, but specifically inconvenient for the rare programmer
who wants to emulate `int` as shown above. It means that there is behavior
of the primitive types that can't be emulated by user-defined types.

(2) Suppose I want to construct a prvalue of a given type, using its
{default,value} constructor. There is no core-language syntax for this at
all.
    auto k1 = int(); // OK, value-initialize an int
    auto k2 = ???; // inexpressible: default-initialize an int
IIRC, we recently discussed this problem w.r.t. -ftrivial-var-init=zero and
prvalues of types like `std::array`: C++'s syntax lets us write
    void compute(std::array<int, 1000> scratch_space) { ... immediately
overwrite whatever's in scratch_space ... }
    void test() { compute(std::array<int, 1000>()); }
but that wastefully zeroes out 4000 bytes of memory before calling
`compute`. What we actually wanted to express here was "pass a
*default-initialized* std::array to `compute`," but C++ doesn't give us any
syntax to express that.

This deficiency-of-syntax leads to things like `S2` at the end of this blog
post:
https://quuxplusone.github.io/blog/2023/06/22/psa-value-initialization/
  struct S2 {
    explicit S2() {}
    int T::*mf;
  };
`S2` has a user-defined default constructor, so even though it is in the
Platonic[*] sense "trivially default-constructible," the compiler cannot
prove that it is.
We could =default its default constructor; but if we do that, then it
becomes in the Platonic sense "*non*-trivially *value* initializable,"
which is worse (as we can see from the codegen of `f2` in the blog post).
We'd like a way to express to the compiler that a type like `S2` is
trivially default-constructible *and* trivially value-initializable, so
that we get the right answer from `is_trivially_default_constructible_v`
*and* good codegen for `return S2()`. Right now, it's "pick one or the
other." Because there's a single-dimensional control knob (the
user-declared default constructor) controlling two different dimensions
(default-initialization behavior *and* value-initialization behavior).

Could we "fix" this by adding core-language syntax for a
"value-initialization constructor"? Maybe, but I'll repeat: this area is
very tricky and your default assumption should be that any change here will
be *very very difficult* and almost certainly not worth the cost. Please
don't assume that anything in this area will admit a "simple fix"; because
then you'll just be wrong. ;)

(I owe a blog post on the general topic of the gap between what the
front-end can prove based on syntax, and what the optimizer is allowed to
generate based on deeper Platonic ideas. The gap is created by the
impedance mismatch between ideas and syntax; we can often close the gap by
adding attributes such as P1144's [[trivially_relocatable]], Clang's
proposed [[equality_is_memberwise]] <https://reviews.llvm.org/D151625>, and
so on.)

–Arthur

[* — When I say a type is trivially fooable in the Platonic sense, I mean
that a sufficiently smart optimizer could in theory replace the type's foo
operation with fooing the individual bytes of its object representation,
without loss of correctness. When I say the compiler can *prove* that it's
trivially fooable, I mean pragmatically, in the front-end, such that
std::is_trivially_fooable_v<T> will evaluate to true at compile time.]

Received on 2023-07-11 12:51:54