Consider this code (for different values of renew
and cleanse
):
struct T {
int mem;
T() { }
~T() { mem = 42; }
};
// identity functions,
// but breaks any connexion between input and output
int &cleanse_ref(int &r) {
int *volatile pv = &r; // could also use cin/cout here
return *pv;
}
void foo () {
T t;
int &ref = t.mem;
int &ref2 = cleanse ? cleanse_ref(ref) : ref;
t.~T();
if (renew)
new (&t) T;
assert(ref2 == 42);
exit(0);
}
Is the assert
guaranteed to pass?
I understand that this style is not recommended. Opinions like "this is not a sound practice" are not of interest here.
I want an answer showing a complete logical proof from standard quotes. The opinion of compiler writers might also be interesting.
EDIT: now with two questions in one! See the renew
parameter (with renew == 0
, this is the original question).
EDIT 2: I guess my question really is: what is a member object?
EDIT 3: now with another cleanse
parameter!
I first had these two quotes, but now I think they actually just specify that things like
int &ref = t.mem;
must happen during the lifetime oft
. Which it does, in your example.12.7 paragraph 1:
And paragraph 3:
We have here a complete object of type
T
and a member subobject of typeint
.3.8 paragraph 1:
By the way, 3.7.3 p1:
And 3.7.5:
So no worries about the compiler "releasing" the storage before the
exit
in this example.A non-normative note in 3.8p2 mentions that "12.6.2 describes the lifetime of base and member subobjects," but the language there only talks about initialization and destructors, not "storage" or "lifetime", so I conclude that section does not affect the definition of "lifetime" for subobjects of trivial type.
If I'm interpreting all this right, when
renew
is false, the lifetime of the complete class object ends at the end of the explicit destructor call, BUT the lifetime of theint
subobject continues to the end of the program.3.8 paragraphs 5 and 6 say that pointers and references to "allocated storage" before or after any object's lifetime can be used in limited ways, and list a whole lot of things you may not do with them. Lvalue-to-rvalue conversion, like the expression
ref == 42
requires, is one of those things, but that's not an issue if the lifetime of theint
has not yet ended.So I think with
renew
false, the program is well-formed and theassert
succeeds!With
renew
true, the storage is "reused" by the program, so the lifetime of the originalint
is over, and the lifetime of anotherint
begins. But then we get into 3.8 paragraph 7:The first bullet point here is the trickiest one. For a standard-layout class like your
T
, the same member certainly must always be in the same storage. I'm not certain whether or not this is technically required when the type is not standard-layout.Although whether
ref
may still be used or not, there's another issue in this example.12.6.2 paragraph 8:
Meaning the implementation is compliant if it sets
t.mem
to zero or0xDEADBEEF
(and sometimes debug modes will actually do such things before calling a constructor).You have not destroyed memory, you only manually called destructor (in this context it's not different then calling normal method). Memory (stack part) of your
t
variable was not 'released'. So this assert will always pass with your current code.