Date: Sat, 26 Feb 2022 12:05:41 -0500
On Sat, Feb 26, 2022 at 10:43 AM organicoman via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
> I see the problem now.
> There is a big misunderstanding of when an object destructor is invoked, and what is its relationship with the constructors.
>
> To be saved by heart:
> "A destructor can be called only when the object is successfully constructed"
No, we understand how the language works just fine. The problems are
manifold-fold:
1: Objects *should be* destroyed in the order opposite of their
initialization. As Arvid has pointed out, this is a *good thing* that
should not be discarded on a whim.
2: Let's say we discard #1 in a sane, reasonable way. We allow
constructors to construct subobjects in arbitrary orders, but the
destructor will always use reverse-declaration order. There's still a
problem. Namely, the case I talked about in my previous message:
member initialization failure.
As you've pointed out, you cannot destroy objects that were never
constructed. So if a class has members X, Y and Z in that order, its
destructor will destroy them in Z, Y, X order. So let's say we have a
constructor that wants to initialize them in Z, X, Y order. If Y
fails, the cleanup code for this constructor needs to destroy *only* X
and Z (in that order). It cannot use the existing destructor's order
for destruction, because invoking that on Z will *also destroy Y*. But
Y was never successfully constructed, so it must not have its
destructor called.
Therefore, this constructor must have special, unique cleanup code for
X and Z that doesn't try to destroy Y. Indeed, *every* constructor
that imposes its own ordering of member initialization *must have* its
own cleanup code.
That's bad.
3: Even if we ignore #1 and #2, there's still the problem that your
idea is under-specified. Specifically... what's the order of
initialization for members that *aren't specified* in the member
initialization list, relative to those which are?
Consider this class:
```
struct S
{
int X;
int Y = X + 5;
int Z;
S() : Z(Y - 2), X(3) {}
};
```
So... what would this do?
In the current standard, this is well-defined due to the order of
initialization. Yes, you'll get a warning because you specified them
in the wrong order. But the compiler will do things correctly. X gets
3, Y gets 3 + 5, and Z gets (3 + 5) - 2.
But what answer would your suggestion give it? That all depends on
when Y gets initialized.
You could argue that Y should be initialized before Z, because it
comes before Z in declaration order. So Y should be initialized first.
But that means it will access the value of X, which has not been
initialized. So you get UB.
You could argue that Y should be initialized after X, since it comes
after X in declaration order. But this means that Z's initialization
would attempt to access Y. Which hasn't been initialized yet.
The overall code in the current standard is valid and meaningful. Yes,
it would be great if we gave an error rather than a warning for
specifying it out-of-order, but that aside, it is logically consistent
and reasonable code.
Your suggestion would turn this into UB. And there would be *no way*
to warn the user about it. I mean sure, a compiler could do it in this
simplistic case, but I can write a case that would hide such accesses
from the compiler, making it impossible for the compiler to diagnose:
```
int compute_value(int const&);
struct S
{
int X;
int Y = compute_value(X);
int Z;
S() : Z(Y - 2), X(3) {}
};
```
Here, all the compiler sees is that `compute_value` gets a reference
to an uninitiailized value. Well... that's OK. Maybe that function
just gets the address of the value (which is well-defined) and does
stuff with it to compute the return value. Maybe that function
accesses the value (which is undefined). The compiler does not and
*cannot* know.
Therefore, it *must assume* that this is valid code.
So even if we made such a change, you would need to define an answer
for this question. And whatever answer it is would be wrong. Not to
mention, it would be a *breaking change*.
<std-proposals_at_[hidden]> wrote:
>
> I see the problem now.
> There is a big misunderstanding of when an object destructor is invoked, and what is its relationship with the constructors.
>
> To be saved by heart:
> "A destructor can be called only when the object is successfully constructed"
No, we understand how the language works just fine. The problems are
manifold-fold:
1: Objects *should be* destroyed in the order opposite of their
initialization. As Arvid has pointed out, this is a *good thing* that
should not be discarded on a whim.
2: Let's say we discard #1 in a sane, reasonable way. We allow
constructors to construct subobjects in arbitrary orders, but the
destructor will always use reverse-declaration order. There's still a
problem. Namely, the case I talked about in my previous message:
member initialization failure.
As you've pointed out, you cannot destroy objects that were never
constructed. So if a class has members X, Y and Z in that order, its
destructor will destroy them in Z, Y, X order. So let's say we have a
constructor that wants to initialize them in Z, X, Y order. If Y
fails, the cleanup code for this constructor needs to destroy *only* X
and Z (in that order). It cannot use the existing destructor's order
for destruction, because invoking that on Z will *also destroy Y*. But
Y was never successfully constructed, so it must not have its
destructor called.
Therefore, this constructor must have special, unique cleanup code for
X and Z that doesn't try to destroy Y. Indeed, *every* constructor
that imposes its own ordering of member initialization *must have* its
own cleanup code.
That's bad.
3: Even if we ignore #1 and #2, there's still the problem that your
idea is under-specified. Specifically... what's the order of
initialization for members that *aren't specified* in the member
initialization list, relative to those which are?
Consider this class:
```
struct S
{
int X;
int Y = X + 5;
int Z;
S() : Z(Y - 2), X(3) {}
};
```
So... what would this do?
In the current standard, this is well-defined due to the order of
initialization. Yes, you'll get a warning because you specified them
in the wrong order. But the compiler will do things correctly. X gets
3, Y gets 3 + 5, and Z gets (3 + 5) - 2.
But what answer would your suggestion give it? That all depends on
when Y gets initialized.
You could argue that Y should be initialized before Z, because it
comes before Z in declaration order. So Y should be initialized first.
But that means it will access the value of X, which has not been
initialized. So you get UB.
You could argue that Y should be initialized after X, since it comes
after X in declaration order. But this means that Z's initialization
would attempt to access Y. Which hasn't been initialized yet.
The overall code in the current standard is valid and meaningful. Yes,
it would be great if we gave an error rather than a warning for
specifying it out-of-order, but that aside, it is logically consistent
and reasonable code.
Your suggestion would turn this into UB. And there would be *no way*
to warn the user about it. I mean sure, a compiler could do it in this
simplistic case, but I can write a case that would hide such accesses
from the compiler, making it impossible for the compiler to diagnose:
```
int compute_value(int const&);
struct S
{
int X;
int Y = compute_value(X);
int Z;
S() : Z(Y - 2), X(3) {}
};
```
Here, all the compiler sees is that `compute_value` gets a reference
to an uninitiailized value. Well... that's OK. Maybe that function
just gets the address of the value (which is well-defined) and does
stuff with it to compute the return value. Maybe that function
accesses the value (which is undefined). The compiler does not and
*cannot* know.
Therefore, it *must assume* that this is valid code.
So even if we made such a change, you would need to define an answer
for this question. And whatever answer it is would be wrong. Not to
mention, it would be a *breaking change*.
Received on 2022-02-26 17:05:49