49

Disclaimer: I know this is bad design, I am simply asking the question out of curiosity in order to try to obtain deeper knowledge of how the destructor works in C++.

In C#, one can write: GC.KeepAlive(this) in the destructor of a class (see edit below), and that would mean that the object will still be alive in memory even after the destructor call is complete.

Does the design of C++ allow reviving an object from the destructor similar to the C# algorithm described above?

Edit: As pointed out by an answer below, GC.ReRegisterForFinalize() is more closely related to the question than GC.KeepAlive(this).

MathuSum Mut
  • 2,765
  • 3
  • 27
  • 59
  • Just leave the destructor empty. – a06e Feb 17 '16 at 13:27
  • 1
    @becko How would that work? If all of the types in the class as POD or RAII then an empty destructor is all you ever have. – NathanOliver Feb 17 '16 at 13:28
  • That would still cause the object to be deleted. – MathuSum Mut Feb 17 '16 at 13:28
  • 2
    In c++, you may separate allocation/deallocation and construction/destruction (via *placement new*). – Jarod42 Feb 17 '16 at 13:29
  • @NathanOliver What I mean is that in C++, objects are gone only when they go out of scope. But at this point you won't be able to refer to it anyway. If you leave the destructor empty, then the object will still be in memory even after the destructor is called, as the OP wants. (As long as the destructor is called manually, not due to object being out of scope) – a06e Feb 17 '16 at 13:29
  • placement new on the 'this'? – MathuSum Mut Feb 17 '16 at 13:30
  • 17
    @becko No. Once the destructor is called the object is done. Even if the destructor is empty the memory the objects uses is returned to the system. An empty destructor will not stop the class from being destroyed. – NathanOliver Feb 17 '16 at 13:32
  • 1
    You should ask yourself: Is an object which you cannot access in any way still alive? even after the destructor is called, the memory containing the members of the object will most likely still have all the values (until it is reallocated by another call/move/whatever) but reading from there is undefined behaviour... – Falco Feb 17 '16 at 16:23
  • 1
    How to "revive" the object? Make a copy of it. In the destructor the object still exists so you can make its copies. The problem is when the class is a parent of another class - a destructor of its child was already executed so the child part doesn't exist anymore. – Marian Spanik Feb 18 '16 at 06:49
  • 1
    The job of the destructor is to *tear down* the object. So, even if you *could* somehow access it after destruction (which you cannot), the object is *allowed* to be in an *undefined* state at this point. *Because* C++ is deterministic with its destructors, RAII is strong: The backend objects are destroyed. The database connection and all file handles are closed. The object is *dead*. Whatever you do to revive it as a zombie, its brains are gone and it's going to be good for nothing.... – DevSolar Feb 18 '16 at 10:16

6 Answers6

106

The short answer is: no. C++ does not employ garbage collection, like Java or C#. When an object is destroyed, it's destroyed immediately. Gone for good. Joined the choir invisible. Pining for the fjords, etc...

And to say this over a couple of times in different words so that there is no possible weasily reinterpretation...

The destructor is invoked as part of object destruction. Object destruction consists of invoking the destructor and deallocating the memory that was used for the object itself. It's a single process, not two separate processes. While the destructor is running, the object still exists, for the destructor to use, but it exists on borrowed time. It's a foregone conclusion that the object is going to be vaporized as soon as the destructor returns. Once a destructor is invoked, the object is going to be destroyed, and nothing is going to change its fate.

Understand this: the reason why a destructor is being invoked is because either: the object was originally allocated on the heap with "new", and it's now being "delete"d. "delete" means "delete", not "delete maybe". So the object's getting deleted. Or, if the object was allocated on the stack, the execution thread exited the scope, so all objects declared in the scope are getting destroyed. The destructor is, technically, getting invoked as a result of the object being destroyed. So, the object is being destroyed. The End.

Having said that, C++ allows you to implement a custom allocator for your classes. If you feel like it, you can write your own custom memory allocation and deallocation functions that implement whatever functionality you want. Though these are never used for stack allocated objects (i.e. local variables).

