12

In the following short example, what can be said about the object the pointer f points to or used to point to just before returning from main?

#include <vector>

struct foo {
    std::vector<int> m;
};

int main()
{
    auto f = new foo;
    f->~foo();
}

I believe that there is no longer an object foo where f used to point. I've received a lot of comments that this may not be correct and that instead there could be an object foo in a destroyed, dead or otherwise invalid state.

What does the language standard have to say about the existence of an objects that is explicitly destroyed but whose storage is still valid?

In other words, can it reasonably be said that there is still an object at f that is outside of its lifetime? Is there such a thing as an object that is not in its lifetime, not begin constructed and not being destructed?


Edit :

It is clear that an object can exist when it isn't in its lifetime. During construction and destruction there is an object and its lifetime has not yet begun or as already ended. From https://timsong-cpp.github.io/cppwp/intro.object#1 :

[...] An object occupies a region of storage in its period of construction ([class.cdtor]), throughout its lifetime, and in its period of destruction ([class.cdtor]). [...]

But after f->~foo(); the object that was pointed to by f (lets call it o) is not being constructed, it is not in its lifetime and it is not being destructed. My reading of this section is that o cannot occupy the storage anymore because it isn't in any of the enumerated situations. It seems like this implies that there is no o anymore and that there can't be a pointer to o anymore. By contradiction, if you had a pointer to o then that pointer would point to storage which o can't occupy.


Edit 2 :

If there isn't an object anymore, then what kind of value does foo have? It seems like the only sensible possible value it can have is a pointer to an object, which would contradict the statement. See this question.

François Andrieux
  • 28,148
  • 6
  • 56
  • 87
  • from C++20 http://eel.is/c++draft/basic.life#5 applies, I believe, but I think things were different before then—not sure on that. – N. Shead Sep 07 '20 at 01:46
  • @N.Shead Interesting that it is only UB to not call the destructor if you rely on side effects. Though it is unrelated to the question, it seems like if I performed placement `new` on the object pointed to by `f` it would be well defined behavior but also leak any memory `f->m` might have held? Or would I be considered to be relying on the side effect that it would be freed? – François Andrieux Sep 07 '20 at 01:49
  • I guess that depends on if you consider deallocating memory to be a side effect. The standard is pretty loose on what exactly a side-effect is, apart from "writing to files", as far as I know. I suspect that generally this would be UB, though I can't say for sure. – N. Shead Sep 07 '20 at 01:51
  • @FrançoisAndrieux Any memory used by `f->m` is released in the `~foo()` destructor, whether it's called manually or automatically. What *would* leak in the posted code is the object memory allocated by `new`. In order to clean that up, you would need to do a placement `new` on `f` then `delete f;`. – dxiv Sep 07 '20 at 02:01
  • @dxiv My previous comment is hypothetical if `f->~foo();` was replaced with `new (f) foo;` with best effort cleanup after and if `f->m` had capacity. See that I start that part with *"Though it is unrelated to the question"*. In that hypothetical scenario, it isn't clear to me if is UB or if it is well defined but with the first `foo` leaking it's member's internal resources. – François Andrieux Sep 07 '20 at 02:02
  • @FrançoisAndrieux In that case the same `foo` object would be constructed twice with no intervening destructor call so, yes, any member and base destructors would be bypassed. Other than that I do not see any reason for UB, unless the particular semantics of the bypassed destructors creates UB. – dxiv Sep 07 '20 at 02:04
  • @dxiv See the first comment's link. If freeing internals is considered a relied upon side effects then it is UB. My reply to that comment was asking whether or not it was. – François Andrieux Sep 07 '20 at 02:07
  • @FrançoisAndrieux If the *language* relied on that as a side effect then there should be no qualification needed - it would always be UB. The way it is written, I take it to mean that if *your* code relies on those side effects then it could/would be UB (for example, having the constructor save its `this` pointer into a global list, and the destructor removing it from that list). At least that's my reading of it. – dxiv Sep 07 '20 at 02:11

1 Answers1

9

In C++, objects essentially are eternal. There's nothing in the language that makes an object disappear. An object that is outside of its lifetime is still an object, it still occupies storage, and the standard has specific things that you can do with a pointer/reference to an object which is outside of its lifetime.

An object only truly goes away when it is impossible to have a valid pointer/reference to it. This happens when the storage occupied by that object ends its storage duration. A pointer to storage that is past its duration is an invalid pointer, even if the address itself later becomes valid again.

So by calling the destructor instead of using delete f (which would also deallocate the storage), f remains pointing to an object of type foo, but that object is outside of its lifetime.


The justification for my above statements basically boils down to the standard having none of the provisions that it would need in order to support the concept of objects being uncreated.

Where is object uncreation?

The standard provides clear, unequivocal statements about when an object comes to exist within a piece of storage. [intro.object]/1 outlines the exact mechanisms that provoke the creation of an object.

The standard provides clear, unequivocal statements about when an object's lifetime begins and ends. [basic.life] in its entirely outlines these things, but [basic.life]/1 in particular explains when an object's lifetime begins and ends.

