6

Suppose I have a class whose constructor spawns a thread that deletes the object:

class foo {
public:
    foo() 
    : // initialize other data-members
    , t(std::bind(&foo::self_destruct, this)) 
    {}

private:
    // other data-members
    std::thread t;
    // no more data-members declared after this

    void self_destruct() { 
        // do some work, possibly involving other data-members
        delete this; 
    }
};

The problem here is that the destructor might get invoked before the constructor has finished. Is this legal in this case? Since t is declared (and thus initialized) last, and there is no code in the constructor body, and I never intend to subclass this class, I assume that the object has been completely initialized when self_destruct is called. Is this assumption correct?

I know that the statement delete this; is legal in member-functions if this is not used after that statement. But constructors are special in several ways, so I am not sure if this works.

Also, if it is illegal, I am not sure how to work around it, other spawning the thread in a special initialization-function that must be called after construction of the object, which I really would like to avoid.

P.S.: I am looking for an answer for C++03 (I am restricted to an older compiler for this project). The std::thread in the example is just for illustration-purposes.

Björn Pollex
  • 75,346
  • 28
  • 201
  • 283
  • 1
    You could end up calling `t`'s destructor before its constructor is fully executed. That would surely be UB. – mfontanini Jan 21 '13 at 17:55
  • As for solving the problem, how about only allowing construction of `foo` from a factory function, that creates a `shared_ptr` you can hand the thread? (using shared_from_this would have been preferable, but that [isn't an option](http://stackoverflow.com/questions/3629557/boost-shared-from-this)) – user786653 Jan 21 '13 at 18:18
  • In that example, what is more worry-some is that the thread object will be destroyed before returning from the function it calls (self-destruct). So, whatever work needs to be done after to "end the thread" will be executed on a dead object, and that's UB for sure (most likely a crash in this case). You can always control your own class implementation to allow for a `delete this;` that is safe because you don't access any data member afterwards, but you can't "trick" a third-party class object (`std::thread`) to destroy itself like that and expect things to be well-behaved. – Mikael Persson Jan 21 '13 at 18:28
  • I suggest taking the acceptance away from my answer. It was accepted while incorrect and although I've tried to answer it fully, I'm not sure it's totally correct myself. – Joseph Mansfield Jan 21 '13 at 20:22

4 Answers4

7

Firstly, we see that an object of type foo has non-trivial initialization because its constructor is non-trivial (§3.8/1):

An object is said to have non-trivial initialization if it is of a class or aggregate type and it or one of its members is initialized by a constructor other than a trivial default constructor.

Now we see that an object of type foo's lifetime begins after the constructor ends (§3.8/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.

Now, it is undefined behaviour if you do delete on the object before the end of the constructor if the type foo has a non-trivial destructor (§3.8/5):

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated [...] any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, [...]

So since our object is under construction, we take a look at §12.7:

Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2).

That means that it's fine for self_destruct to be called while the object is being constructed. However, this section says nothing specifically about destroying an object while it is being constructed. So I suggest we look at the operation of the delete-expression.

First, it "will invoke the destructor (if any) for the object [...] being deleted." The destructor is a special case of member function, so it is fine to call it. However, §12.4 Destructors says nothing about whether it is well-defined when the destructor is called during construction. No luck here.

Second, "the delete-expression will call a deallocation function" and "the deallocation function shall deallocate the storage referenced by the pointer". Once again, nothing is said about doing this to storage that is currently being used be an object under construction.

So I argue that this is undefined behaviour by the fact that the standard hasn't defined it very precisely.

Just to note: the lifetime of an object of type foo ends when the destructor call starts, because it has a non-trivial destructor. So if delete this; occurs before the end of the object's construction, its lifetime ends before it starts. This is playing with fire.

Community
  • 1
  • 1
Joseph Mansfield
  • 108,238
  • 20
  • 242
  • 324
  • 1
    This convincingly forbids the case under discussion, but I think it may be invalid even with a trivial destructor, since the Standard doesn't define behavior unless an object lives for the duration of a call to any non-static member function. – Ben Voigt Jan 21 '13 at 18:02
  • @BenVoigt: i wouldn't say "convincingly"... ;-) – Cheers and hth. - Alf Jan 21 '13 at 18:04
  • @Cheersandhth.-Alf How can I convince you? :( – Joseph Mansfield Jan 21 '13 at 18:13
  • uhm, i had to delete my reply. not sure. but i'm thinking about this object as a sub-object of another. then it's surely UB anyway. i don't feel that that is addressed by this answer. – Cheers and hth. - Alf Jan 21 '13 at 18:28
  • @sftrabbit: OK I found where it goes awry: the quote of §3.8/5 leaves out the part -- indicated by ellipsis -- where it says that the bullet point **does not apply**. instead for the case at hand it directs to §12.7; quote "For an object under construction or destruction, see 12.7". So this answer certainly isn't it :-(, but then my own answer isn't good enough either, sorry. – Cheers and hth. - Alf Jan 21 '13 at 19:20
  • -1 Ouch incorrect answer (§3.8/5 bullet point does not apply) selected as solution, I must downvote, unfortunately the only visible means of indicating incorrectness in SO. – Cheers and hth. - Alf Jan 21 '13 at 19:27
  • @Cheersandhth.-Alf I did my best, but I think this is just poorly defined. I wish I could remove my accept though. – Joseph Mansfield Jan 21 '13 at 19:53
  • 0 removed downvote after correction. this may be the best that can be said. – Cheers and hth. - Alf Jan 21 '13 at 22:12
2

I daresay it is well-defined to be illegal (though it might obviously still work with some compilers).

This is somewhat the same situation as "destructor not called when exception is thrown from constructor".

A delete-expression, according to the standard, destroys a most derived object (1.8) or array created by a new-expression (5.3.2). Before the end of the constructor, an object is not a most derived object, but an object of its direct ancestor's type.

Your class foo has no base class, so there is no ancestor, this therefore has no type and your object is not really an object at all at the time delete is called. But even if there was a base class, the object would be a not-most-derived object (still rendering it illegal), and the wrong constructor would be called.

Damon
  • 67,688
  • 20
  • 135
  • 185
1

delete this; works correctly in practice on most platforms; some may even guarantee correct behavior as a platform-specific extension. But IIRC it isn't well-defined according to the Standard.

The behavior you're relying on is that it's often possible to call a non-virtual non-static member function on a dead object, as long as that member function doesn't actually access this. But this behavior is not allowed by the Standard; it is at best non-portable.

Section 3.8p6 of the Standard makes it undefined behavior if an object isn't live during a call to a non-static member function:

Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a glvalue refers to allocated storage, and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if:

  • an lvalue-to-rvalue conversion is applied to such a glvalue,
  • the glvalue is used to access a non-static data member or call a non-static member function of the object, or
  • the glvalue is implicitly converted to a reference to a base class type, or
  • the glvalue is used as the operand of a static_cast except when the conversion is ultimately to cvchar& or cvunsigned char&, or
  • the glvalue is used as the operand of a dynamic_cast or as the operand of typeid.

For this specific case (deleting an object under construction), we find in section 5.3.5p2:

... In the first alternative (delete object), the value of the operand of delete may be a null pointer value, a pointer to a non-array object created by a previous new-expression, or a pointer to a subobject representing a base class of such an object (Clause 10). If not, the behavior is undefined. In the second alternative (delete array), the value of the operand of delete may be a null pointer value or a pointer value that resulted from a previous array new-expression. If not, the behavior is undefined.

This requirement is not met. *this is not an object created, past tense, by a new-expression. It is an object being created (present progressive). And this interpretation is supported by the array case, where the pointer must be the result of a previous new-expression... but the new-expression is not yet completely evaluated; it is not previous and it has no result yet.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • "But IIRC [delete this] isn't well-defined according to the Standard." It is. – Cheers and hth. - Alf Jan 21 '13 at 18:05
  • @Alf: Reference please, I'd like to review that rule. – Ben Voigt Jan 21 '13 at 18:06
  • I think you could end the quote right after the " For an object under construction or destruction, see 12.7". – Cheers and hth. - Alf Jan 21 '13 at 18:06
  • there is no special rule to allow computing `2+2`, and there is no special rule to allow `delete this`. but there are rules against accessing stuff in a destroyed object, so that's what one needs to avoid after a `delete this`. most GUI frameworks do `delete this`, so it's pretty crucial functionality. – Cheers and hth. - Alf Jan 21 '13 at 18:09
  • @Alf: And the rule I quoted may be interpreted to forbid overlapping a call to a non-static member function with destruction of the object. (Is a function call the instant control transfers, or the duration of execution of the function?) – Ben Voigt Jan 21 '13 at 18:10
  • @BenVoigt Maybe I'm misunderstanding this text, but it seems that it's only talking about glvalues that refer to objects that are not under construction or destruction, but the object in question is under construction. – Joseph Mansfield Jan 21 '13 at 18:11
  • @sftrabbit: I'm trying to address a broader topic of "can I allow an object to be deleted while its members are executing?" After the deleter is invoked, the object isn't under construction, it's dead, and this rule would apply as well. – Ben Voigt Jan 21 '13 at 18:12
  • @BenVoight But by that point the glvalue has already been used to call the non-static member function, hasn't it? Yes, execution occurs after destruction, but is that a problem? It only says here that *calling* the function would cause undefined behaviour. – Joseph Mansfield Jan 21 '13 at 18:44
  • @sftrabbit: Like I said in my earlier comment, what is the duration of a call? The call instruction, or the entire execution frame up until the (possibly implicit) `return` statement is reached? – Ben Voigt Jan 21 '13 at 20:40
1

Formally the object doesn't exist until the constructor has finished successfully. Part of the reason is that the constructor might be called from a derived class' constructor. In that case you certainly don't want to destroy the constructed sub-object via an explicit destructor call, and even less invoke UB by calling delete this on a (part of a) not completely constructed object.


Standardese about the object existence, emphasis added:

C++11 §3.8/1:
The lifetime of an object is a runtime property of the object. An object is said to have non-trivial initialization if it is of a class or aggregate type and it or one of its members is initialized by a constructor other than a trivial default constructor. [Note: initialization by a trivial copy/move constructor is non-trivial initialization. —end note ] 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 constructor in this case is non-trivial just by being user-provided.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • 1
    Citations please. Cheers and hth. – Lightness Races in Orbit Jan 21 '13 at 18:38
  • @LightnessRacesinOrbit: I added the relevant standardese I found, but I think this is a case of something being UB by way of not being explicitly allowed (namely not explicitly allowed by §12.7). Those cases generally require a lot of work to nail down. I'm sorry, but my answer here is more on the level of hand-waiving and I don't have time to do the extended research :-( – Cheers and hth. - Alf Jan 21 '13 at 19:24
  • I'm pretty sure this quote covers it. – Lightness Races in Orbit Jan 21 '13 at 19:31
  • @BenVoigt: no, I haven't said what you misleadingly paraphrase. but anyway, in the case of what you can do with a pointer to an object that's under construction, there is some vague wording in §3.8/5 to "see §12.7". and in that section some things that can be done (e.g. `typeid` and `dynamic_cast`) are explicitly listed. That said (which should address your puzzlement about why the situations are different), as I noted to Tomalak I don't think this answer is very solid, at all. It may be that the standard isn't fully clear here, in which case we're into interpretation... – Cheers and hth. - Alf Jan 21 '13 at 21:03