Omnifarious
  • 54,333
  • 19
  • 131
  • 194
Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • 1
    So the destructor is called *after* the object is destroyed? – MathuSum Mut Feb 17 '16 at 13:28
  • 34
    The destructor is invoked as part of object destruction. Object destruction consists of invoking the destructor and deallocating the memory that was used for the object itself. It's a single process, not two separate processes. While the destructor is running, the object still exists, for the destructor to use, but it exists on borrowed time. It's a foregone conclusion that the object is going to be vaporized as soon as the destructor returns. Once a destructor is invoked, the object is going to be destroyed, and nothing is going to change its fate. – Sam Varshavchik Feb 17 '16 at 13:29
  • But if you do an assignment of a pointer of 'this' to an object, would that keep it alive? – MathuSum Mut Feb 17 '16 at 13:31
  • 3
    No. Assigment to this will replace the values of the object's members. But the object is still getting destroyed when the destructor finishes. Which is a foregone conclusion. You can't change the object's destiny. You could certainly allocate another object, unrelated to the object being destroyed, in the first object's destructor, but it will be a different, independent, object. – Sam Varshavchik Feb 17 '16 at 13:32
  • 7
    Understand this: the reason why a destructor is being invoked is because either: the object was originally allocated on the heap with "new", and it's now being "delete"d. "delete" means "delete", not "delete maybe". So the object's getting deleted. Or, if the object was allocated on the stack, the execution thread exited the scope, so all objects declared in the scope are getting destroyed. The destructor is, technically, getting invoked as a result of the object being destroyed. So, the object is being destroyed. The End. – Sam Varshavchik Feb 17 '16 at 13:40
  • Destroying an object usually only consists of calling the destructor. After that the memory manager will simply not mark the memory which the object occupies as in-use anymore. There is usually no actual deleting or destroying taking place. So if you save a pointer to `this` from the destructor in a global variable, you could probably still access the memory location and use the object as if it still was alive, but depending on your memory manager you could also crash or read garbage... – Falco Feb 17 '16 at 16:27
  • 5
    @Falco It *is* possible that you could destroy the object, then follow a pointer to the memory where it used to be and be lucky enough to read a valid object. It's also possible that you could follow a pointer to e.g. 0x8F3B2780, and just happen to read an identical valid object. However in both cases, there is absolutely *no* way to determine if it'll work without trying it. Both behaviors are equally undefined and neither should be in any way encouraged. If you want to access the object, *don't destroy it*. – Ray Feb 18 '16 at 03:27
  • 1
    The first sentence is a bit misleading as having a garbage collector does not necessarily imply having such special function the OP asked for. E.g., Java doesn’t have such functions. It has the `finalize()` thing which may add a squiggle to the object’s lifetime but can’t change the principle of reachability. And, obviously, other languages with garbage collection don’t need to have such a feature, so supporting life time extensions is not connected with having gc or not. – Holger Feb 18 '16 at 08:59
55

You are actually misrepresenting what GC.KeepAlive does in .NET. It is not to be used in a destructor of an object to keep that object from being destructed -- actually, GC.KeepAlive() is empty and has no implementation. See the .NET source code here.

It makes sure that the object passed as a parameter is not garbage collected before the call to GC.KeepAlive happens. The object passed to KeepAlive as a parameter can be garbage collected immediately after the call to GC.KeepAlive. Since KeepAlive has no actual implementation, this happens purely based on the fact that the compiler has to maintain a reference to the object to be passed as a parameter to KeepAlive. Any other function (that is not inlined by the compiler or runtime) taking the object as a parameter could be used instead as well.

NineBerry
  • 26,306
  • 3
  • 62
  • 93
  • 7
    In .net objects are kept alive (prevented from being garbage collected) by having references to them. Threads play no role here. – NineBerry Feb 17 '16 at 13:51
  • 1
    An object is kept alive if it is referenced in another thread (ie. kept alive) – MathuSum Mut Feb 17 '16 at 13:52
  • 1
    What I'm wondering about is that comment on `KeepAlive`: "[...] this can cause subtle ----s with the finalizer thread.". What's with the dashes, censorship for "FU's"? Are developers not allowed to mention the word "bug" in docs? What? – CompuChip Feb 17 '16 at 14:59
  • 2
    @CompuChip that's a PITA in the referencesource site. See [here](http://stackoverflow.com/a/30631947/3764814) for an explanation. – Lucas Trzesniewski Feb 17 '16 at 19:42
  • 2
    Actually the only reason `GC.KeepAlive` works is because it is special cased by the JIT, otherwise after inlining the function, the JIT would see that it wouldn't need to keep the reference.. – Voo Feb 18 '16 at 09:45
  • @Voo: Yes, that is what the [MethodImplAttribute(MethodImplOptions.NoInlining)] does in the .NET source code of the method. I added some detail to my answer. – NineBerry Feb 18 '16 at 09:46
