14

I've always assumed that an object begins and ends its lifetime in the same memory location, but I've recently come across a scenario where I need to be sure. Specifically, I'm looking for a guarantee from the standard that no matter what optimizations the compiler performs the address an object is constructed at is the same one that it will have its destructor called from... and that its destructor is, indeed, guaranteed to be called from that location unless the program is terminating.

I've always taken this stuff for granted, but upon closer examination I can't find a guarantee, and there's some language around copy and move elision that I'm not sure how to interpret. I'm hoping that some of the more standards-conversant people here can point me to chapter and verse.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
Reid Rankin
  • 1,078
  • 8
  • 26
  • 1
    This is not C. I think it's UB to assume anything about the address of an object. What if there's a garbage collector? – Jazzwave06 Aug 15 '19 at 20:28
  • That is absolutely not true. – Alecto Irene Perez Aug 15 '19 at 20:29
  • 2
    @sturcotte06 garbage collectors are not allowed to change the address of a living object. (because if they did, they would break any code that was holding a pointer to that object's original location. All the garbage collector can do is free up the memory of objects that no longer have anything pointing to them) – Jeremy Friesner Aug 15 '19 at 20:33
  • You can have effects that LOOK like the address changed, [Iterator invalidation](https://stackoverflow.com/questions/6438086/iterator-invalidation-rules) in a container, for example. – user4581301 Aug 15 '19 at 21:02

3 Answers3

15

What you are looking for is defined in [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]).

This means the address cannot change as long as you can access it.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • This would have to be so: take class, "House," which has as a member a pointer to a class, "Street," which is assigned at some point after a Street object has been created. If the address of the Street object changed, how would you go through all the (potentially huge number) of House objects to change the value of its Street pointer? – Adrian Mole Aug 15 '19 at 20:33
  • Wow, fast and precise. Can you perchance shed any light on the destructor question? Specifically, [[basic.life]/5](https://timsong-cpp.github.io/cppwp/basic.life#5) seems to indicate that there are scenarios where the destructor might not be called due to direct storage reusage or release, but destructors with side-effects are obviously a thing people use, and I can't square those facts in my head. – Reid Rankin Aug 15 '19 at 20:41
  • @Adrian I could imagine a situation in which the compiler, as an optimization, would allow the address of the Street to change, but only if it knows there are no outstanding references to it. – Reid Rankin Aug 15 '19 at 20:43
  • @ReidRankin For objects with automatic storage, their destructor is guaranteed to be called when going out of scope. For objects with dynamic storage duration, you have to call `delete`/`delete[]` for the destructor to be called. for objects with static storage duration the destructor is called at the end of the program. All that said, there is the as if rule so if there is no side effect to destructing something, the compiler doesn't actually have to call the destructor. – NathanOliver Aug 15 '19 at 20:47
  • This can be kind of situational so if you want a guaranteed answer, I would suggest asking a new question with an example and asking if the behavior is guaranteed. – NathanOliver Aug 15 '19 at 20:47
  • I feel thick, but how does that quote guarantee that the address cannot change? – Maxim Egorushkin Aug 15 '19 at 21:21
  • 1
    @MaximEgorushkin *a* is singular, so it only has one location. – NathanOliver Aug 15 '19 at 21:23
  • @NathanOliver I have found this more-general question useful in its own right, but [here you go](https://stackoverflow.com/questions/57516409). – Reid Rankin Aug 15 '19 at 21:23
  • @NathanOliver Still, I fail to see how "a (one) region of storage" guarantees its location? I can remap the page elsewhere and that still maintains "a (one) region of storage" property. I do not think this is a good answer, at least for thick people like me. – Maxim Egorushkin Aug 15 '19 at 21:25
  • @NathanOliver one location at each period of time == the same location at each period of time? Do not think so. – Language Lawyer Aug 15 '19 at 21:27
  • @LanguageLawyer _a_ in English means _one_ as well as _some_. So one could read that the object resides in _some_ location. – Maxim Egorushkin Aug 15 '19 at 21:28
  • @LanguageLawyer The way the the oxford comma works in *construction ([class.cdtor]), throughout its lifetime, and in its period of destruction* meas that one storage location is the location the object has from when it is constructed until after the destructor exits. – NathanOliver Aug 15 '19 at 21:31
  • @LanguageLawyer IMO, this quote contains a provision for object moving garbage collector. – Maxim Egorushkin Aug 15 '19 at 21:31
  • @NathanOliver The way I read it is that an object occupies some location. If it doesn't - there is no object (may be empty base class sub-object). No word that the location must be fixed. If the standard wanted to fix the location it would say _the object occupies a particular **location** throughout its lifetime_. This is a too important property to require it only using _a_ wording that has many meanings. – Maxim Egorushkin Aug 15 '19 at 21:34
1

Specifically, I'm looking for a guarantee from the standard that no matter what optimizations the compiler performs the address an object is constructed at is the same one that it will have its destructor called from...

and that its destructor is, indeed, guaranteed to be called from that location unless the program is terminating.

The standard guarantees both for automatic variables and static variables as long as one doesn't do bad things with the objects. However, it does not guarantee either for objects allocated from the free store.

Even for automatic variables, a crafty programmer can subvert the intention through pointer manipulation and explicitly calling the destructor through a pointer.

In addition, the wrong destructor will be called when delete-ing a base class pointer when the base class does not have a virtual destructor. This will be a programming error, not the result of intention to subvert.

Example:

struct Base
{
   int b;
};

struct Derived : virtual Base
{
   float d;
};


int main()
{
   {
      Derived d1; // Not a problem.
   }

   {
      Derived d1;
      Derived* ptr = &d1;
      delete ptr;    // Bad. The programmer subverts the program.
                     // Must not use delete.
   }

   {
      Derived* d2 = new Derived; // The destructor does not get called automatically.
   }

   {
      Derived* d2 = new Derived;
      delete d2;   // OK. The proper destructor gets called.
   }

   {
      Derived* d2 = new Derived;
      Base* ptr = d2;
      delete ptr;  // Programmer error. The wrong destructor gets called.
   }
}
Community
  • 1
  • 1
R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • I should perhaps clarify, then, that I'm not interested in cases where the programmer is being crafty--only in cases where the compiler author is. Let's assume that there's no inheritance involved. – Reid Rankin Aug 15 '19 at 20:47
  • @ReidRankin, what you asking for is guaranteed for automatic variables and `static` variables. It's not guaranteed for objects from the free store. The last use case in my answer is tricky -- it's a programming error without the intention to subvert. – R Sahu Aug 15 '19 at 20:51
-1

As mentioned by Nathan Oliver, the standard states that:

[...] 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]).

Compilers respect this, and there are objects (similar to the one you describe) for which it must hold true. Consider std::mutex. A mutex cannot be copied or moved, and the reason for this is that it must remain at the same location in memory for the duration of it's lifetime in order to work.

So how does copy/move elision work?

Copy/move elision works by creating the object where it needs to go. It's that simple.

We can see this behavior for ourselves:

#include <iostream>

struct Foo {
    Foo() {
        std::cout << "I am at " << (void*)this << '\n';
    }
    // Delete copy and move, to ensure it cannot be moved
    Foo(const Foo&) = delete;
    Foo(Foo&&) = delete;
};

Foo getFoo() {
    return Foo(); 
}

int main() {

    Foo* ptr = new Foo(getFoo()); 

    std::cout << "Foo ptr is at " << (void*)ptr << '\n';
    delete ptr; 
}

This code outputs:

I am at 0x201ee70
Foo ptr is at 0x201ee70

And we see that Foo remains at the same location for the duration of it's lifetime, without ever being copied or moved, even though it's being created in dynamically allocated memory.

How does the compiler know where to create an object?

If a function returns a type that is not trivially copyable, then that function takes an implicit parameter representing the memory address where it's supposed to construct the return value.

Alecto Irene Perez
  • 10,321
  • 23
  • 46