The standard does not provide any statement (clear or otherwise) about when an object no longer exists. The standard says when objects are created, when their lifetimes begin, and when they end. But never does it say when they stop existing within a piece of storage.

There has also been discussion about statements of the form:

any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways.

Emphasis added.

The use of the past-tense suggests that the object is no longer located in that storage. But when did the object stop being located there? There is no clear statement about what exactly caused that to happen. And without that, the use of past-tense here just doesn't matter.

If you can't point to a statement about when it stopped being there, then the absolute most you can say is that there are a couple of places in the standard with wording that could be cleaned up. It doesn't undo the clear fact that the standard does not say when objects stop existing.

Pointer validity

But it does say when objects are no longer accessible.

In order for an object to cease existing, the standard would have to account for pointers which point to those objects when they no longer exist. After all, if a pointer is pointing to an object, then that object must still exist, right?

[basic.compound]/3 outlines the states that a pointer can have. Pointers can be in one of four states:

  • a pointer to an object or function (the pointer is said to point to the object or function), or
  • a pointer past the end of an object ([expr.add]), or
  • the null pointer value ([conv.ptr]) for that type, or
  • an invalid pointer value.

There is no allowance given for a pointer which points to no object. There is an allowance for an "invalid pointer value", but pointers only become invalid when the storage duration for the storage they point into ends:

When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values.

Note that this statement means that all pointers to such objects cease being in the "pointer to object" state and enter the "invalid pointer" state. Thus, objects within such storage (both within and outside of their lifetimes) stop being accessible.

This is exactly the sort of statement that would need to exist for the standard to support the concept of objects no longer existing.

But no such statement exists.

[basic.life] does have several statements that address limited ways that pointers to objects outside of their lifetime can be used. But note the specific wording it uses:

For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the pointer as if the pointer were of type void*, is well-defined.

It never says that the pointer "points to" allocated storage. It never undoes [basic.compound]/3's declaration about the kinds of pointers. The pointer is still a pointer to an object; it's just that the pointer "refers to allocated storage". And that the pointer can be used as a void*.

That is, there's no such thing as a "pointer to allocated storage". There is a "pointer to an object outside of its lifetime, whose pointer value can be used to refers to allocated storage". But is still a "pointer to an object".

Lifetime is not existence

Objects must exist in order to have a lifetime. The standard makes that clear. However, the standard does not at any point link the existence of an object to its lifetime.

Indeed, the object model would be a lot less complicated if ending the lifetime of an object meant that the object didn't exist. Most of [basic.life] is about carving out specific ways you can use the name of an object or a pointer/reference to it outside of the lifetime of that object. We wouldn't need that sort of stuff if the object itself didn't exist.

Stated in discussion about this matter was this:

I believe mentions of out-of-lifetime objects are there to account for objects that are being constructed and objects that are being destructed.

If that were true, what is [basic.life]/8 talking about with this statement:

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

If pointers to the original object become pointers to allocated memory when the object's lifetime ends, why does this statement talk about pointers to the original object? Pointers can't point to objects that don't exist because they don't exist.

This passage can only make sense if those objects continue to exist outside of their lifetimes. And no, it's not just about within the constructor/destructor; the example in the section makes that abundantly clear:

struct C {
  int i;
  void f();
  const C& operator=( const C& );
};

const C& C::operator=( const C& other) {
  if ( this != &other ) {
    this->~C();                 // lifetime of *this ends
    new (this) C(other);        // new object of type C created
    f();                        // well-defined
  }
  return *this;
}

C c1;
C c2;
c1 = c2;                        // well-defined
c1.f();                         // well-defined; c1 refers to a new object of type C

While operator= does call the destructor, that destructor finishes before the this pointer is used. Thus, the special provisions of of [class.cdtor] does not apply to this at the moment the new object is created. So the new object is created outside of the destructor call to the old one.

So it's very clear that the "outside its lifetime" rules for objects are meant to always work. It's not just a provision for constructors/destructors (if it was, it would explicitly call that out). This means that names/pointers/references must still name/point-to/reference objects outside of their lifetime until the creation of the new object.