9

Here's an idea:

C* gPhoenix= nullptr;

C::~C ()
{
gPhoenix= new C (*this);  // note: loses any further-derived class ("slice")
}

Now if the objects involved (bases or members) really do have destructors that do something, this runs into a problem if you delete gPhoenix; so you'll need more elaborate mechanisms depending on what it's really trying to accomplish. But you don't have any real goals, just curious exploration, so pointing this out should suffice.

When the body of the destructor is called, the object is still perfectly good. It appears perfectly vital and normal as you make normal member function calls from within the destructor.

The memory owning the object will be reclaimed, so you can't stick around in-place. And after leaving the body, other destruction takes place automatically and can't be interfered with. But, you can duplicate the object before that happens.

JDługosz
  • 5,592
  • 3
  • 24
  • 45
  • 1
    One thing to note is, that any subclass destructors have already been called, if object was actually an instance of a subclass. In that case things may get hairy, depending on what the subclass has done. At best you get a valid copy of instance of currently destructed class, and just lose whatever the subclass added. – hyde Feb 17 '16 at 16:57
  • @Deduplicator Destructor call order is from subclass to superclass, reverse of constructor call order (quite naturally, when you think about it, subclass stuff depends on valid superclass stuff under it). So I think I wrote correctly above. – hyde Feb 17 '16 at 18:42
  • @hyde I always swap superclass and subclass. Probably because the superclass is a subobject. – Deduplicator Feb 17 '16 at 18:57
  • 2
    superclass, subclass: Well, neither term is used in C++, so don't worry about it. Use the nomenclature from the Standard and Stroustrup before it: *derived class* (and *most derived class*), *base class*. Then feel free to use terms like subclass in a general mathematical sense as you define in-context locally. Note that base and derived is much harder to get mixed up! – JDługosz Feb 17 '16 at 21:51
6

As has already been pointed out, GC.KeepAlive doesn't do that.

As long as .NET goes, it is possible to resurrect from the finalizer using GC.ReRegisterForFinalize, you can still get a reference to it if you have a WeakReference or GCHandle tracking ressurection, or just give this to something outside the class. Doing that will abort the destruction.

That is a old trick to detect garbage collection in .NET 2.0 no longer relevant, but still works (kinda, garbage collection can now be partial, and done in parallel with other threads).

Emphasis should be put on the fact that on .NET you are using a finalizer, which runs before destruction, and can prevent it. So, while it is technically correct that you can't recover an object after destruction - in any language - you can get close to the behaviour you describe in .NET, except using GC.ReRegisterForFinalize instead.


On C++, you have already been given the correct answer.

Community
  • 1
  • 1
Theraot
  • 31,890
  • 5
  • 57
  • 86
4

That's not possible in any language.

Your understanding is a bit off. GC.KeepAlive will mark the object as not collectible by the garbage collector. This will prevent the garbage collection strategy from destroying the object and it's useful if the object is used in unmanaged code where the garbage collector can't keep track of usage. This doesn't mean that the object is in memory after destruction.

Once an object begins the destruction the code will free resources (memory, file handlers, network connections). The order is usually from the deepest derived class back to the base class. If something in the middle were to prevent the destruction there's no guarantee that these resources could be re-acquired and the object would be in an inconsistent state.

What you want more likely is to have a std::shared_ptr that keeps track of copies and references and only destroys the object once nobody needs it anymore.

Sorin
  • 11,863
  • 22
  • 26
  • 4
    Note my answer that GC.KeepAlive() only protects the object from being garbage collected BEFORE the call to KeepAlive(), not after. – NineBerry Feb 17 '16 at 13:53
  • 4
    Your understanding of `GC.KeepAlive` is wrong. Also, an object needs to be *pinned* for usage in unmanaged code, at which point it's not collectible anyway. – Lucas Trzesniewski Feb 17 '16 at 19:47
3

In case it helps, the destructor function and memory allocation are distinct.

The destructor is just a function. You can call it explicitly. If it does nothing destructive, then calling it again (e.g. when the object goes out of scope or is deleted) is not necessarily problematic, although it would be very strange; possibly there is a section dealing with this in the standard. See example below. For example, some STL containers explicitly call the destructor since they manage object lifetimes and memory allocation separately.

Typically the compiler will insert code to call the destructor when an auto variable goes out of scope, or a heap allocated object is destroyed with delete. This deallocation of memory cannot be tampered with inside the destructor.

