Object destruction in C++
Asked Answered
S

2

80

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

Subserve answered 19/6, 2011 at 14:43 Comment(19)
Who voted for close this topic? I don't see any reason. In fact, its surely a good FAQ. +1Goldenrod
@Nawaz: however, as far as I know, SO policy only talks about good questions, not "good FAQ entries*. ;)Unaccountable
@jalf: FAQ is also a form of question and answer. FAQ itself means Frequently Asked Questions. Besides, there are too many such topics, and many of them are good. Ask them who benefited reading them. Ask me. :DGoldenrod
@Nawaz: But is this a good question (which is the criteria on SO), if it wasn't asked by someone who actually needed to know the answer? If it is asked so commonly, why did @Fred need to ask it himself just so he could provide an answer? My point is simply that if you play by the rules, then "it is a good FAQ" doesn't matter, what matters is "is this a good question", and I, at least, judge this based on whether it is likely to get the OP the answer he needs (which is void in this case because the OP knows the answer), and on whether others with the same problem are likely to find it.Unaccountable
Whether the clique who posts FAQ questions and knows the answers already considers it to be a "good FAQ" is irrelevantUnaccountable
To go by strict rules, was'nt the idea of C++-Faq originated to be a repo of most frequently asked Q's & not just good answers or well explained content? Then the Q arises is has this Q been asked so many times before to be a FAQ? Having said so the content is excellent and top notch Q is does it fit in with the idea of FAQ.Anglin
@jalf: what about the clique who, contrary to what Joel and Jeff have said, think that whether the questioner knows the answer or not has any bearing on whether it's a good/worthwhile question? Apparently it's very relevant what that clique thinks, since it only takes 5 votes to close a question, and then unless the people who upvoted the question come back to check, they'll never discover that a smaller group of people has won the argument. It's a good question if (a) people can find it by search, or (b) people can be referred to it when they ask similar questions, and the answers help them.Cholla
"If it is asked so commonly, why did @Fred need to ask it himself" - usually because particular people who don't know about this stuff don't think to ask, "when are objects destroyed", instead they ask some specific question about their particular code, to which the answer is, "you need to understand the lifetime of your objects". So the specific questions have too many details that are irrelevant to other questioners on the same issue. I don't know if that's the case here, but it is for the FAQ questions that I've used in the past to refer questioners to.Cholla
Good series, keep it up bro ;)Esprit
@jalf: there is nothing wrong with self-answering questions. In fact, it's encouraged.Beastings
This question added to answer this prematurely closed question: stackoverflow.com/q/11712020/14065Whensoever
The irony comes when this one gets closed as a dupe of that one. I like your answer btw. I hope the other asker sees it.Gunpaper
Shameless rep-grab. No, really, nice job. Good example that easy questions don’t have to be stupid.Randy
@KonradRudolph: Only needs 5 edits to make a community question. More of a result of being disgusted at people being unhelpful to new users.Whensoever
I really appreciate this. it is not the first time in so I'm seeing humble qns get closedTiossem
stackoverflow.com/q/11712020/14065 is now reopened. Shall we close this as a dupe now and ask a mod to merge the answer?Kephart
@FredOverlow: If that's indeed a dupe of the FAQ, then so is the one this is a dupe of...Kephart
This is a great question/answer. Thank you! I had one more question, regarding lifetime of automatic objects returned from the scope in which they were created. It seems that the compiler automatically tracks the lifetime of the object, and only destroys it when there are no more references to it. ``` Test f1() { cout << "In f1()\n"; Test t; // local object cout << "leaving f1()\n"; return t; } ``` If you assign the result of f1() to something in the calling scope, it will "escape" to the calling scope, otherwise it will be destructed on leaving f1()Joanajoane
@RoganDawes What you observe is C++17 "guaranteed copy elision". It used to be a compiler optimization for decades, now it is required by the language specification.Subserve
S
101

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

Subserve answered 19/6, 2011 at 14:43 Comment(7)
there is no mention of the order of destruction for static local variables vs static global variablesOminous
I suggest describing in detail the case where you have an automatic object in a non-void function.Stash
@FredOverflow Regarding "You should generally prefer std::vector<Foo> over Foo* for dynamic arrays." - Actually, most of the times std::deque<Foo>is a better choice than std::vector<Foo>, but this is another discussion.Intracranial
@MihaiTodor I have seen that preached quite a lot, but it seems in practice, everybody uses std::vector instead of std::deque. Only speaking for myself here, but I like my memory to be contiguous.Subserve
@FredOverflow Hopefully, the users will remember to resize() it accordingly before inserting elements in it :)Intracranial
@MihaiTodor No need to worry, std::vector::push_back has amortized constant time.Subserve
@FredOverflow I would be concerned about reallocation in some scenarios, especially if it's a vector of complex objects, since you don't get any warning when it occurs. Also, I found Herb Sutter's advice convincing: gotw.ca/gotw/054.htm so I'm not sure if it's worth worrying about the slight performance hit of having non-contiguous memory.Intracranial
W
39

The destructor of an object is called automatically when the object lifespan ends and it is destroyed. You should not usually call it manually.

We will use this object as an example:

class Test
{
    public:
        Test()                           { std::cout << "Created    " << this << "\n";}
        ~Test()                          { std::cout << "Destroyed  " << this << "\n";}
        Test(Test const& rhs)            { std::cout << "Copied     " << this << "\n";}
        Test& operator=(Test const& rhs) { std::cout << "Assigned   " << this << "\n";}
};

There are three (four in C++11) distinct types of object in C++ and the type of the object defines the objects lifespan.

  • Static Storage duration objects
  • Automatic Storage duration objects
  • Dynamic Storage duration objects
  • (In C++11) Thread Storage duration objects

Static Storage duration objects

These are the simplest and equate to global variables. The lifespan of these objects is (usually) the length of the application. These are (usually) constructed before main is entered and destroyed (in the reverse order of being created) after we exit main.

Test  global;
int main()
{
    std::cout << "Main\n";
}

> ./a.out
Created    0x10fbb80b0
Main
Destroyed  0x10fbb80b0

Note 1: There are two other type of static storage duration object.

static member variables of a class.

These are for all sense and purpose the same as global variables in terms of lifespan.

static variables inside a function.

These are lazily created static storage duration objects. They are created on first use (in a thread safe manor for C++11). Just like other static storage duration objects they are destroyed when the application ends.

Order of construction/destruction

  • The order of construction within a compilation unit is well defined and the same as declaration.
  • The order of construction between compilation units is undefined.
  • The order of destruction is the exact inverse of the order of construction.

Automatic Storage duration objects

These are the most common type of objects and what you should be using 99% of the time.

These are three main types of automatic variables:

  • local variables inside a function/block
  • member variables inside a class/array.
  • temporary variables.

Local Variables

When a function/block is exited all variables declared inside that function/block will be destroyed (in the reverse order of creation).

int main()
{
     std::cout << "Main() START\n";
     Test   scope1;
     Test   scope2;
     std::cout << "Main Variables Created\n";


     {
           std::cout << "\nblock 1 Entered\n";
           Test blockScope;
           std::cout << "block 1 about to leave\n";
     } // blockScope is destrpyed here

     {
           std::cout << "\nblock 2 Entered\n";
           Test blockScope;
           std::cout << "block 2 about to leave\n";
     } // blockScope is destrpyed here

     std::cout << "\nMain() END\n";
}// All variables from main destroyed here.

> ./a.out
Main() START
Created    0x7fff6488d938
Created    0x7fff6488d930
Main Variables Created

block 1 Entered
Created    0x7fff6488d928
block 1 about to leave
Destroyed  0x7fff6488d928

block 2 Entered
Created    0x7fff6488d918
block 2 about to leave
Destroyed  0x7fff6488d918

Main() END
Destroyed  0x7fff6488d930
Destroyed  0x7fff6488d938

member variables

The lifespan of a member variables is bound to the object that owns it. When an owners lifespan ends all its members lifespan also ends. So you need to look at the lifetime of an owner which obeys the same rules.

Note: Members are always destroyed before the owner in reverse order of creation.

  • Thus for class members they are created in the order of declaration
    and destroyed in the reverse order of declaration
  • Thus for array members they are created in order 0-->top
    and destroyed in the reverse order top-->0

temporary variables

These are objects that are created as the result of an expression but are not assigned to a variable. Temporary variables are destroyed just like other automatic variables. It is just that the end of their scope is the end of the statement in which they are created (this is usally the ';').

std::string   data("Text.");

std::cout << (data + 1); // Here we create a temporary object.
                         // Which is a std::string with '1' added to "Text."
                         // This object is streamed to the output
                         // Once the statement has finished it is destroyed.
                         // So the temporary no longer exists after the ';'

Note: There are situations where the life of a temporary can be extended.
But this is not relevant to this simple discussion. By the time you understand that this document will be second nature to you and before it is extending the life of a temporary is not something you want to do.

Dynamic Storage duration objects

These objects have a dynamic lifespan and are created with new and destroyed with a call to delete.

int main()
{
    std::cout << "Main()\n";
    Test*  ptr = new Test();
    delete ptr;
    std::cout << "Main Done\n";
}

> ./a.out
Main()
Created    0x1083008e0
Destroyed  0x1083008e0
Main Done

For devs that come from garbage collected languages this can seem strange (managing the lifespan of your object). But the problem is not as bad as it seems. It is unusual in C++ to use dynamically allocated objects directly. We have management objects to control their lifespan.

The closest thing to most other GC collected languages is the std::shared_ptr. This will keep track of the number of users of a dynamically created object and when all of them are gone will call delete automatically (I think of this as a better version of a normal Java object).

int main()
{
    std::cout << "Main Start\n";
    std::shared_ptr<Test>  smartPtr(new Test());
    std::cout << "Main End\n";
} // smartPtr goes out of scope here.
  // As there are no other copies it will automatically call delete on the object
  // it is holding.

> ./a.out
Main Start
Created    0x1083008e0
Main Ended
Destroyed  0x1083008e0

Thread Storage duration objects

These are new to the language. They are very much like static storage duration objects. But rather than living the same life as the application they live as long as the thread of execution they are associated with.

Whensoever answered 29/7, 2012 at 19:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.