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 10:36:16 -0400
On Tue, Jul 11, 2023 at 9:22 AM Marcin Jaczewski <
marcinjaczewski86_at_[hidden]> wrote:

> wt., 11 lip 2023 o 14:52 Arthur O'Dwyer via Std-Proposals <
> std-proposals_at_[hidden]> napisał(a):
> >
> > 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.
>
> And this have another problem that `int i;` can be source of UB if was
> done unintentionally, for me we could fix both problems by introducing
> new keyword similar to `nullptr`,
> like `default_init` or something like this, then we could make:
>
> class F
> {
> int i;
> F(std::default_init_t) = default; //make default init for `i`
> F() = default; // value init for `i`
> };
>
> int main()
> {
> int i[10] = default_init; // same as `int i[10];`
> std::array<F, 10> f = { default_init }; // or `std::array<F, 10> f
> = default_init;`
>

Hmm, normally aggregate-initialization zero-fills the unspecified values. So
    int a[10] = default_init;
would mean "garbage-fill the entire array object," but
    int a[10] = {default_init};
would mean "garbage-fill a[0], and then zero-fill a[1] through a[9]."
Right? This is fine, but awfully subtle.
Likewise,
    std::array<F, 10> f = { default_init };
    std::array<F, 10> f = default_init;
would presumably do *different* things, not the same thing (as you implied).

Interesting thing will be when we do something like:
>
> class SafeInt {
> int i;
> explicit SafeInt(std::default_init_t) = default;
> };
> int main() {
> SafeInt i; //error, it uses explicit constructor!
> SafeInt j = SafeInt(default_init); //ok, no initialization is done
> };
>

This syntax seems suboptimal to me. I observe:
- `SafeInt i;` is not an implicit conversion; it's perfectly well allowed
to use an `explicit` constructor. So `explicit` isn't the keyword you mean,
here. What `explicit` disallows is `SafeInt i = {};` — the implicit
[scare-quotes] "conversion" of `{}` to an expression of type `SafeInt`.
- Your use-case, IIUC, wants to forbid `SafeInt i;` but explicitly permit
`SafeInt j = SafeInt(default_init)`. Presumably you'd also like to permit
`SafeInt j = default_init`, since it's equally explicit about what's
happening there. This smells more like a case for a *tag type* than
anything specifically related to default-initialization. Like, today, we
could say

    struct default_init_t { explicit default_init_t() = default; };
    inline constexpr default_init_t default_init;

    class SafeInt {
    public:
        SafeInt(default_init_t) {}
        SafeInt(int i) : i_(i) {}
    private:
        explicit SafeInt() = default;
        int i_;
    };
    SafeInt si1; // ill-formed
    SafeInt si2 = 0; // OK
    SafeInt si3 = default_init; // OK, i_ is garbage
    SafeInt si4 = SafeInt(default_init); // OK, i_ is garbage
    SafeInt si5 = SafeInt(); // ill-formed (!!)
    void f6(SafeInt); ... f6(default_init);

The two problems I see with today's solution are:
- si5 is ill-formed
- We would like si3 to be well-formed but f6 to be ill-formed; we can't get
that without a core-language change, since both are syntactically implicit
conversions

Your idea, to make `default_init` a keyword with semantics different from
the above `::default_init`, clearly has the potential to help with one or
both of these problems; but I don't think it quite gets there yet, does it?

–Arthur

Received on 2023-07-11 14:36:29