C++ Logo

std-proposals

Advanced search

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

From: Magnus Fromreide <magfr_at_[hidden]>
Date: Sun, 16 Jul 2023 02:41:16 +0200
On Tue, Jul 11, 2023 at 05:34:24PM +0200, Marcin Jaczewski via Std-Proposals wrote:

I just wanted to throw in basic_string::resize_and_overwrite as that also
tries to basically solve this very same problem or at least another aspect
of it.

With this proposal one could write a nonmember resize_and_overwrite as

void resize_and_overwrite(string& s, size_type count, Operation op)
{
  s.resize(count + 1, default_init);
  s.resize(std::move(op)(s.data(), count));
}

/MF

> wt., 11 lip 2023 o 16:36 Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]> napisał(a):
> >
> > 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).
> >
>
> In general case yes, but if `array` is "default aware" it would have
> the same effect.
> First line `f = { default_init };` do not need any changes in `array`
> and could work right after language change,
> similar like in some random struct:
> ```
> {
> default_init,
> 13,
> default_init,
> }
> ```
> we can partially aggregate initialization with some uninitialized members.
>
> >> 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`.
>
> Ok, probaby bit too much for "overload" for explicit meaning.
>
> > - 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
> >
>
> Yes
>
> > 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;
>
> Yes, as `::default_init` can work only with user types but can't with
> builtin ones (at least not easily). As this is a special type, we can
> specify how we will handle all corner cases and even add some
> exceptions for better user experience. Like it will work by default
> for any type, even if it does not explicitly declare use of it.
> Another thing is that compiler can better interact with it as know
> intention and semantic of this new keyword.
>
> > but I don't think it quite gets there yet, does it?
> >
> > –Arthur
>
> Its possible, for now, is more analogous to `nullptr` functionality.
> But I see potential in something like this.
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

-- 
Magnus Fromreide     +46-13 17 68 48
Tornhagsvägen 24, 2tr     magfr_at_[hidden]
SE-582 37  LINKÖPING

Received on 2023-07-16 00:41:20