You can take charge of memory allocation by providing additional implementations of the new operator, or using existing ones like placement new, but the general default behaviour is that the compiler will call your destructor, and that's a chance to tidy up. The fact that some memory will subsequently be cleared up is outside the control of the destructor.

#include <iostream>
#include <iomanip>

namespace test
{
  class GotNormalDestructor
  {
    public:
      ~GotNormalDestructor() { std::wcout << L"~GotNormalDestructor(). this=0x" << std::hex << this << L"\n"; }
  };

  class GotVirtualDestructor
  {
    public:
      virtual ~GotVirtualDestructor() { std::wcout << L"~GotVirtualDestructor(). this=0x" << std::hex << this << L"\n"; }
  };

  template <typename T>
  static void create_destruct_delete(wchar_t const name[])
  {
    std::wcout << L"create_destruct_delete<" << name << L">()\n";
    {
      T t;
      std::wcout << L"Destructing auto " << name << L" explicitly.\n";
      t.~T();
      std::wcout << L"Finished destructing " << name << L" explicitly.\n";
      std::wcout << name << L" going out of scope.\n";
    }
    std::wcout << L"Finished " << name << L" going out of scope.\n";
    std::wcout << L"\n";
  }

  template <typename T>
  static void new_destruct_delete(wchar_t const name[])
  {
    std::wcout << L"new_destruct_delete<" << name << L">()\n";
    T *t = new T;
    std::wcout << L"Destructing new " << name << L" explicitly.\n";
    t->~T();
    std::wcout << L"Finished destructing new " << name << L" explicitly.\n";
    std::wcout << L"Deleting " << name << L".\n";
    delete t;
    std::wcout << L"Finished deleting " << name << L".\n";
    std::wcout << L"\n";
  }

  static void test_destructor()
  {
    {
      std::wcout << L"\n===auto normal destructor variable===\n";
      GotNormalDestructor got_normal;
    }

    {
      std::wcout << L"\n===auto virtual destructor variable===\n";
      GotVirtualDestructor got_virtual;
    }

    {
      std::wcout << L"\n===new variables===\n";
      new_destruct_delete<GotNormalDestructor>(L"GotNormalDestructor");
      new_destruct_delete<GotVirtualDestructor>(L"GotVirtualDestructor"); 
    }

    {
      std::wcout << L"\n===auto variables===\n";
      create_destruct_delete<GotNormalDestructor>(L"GotNormalDestructor");
      create_destruct_delete<GotVirtualDestructor>(L"GotVirtualDestructor");
    }

    std::wcout << std::endl;
  }
}

int main(int argc, char *argv[])
{
  test::test_destructor();

  return 0;
}

Sample output

===auto normal destructor variable===
~GotNormalDestructor(). this=0x0x23fe1f

===auto virtual destructor variable===
~GotVirtualDestructor(). this=0x0x23fe10

===new variables===
new_destruct_delete<GotNormalDestructor>()
Destructing new GotNormalDestructor explicitly.
~GotNormalDestructor(). this=0x0x526700
Finished destructing new GotNormalDestructor explicitly.
Deleting GotNormalDestructor.
~GotNormalDestructor(). this=0x0x526700
Finished deleting GotNormalDestructor.

new_destruct_delete<GotVirtualDestructor>()
Destructing new GotVirtualDestructor explicitly.
~GotVirtualDestructor(). this=0x0x526700
Finished destructing new GotVirtualDestructor explicitly.
Deleting GotVirtualDestructor.
~GotVirtualDestructor(). this=0x0x526700
Finished deleting GotVirtualDestructor.


===auto variables===
create_destruct_delete<GotNormalDestructor>()
Destructing auto GotNormalDestructor explicitly.
~GotNormalDestructor(). this=0x0x23fdcf
Finished destructing GotNormalDestructor explicitly.
GotNormalDestructor going out of scope.
~GotNormalDestructor(). this=0x0x23fdcf
Finished GotNormalDestructor going out of scope.

create_destruct_delete<GotVirtualDestructor>()
Destructing auto GotVirtualDestructor explicitly.
~GotVirtualDestructor(). this=0x0x23fdc0
Finished destructing GotVirtualDestructor explicitly.
GotVirtualDestructor going out of scope.
~GotVirtualDestructor(). this=0x0x23fdc0
Finished GotVirtualDestructor going out of scope.
WaffleSouffle
  • 3,293
  • 2
  • 28
  • 27