And for that to happen, the object they name/point-to/reference must still exist.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 2
    _An object that is outside of its lifetime is still an object, it still occupies storage_ [This](https://timsong-cpp.github.io/cppwp/n4861/intro.object#1.sentence-3) disagrees – Language Lawyer Sep 07 '20 at 11:01
  • To clarify what it disagrees with: with that an object still occupies storage, not with that it still exists. – Language Lawyer Sep 07 '20 at 12:57
  • Thank you for the answer. This all makes sense to me, except that according to [this](https://timsong-cpp.github.io/cppwp/intro.object#1) *"An object occupies a region of storage in its period of construction ([class.cdtor]), throughout its lifetime, and in its period of destruction ([class.cdtor])."*. So after `~foo()` the object is neither being constructed, nor in its lifetime nor being destructed so it can't occupy storage. If you can just reconcile that with the answer, it would definitely answer the question. – François Andrieux Sep 07 '20 at 15:01
  • ... I believe mentions of out-of-lifetime objects are there to account for objects that are being constructed and objects that are being destructed. Edit : Additionally, in the passage you linked : *"For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.deallocation]),"* implies that the pointer no longer points to the object after it is destroyed but rather to the storage it occupied. – François Andrieux Sep 07 '20 at 15:02
  • 1
    @FrançoisAndrieux _Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.deallocation])," implies that the pointer no longer points to the object_ This is some ancient crap wording not updated to match the new pointer values taxonomy. – Language Lawyer Sep 07 '20 at 15:19
  • I've been going over the linked passage again and it seems to me like it contradicts your answer. This scenario would fall under the clause *"Otherwise, such a pointer refers to allocated storage"* which means the pointer doesn't point to the object. Unless, the object *is* the storage, in which case the storage should have the properties of an object such as type and value which I'm not sure is true. – François Andrieux Sep 19 '20 at 01:31
  • Is there a nuance difference between the *C++ virtual machine* to which the standard specifies, versus the actual implementations and vendor's aggressively (and rightly so) exploiting the *as-if* rule? – Eljay Sep 22 '20 at 21:25
  • 1
    @Eljay This question has the `language-lawyer` tag so it should only be about the C++ virtual machine. – François Andrieux Sep 24 '20 at 13:18
  • 1
    I believe the standard you quote disagrees with your conclusion. "[...] the address of the storage location where the object will be or was located [...]". This implies to me that a class pointer has become a pointer to storage, not a pointer to an object, because the object no longer exists. – AndyG Sep 24 '20 at 13:49
  • @AndyG: Then find the place in the standard that says that an object is to be removed from a location. [intro.object]/1 outlines the *exact* conditions under which an object comes to exist in storage. Where is the corresponding part that outlines the conditions under which an object leaves storage? [basic.life] outlines when an object's lifetime starts and ends. If you're so sure the standard thinks that objects go away before their storage does, then show me a quote outlining the circumstances that would cause it to happen. – Nicol Bolas Sep 24 '20 at 13:52
  • @NicolBolas It's not so much about the object being "removed" from anywhere as it is that it's no longer considered to be an object. Specifically, here's the wording in your answer I disagree with: "An object that is outside of its lifetime is still an object" – AndyG Sep 24 '20 at 13:54
  • @NicolBolas I've proposed such a passage in an earlier comment, which cites under which circumstances an object occupies storage. If it is accurate that an object must occupy storage, any object that would not occupy storage would not be an object and by contradiction doesn't exist. – François Andrieux Sep 24 '20 at 13:55
  • @AndyG: But it is still an object. There's specific wording in [basic.life] that talks about pointer/references to objects "outside of its lifetime". – Nicol Bolas Sep 24 '20 at 13:55
  • @NicolBolas Those may only refer to objects being constructed and destructed, which are unambiguously objects and are outside of their lifetime. – François Andrieux Sep 24 '20 at 13:56
  • @FrançoisAndrieux: So you're saying that you agree with my second paragraph, where I say: "*An object only truly goes away when it is impossible to have a valid pointer/reference to it. This happens when the storage occupied by that object ends its storage duration.*" – Nicol Bolas Sep 24 '20 at 13:56
  • @FrançoisAndrieux: "*Those may only refer to objects being constructed and destructed*" "may"? Look at the example in [basic.life]/8, where there is clearly a valid pointer to an object. From [basic.compound]/3, we see that a pointer must point to an object, point to past-the-end of an object, be null, or be invalid. And the latter *only happens* when the storage being pointed to goes away. – Nicol Bolas Sep 24 '20 at 14:00
  • @NicolBolas I'm not sure about the first sentence. And the second sentence is what I'm asking about in this question. If I could conclusively argue that sentence either way, I would have been able to answer my own question. I've found evidence that seems contradictory and I am trying to sort it out. The last few comments are evidence that I found which lean towards objects no longer existing after the destructor. I've found evidence to the contrary, which can be found in the question I linked in my edit. Edit : Your last comment basically sums up the linked question. – François Andrieux Sep 24 '20 at 14:00
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/222021/discussion-between-francois-andrieux-and-nicol-bolas). – François Andrieux Sep 24 '20 at 14:01
  • _In C++, objects essentially are eternal. There's nothing in the language that makes an object disappear_ [Once a destructor is invoked for an object, the object no longer exists](https://timsong-cpp.github.io/cppwp/n4861/class.dtor#19.sentence-1) ;) – Language Lawyer Oct 05 '20 at 22:38
  • @LanguageLawyer: Then that contradicts the rules of [class.cdtor] and [basic.life], which spell out rules for accessing the object within its constructor/destructor outside of its lifetime. If the object no longer exists, then you can't talk about it or have pointers to it. – Nicol Bolas Oct 05 '20 at 23:02
  • @NicolBolas the passage says "pointED to the original object" in past tense. It does not imply that the pointer being talked about is still pointing to the out-of-lifetime object. Although I generally agree that that might be a more sensible interpretation, given your last code snippet. – JMC Dec 18 '20 at 16:47