When exactly are objects destroyed in C++, and what does that mean? Do I have to destroy them manually, since there is no Garbage Collector? How do exceptions come into play?
(Note: This is meant to be an entry to Stack Overflow's C++ FAQ. If you want to critique the idea of providing an FAQ in this form, then the posting on meta that started all this would be the place to do that. Answers to that question are monitored in the C++ chatroom, where the FAQ idea started out in the first place, so your answer is very likely to get read by those who came up with the idea.)
A destructor is a member function that is invoked automatically when the object goes out of scope or is explicitly destroyed by a call to delete . A destructor has the same name as the class, preceded by a tilde ( ~ ). For example, the destructor for class String is declared: ~String() .
There is no such thing called 'constructors' and 'destructors' in C programming language or in structured languages, although there is no boundaries on defining such functions which act like them. You need to make functions which act like the constructors and destructors and then call them manually.
Destructors are usually used to deallocate memory and do other cleanup for a class object and its class members when the object is destroyed. A destructor is called for a class object when that object passes out of scope or is explicitly deleted.
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.
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.
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.
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.
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 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.
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;
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".
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:
delete[]
(note the square brackets), free
or any other meansIf 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.)
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:
delete
, free
or any other meansIf 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.)
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With