After an object is destroyed, what happens to subo

2019-01-29 12:02发布

问题:

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!

回答1:

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 of t. Which it does, in your example.

12.7 paragraph 1:

For an object with a non-trivial destructor, referring to any non-static member or base class of the object after the destructor finishes execution results in undefined behavior.

And paragraph 3:

To form a pointer to (or access the value of) a direct non-static member of an object obj, the construction of obj shall have started and its destruction shall not have completed, otherwise the computation of the pointer value (or accessing the member value) results in undefined behavior.

We have here a complete object of type T and a member subobject of type int.

3.8 paragraph 1:

The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • if the object has non-trivial initialization, its initialization is complete.

The lifetime of an object of type T ends when:

  • if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
  • the storage which the object occupies is reused or released.

By the way, 3.7.3 p1:

The storage for these [automatic storage duration] entities lasts until the block in which they are created exits.

And 3.7.5:

The storage duration of member subobjects, base class subobjects and array elements is that of their complete object (1.8).

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 the int 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 the int has not yet ended.

So I think with renew false, the program is well-formed and the assert succeeds!

With renew true, the storage is "reused" by the program, so the lifetime of the original int is over, and the lifetime of another int begins. But then we get into 3.8 paragraph 7:

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and
  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
  • the original object was a most derived object (1.8) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

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:

After the call to a constructor for class X has completed, if a member of X is neither initialized nor given a value during execution of the compound-statement of the body of the constructor, the member has indeterminate value.

Meaning the implementation is compliant if it sets t.mem to zero or 0xDEADBEEF (and sometimes debug modes will actually do such things before calling a constructor).



回答2:

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.