Object Destruction in C++

Revive object from destructor in C++?

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

Destruction of returned object

According to the C++17 standard [except.ctor]/2:

If an exception is thrown during the destruction of temporaries or local variables for a return statement, the destructor for the returned object (if any) is also invoked. The objects are destroyed in the reverse order of the completion of their construction.

Then there is an illustrative example (which you modified slightly) to demonstrate: after the Y destructor throws, the next thing that happens is that the A(100) object should be destroyed, so you should see a destruction message.

The posted output indicates a compiler bug. This is very similar to the issue reported as LLVM bug 12286 and gcc bug 33799.

The latter is marked as being fixed in GCC 10 (after initially being reported in 2007!). However, testing with the version 10.0.1 20200418 (experimental) on Godbolt: even though the test case in the 33799 bug report is fixed, the code in this question remains unfixed. I added a comment to that bug report about this example.

Object destruction in C++

In the following text, I will distinguish between scoped objects, whose time of destruction is statically determined by their enclosing scope (functions, blocks, classes, expressions), and dynamic objects, whose exact time of destruction is generally not known until runtime.

While the destruction semantics of class objects are determined by destructors, the destruction of a scalar object is always a no-op. Specifically, destructing a pointer variable does not destroy the pointee.

Scoped objects

automatic objects

Automatic objects (commonly referred to as "local variables") are destructed, in reverse order of their definition, when control flow leaves the scope of their definition:

void some_function()
{
Foo a;
Foo b;
if (some_condition)
{
Foo y;
Foo z;
} <--- z and y are destructed here
} <--- b and a are destructed here

If an exception is thrown during the execution of a function, all previously constructed automatic objects are destructed before the exception is propagated to the caller. This process is called stack unwinding. During stack unwinding, no further exceptions may leave the destructors of the aforementioned previously constructed automatic objects. Otherwise, the function std::terminate is called.

This leads to one of the most important guidelines in C++:

Destructors should never throw.

non-local static objects

Static objects defined at namespace scope (commonly referred to as "global variables") and static data members are destructed, in reverse order of their definition, after the execution of main:

struct X
{
static Foo x; // this is only a *declaration*, not a *definition*
};

Foo a;
Foo b;

int main()
{
} <--- y, x, b and a are destructed here

Foo X::x; // this is the respective definition
Foo y;

Note that the relative order of construction (and destruction) of static objects defined in different translation units is undefined.

If an exception leaves the destructor of a static object, the function std::terminate is called.

local static objects

Static objects defined inside functions are constructed when (and if) control flow passes through their definition for the first time.1
They are destructed in reverse order after the execution of main:

Foo& get_some_Foo()
{
static Foo x;
return x;
}

Bar& get_some_Bar()
{
static Bar y;
return y;
}

int main()
{
get_some_Bar().do_something(); // note that get_some_Bar is called *first*
get_some_Foo().do_something();
} <--- x and y are destructed here // hence y is destructed *last*

If an exception leaves the destructor of a static object, the function std::terminate is called.

1: This is an extremely simplified model. The initialization details of static objects are actually much more complicated.

base class subobjects and member subobjects

When control flow leaves the destructor body of an object, its member subobjects (also known as its "data members") are destructed in reverse order of their definition. After that, its base class subobjects are destructed in reverse order of the base-specifier-list:

class Foo : Bar, Baz
{
Quux x;
Quux y;

public:

~Foo()
{
} <--- y and x are destructed here,
}; followed by the Baz and Bar base class subobjects

If an exception is thrown during the construction of one of Foo's subobjects, then all its previously constructed subobjects will be destructed before the exception is propagated. The Foo destructor, on the other hand, will not be executed, since the Foo object was never fully constructed.

Note that the destructor body is not responsible for destructing the data members themselves. You only need to write a destructor if a data member is a handle to a resource that needs to be released when the object is destructed (such as a file, a socket, a database connection, a mutex, or heap memory).

array elements

Array elements are destructed in descending order. If an exception is thrown during the construction of the n-th element, the elements n-1 to 0 are destructed before the exception is propagated.

temporary objects

A temporary object is constructed when a prvalue expression of class type is evaluated. The most prominent example of a prvalue expression is the call of a function that returns an object by value, such as T operator+(const T&, const T&). Under normal circumstances, the temporary object is destructed when the full-expression that lexically contains the prvalue is completely evaluated:

__________________________ full-expression
___________ subexpression
_______ subexpression
some_function(a + " " + b);
^ both temporary objects are destructed here

The above function call some_function(a + " " + b) is a full-expression because it is not part of a larger expression (instead, it is part of an expression-statement). Hence, all temporary objects that are constructed during the evaluation of the subexpressions will be destructed at the semicolon. There are two such temporary objects: the first is constructed during the first addition, and the second is constructed during the second addition. The second temporary object will be destructed before the first.

If an exception is thrown during the second addition, the first temporary object will be destructed properly before propagating the exception.

If a local reference is initialized with a prvalue expression, the lifetime of the temporary object is extended to the scope of the local reference, so you won't get a dangling reference:

{
const Foo& r = a + " " + b;
^ first temporary (a + " ") is destructed here
// ...
} <--- second temporary (a + " " + b) is destructed not until here

If a prvalue expression of non-class type is evaluated, the result is a value, not a temporary object. However, a temporary object will be constructed if the prvalue is used to initialize a reference:

const int& r = i + j;

Dynamic objects and arrays

In the following section, destroy X means "first destruct X and then release the underlying memory".
Similarly, create X means "first allocate enough memory and then construct X there".

dynamic objects

A dynamic object created via p = new Foo is destroyed via delete p. If you forget to delete p, you have a resource leak. You should never attempt to do one of the following, since they all lead to undefined behavior:

  • destroy a dynamic object via delete[] (note the square brackets), free or any other means
  • destroy a dynamic object multiple times
  • access a dynamic object after it has been destroyed

If an exception is thrown during the construction of a dynamic object, the underlying memory is released before the exception is propagated.
(The destructor will not be executed prior to memory release, because the object was never fully constructed.)

dynamic arrays

A dynamic array created via p = new Foo[n] is destroyed via delete[] p (note the square brackets). If you forget to delete[] p, you have a resource leak. You should never attempt to do one of the following, since they all lead to undefined behavior:

  • destroy a dynamic array via delete, free or any other means
  • destroy a dynamic array multiple times
  • access a dynamic array after it has been destroyed

If an exception is thrown during the construction of the n-th element, the elements n-1 to 0 are destructed in descending order, the underlying memory is released, and the exception is propagated.

(You should generally prefer std::vector<Foo> over Foo* for dynamic arrays. It makes writing correct and robust code much easier.)

reference-counting smart pointers

A dynamic object managed by several std::shared_ptr<Foo> objects is destroyed during the destruction of the last std::shared_ptr<Foo> object involved in sharing that dynamic object.

(You should generally prefer std::shared_ptr<Foo> over Foo* for shared objects. It makes writing correct and robust code much easier.)

C++17 copy elision and object destruction

The symmetric case where the source outlives the target is when the prvalue is a parameter:

struct A {
static int *data;
A() {if(!refs++) data=new int(42);}
A(const A&) {++refs;} // not movable
~A() {if(!--refs) delete data;}
private:
static int refs;
};
int A::refs,*A::data;
int* f(A) {return A::data;}
A returnA();
int returnInt() {return *f(returnA());} // ok

Because the result of returnA() is a temporary, its lifetime extends to the end of the return statement’s full-expression. The implementation may identify it with f’s parameter, but may not destroy it when f returns, so the dereference in returnInt is valid. (Note that parameters may survive that long anyway.)

The adjustment in C++17 (along with such elision being guaranteed) is that if you (would) move the prvalue, it may be destroyed when the parameter is (since you shouldn’t be relying on its contents anyway). If that’s when f returns, the (ill-advised) code above becomes invalid if A is made movable.

C++ object creation and destruction

Animal *pDog2 = &Animal("Loki");

What happens here is:

  1. A temporary object of type Animal is created: Animal("Loki"). The output "Constructor of Loki called." is shown.
  2. The address of this temporary is assigned to pDog2.
  3. The temporary is destroyed at the end of the full expression: Animal *pDog2 = &Animal("Loki"); and the output "Destructor of Loki called." is shown.

After that pDog2 becomes a dangling pointer as the temporary it pointed to doesn't exist anymore.

Object creation and destruction order in C++

As already noted in the comments, objects of type A are also constructed via copy-construction when you pass them as arguments by value. In order to see this you can add a copy-constructor on your own:

A(const A& other)
: name(other.name)
{
cout << "A(" << name << ")::copy-constructor(), this = " << this << endl;
}

Sample output:

A(a1)::constructor(), this = 0xbff3512c
A(a2)::constructor(), this = 0xbff35130
A(a1)::copy-constructor(), this = 0xbff350e8
A(a1)::copy-constructor(), this = 0xbff35138
C(c1)::constructor()
A(a1)::destructor(), this = 0xbff350e8
A(a3)::constructor(), this = 0xbff3513c
B(b1)::constructor()
B(b1)::destructor()
A(a3)::destructor(), this = 0xbff3513c
C(c1)::destructor()
A(a1)::destructor(), this = 0xbff35138
A(a2)::destructor(), this = 0xbff35130
A(a1)::destructor(), this = 0xbff3512c

Try it online

As you can see, one copy-construction happens when you pass a1 as parameter to the constructor of c1 and a second one happens when this constructor initializes its member a. The temporary copy is destructed immediately afterwards while the member is destructed when c is destructed.

Edit:

Here you can read the exact rules when a copy-constructor is created.

In order to not create a default copy-constructor it is not sufficient to provide any user-defined constructor, it needs to be a copy/move-constructor.

Edit2:

Taken from C++14 standard (12.8 Copying and moving class objects):

7 If the class definition does not explicitly declare a copy constructor, one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted (8.4). The latter case is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor.

Avoid object destruction when initializing in local scope, C++

I thought of this just when I was finishing typing up the question. Use move-assignment.

Take the original code and add the following method to the class definition:

Foo & operator=(Foo && other) {
swap(v, other.v);
return *this;
}

This way, when constructing an object only to assign it to some other object, this method will be called and it will simply swap the values in the v members. The other object will be immediately destroyed, but with the "old" value of v.

Take care to assign some meaningless value to v by default, perhaps -1.

Now, the output of the program is

Foo default constructor with v = 0.
Foo constructor with v = 3.
Foo destructor with v = 0.
Foo destructor with v = 3.

which is exactly what I was looking for.



Related Topics



Leave a reply



Submit