24

I have this code:

struct data {
  void doNothing() {}
};

int main() {
    data* ptr = new data();
    ptr->~data();
    ptr->doNothing();
    ::operator delete(ptr);
}

Note that doNothing() is being called after the object has been destroyed but before its memory was deallocated. It looks like "object lifetime" has ended however the pointer still points to proper allocated memory. The member function does not access any member variables.

Would member function call be legal in this case?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
sharptooth
  • 167,383
  • 100
  • 513
  • 979
  • 6
    Regardless of the answer, this would be terribly confusing to anyone later on trying to maintain the code. Don't do it, even if it somehow turns out to be legal (or working in a certain scenario). I'd say it's not legal but it's been a while since I read the C++ specs and I may be wrong. – xxbbcc May 18 '15 at 18:50
  • 4
    If your data structure truly has no destruction logic, why do you call the destructor in the first place before you call the member function. If you expect the destructor to change, you shouldn't rely on the trivial behavior. In a larger application, data and the code in your main would likely be in different source files and one developer changing one of them could be unaware of the contents of the other. – xxbbcc May 18 '15 at 18:50
  • @xxbbcc Suppose that call comes from a template written by someone else. – sharptooth May 18 '15 at 19:06
  • 2
    Very interesting question; however, I wonder - if the member function does not access any members, could it be made `static` such that it is disconnected from any instance? – Codor May 18 '15 at 20:26
  • @sharptooth Here's [a followup](http://stackoverflow.com/questions/30315524/what-does-it-mean-for-an-object-to-exist-in-c) that's relevant and/or no less confusing. – Barry May 19 '15 at 02:12
  • 1
    No, it is not legal. Officers have been dispatched. – Atsby May 19 '15 at 03:43
  • Don't do it. Seems a really bad idea. +1 for the imagination and being creative – rpax May 20 '15 at 18:36

3 Answers3

31

Yes, in the case of the code in the OP. Because the destructor is trivial, calling it doesn't end the object's lifetime. [basic.life]/p1:

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.

[class.dtor]/p5:

A destructor is trivial if it is not user-provided and if:

  • the destructor is not virtual,
  • all of the direct base classes of its class have trivial destructors, and
  • for all of the non-static data members of its class that are of class type (or array thereof), each such class has a trivial destructor.

No, not in the general case. Invoking a non-static member function after the object's lifetime has ended is UB. [basic.life]/p5:

[A]fter the lifetime of an object has ended and before the storage which the object occupied is reused or released, 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, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:

  • [...]
  • the pointer is used to access a non-static data member or call a non-static member function of the object, or
  • [...]
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • 1
    So since the lifetime of the trivial object does not end with the destructor call, am I allowed to explicitly destroy it multiple times? – Baum mit Augen May 18 '15 at 19:40
  • The OP calls "a non-static member function of the object", so according to your last citation the OP code indeed has undefined behavior! – cmaster - reinstate monica May 18 '15 at 20:17
  • @BaummitAugen This is interesting. In [class.dtor], the part immediately after what I quoted in my answer is "the behavior is undefined if the destructor is invoked for an object whose lifetime has ended (3.8). [ Example: if the destructor for an automatic object is explicitly invoked, and the block is subsequently left in a manner that would ordinarily invoke implicit destruction of the object, the behavior is undefined. —end example ]" The example suggests that "no longer exists" *is* synonymous with "lifetime has ended". No? – Barry May 18 '15 at 20:25
  • 1
    @cmaster only if the object's lifetime has ended. – T.C. May 18 '15 at 20:35
  • @Barry I guess not. The very first quote in this answers proofs that the lifetime has *not* ended. Together with your quote, that would mean that you can actually destruct such an object multiple times. I feel like the example does not apply here. Interesting indeed. – Baum mit Augen May 18 '15 at 20:41
  • What does user-provided mean? – Random832 May 18 '15 at 23:49
  • @Random832: It means you wrote a `Foo::~Foo` function explicitly rather than letting the compiler write it for you. – Kevin May 19 '15 at 02:03
  • @Kevin Actually, explicitly defaulting it outside the class counts too. – Deduplicator Jan 08 '19 at 00:56
5

Given [class.dtor]:

Once a destructor is invoked for an object, the object no longer exists

This fragment from [basic.life]:

... or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways... The program has undefined behavior if:
— ...
— the pointer is used to access a non-static data member or call a non-static member function of the object

stipulates that what you have is undefined behavior. However, there's different language here - "the object no longer exists" versus "the object has ended", and earlier in [basic.life], it's stated that:

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.

On the one hand, you do not have a non-trivial destructor, so [basic.life] suggests that the lifetime of the object isn't ended yet - the storage hasn't be reused or released. On the other hand, [class.dtor] suggests that the object "no longer exists" which certainly sounds like it should be a synonym for "ended", yet is not.

I suppose the "language-lawyer" answer is: it's technically not undefined behavior and seems perfectly legal. The "code quality" answer is: don't do it, it's confusing at best.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    Interestingly, that phrase from [class.dtor] is the only part of the standard (as far as I can tell) that refers to an object as "existing". – Barry May 18 '15 at 18:52
4

The other answers are correct, but leave out one detail:

It is allowed if either the destructor or constructor are trivial. Other answers have clearly explained that if the destructor is trivial, the lifetime of the original object has not ended.

But if the constructor is trivial, then an object exists whenever a memory location of appropriate size and alignment is present. So even with a non-trivial destructor and trivial constructor, a brand-new object exists that you can call members on.

The verbiage that the other answers left out, that immediately precedes the end-of-lifetime rule they quoted, says

The lifetime of an object is a runtime property of the object. An object is said to have non-vacuous 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-vacuous 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-vacuous initialization, its initialization is complete.

An important note on usage of a new object trivially created in the storage of the old destroyed one: due to the trivial construction, no initialization has been performed on the data members and they now all have indeterminate value, so you must set their values (either through initialization, or invocation of an assignment operator that doesn't use the previous value) prior to reading any.

In the OP's situation, the original object still lives, so this caveat does not apply.

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720