Sep 18

Memory Management in C++

In C++, there are three regions of memory in our program: code, the call stack, and the heap.

The call stack consists of one frame per currently executing function. The frame for a function stores the contents of the local variables of the function. So for example,

void foo(int a) {
    int b = a + 50;
    StringArrayStack s;
    //code here...
}

When foo is called, C++ puts a frame for foo on the call stack. This frame consists of memory locations for a, b, and s. Note that since the struct StringArrayStack consists of several private properties, the space on the call stack for s is enough to hold all the variables for the structure. The call-stack is managed by C++, and when a function returns its frame is removed from the call-stack. This means C++ considers that memory as unused and will re-use it for another frame, which in turn means that as programmers we can no longer use that memory. In other words, anything put on the call stack is only valid for the life of the function.

To support creating data values which live longer than a single function, C++ provides the heap. The heap is a region of memory C++ requests from the kernel, and then C++ provides commands to allocate memory from the heap. Here allocate means we request some memory from the heap using the new statement and C++ finds some unused portion of the heap and gives us a pointer to it. C++ will then consider that memory as used and not give it out again, until we notify C++ that we are no longer using that memory using the delete statement.

A major task as C++ programmers is therefore what we call memory management, which is making sure that:

  1. We always call delete on memory we allocate once we are done with it.
  2. We never use any memory after we have called delete on it.

This can be somewhat complicated because multiple places in our program could have pointers to the same memory (we will see examples of this soon), and it is sometimes hard to determine when all users are no longer using the memory. To assist with this task, there are a couple of C++ features:

Structs can have constructors and destructors. These are methods of the struct which are called automatically from C++. In the StringArrayStack, they looked like

struct StringArrayStack {
    private:
        //...
    public:
        //constructor
        StringArrayStack() {
            n = 0;
            array_size = 1;
            a = new string[1];
        }

        //destructor
        ~StringArrayStack() {
            delete[] a;
        }

The constructor looks like a method, but it has the same name as the struct and does not have a return value. The desctructor looks like a method, but does not have a return value and is the name of the struct prefixed by tilde. The constructor will be called by C++ whenever it creates space for a StringArrayStack, either on the call stack or the heap. For example,

void foo(int a) {
    StringArrayStack s;

    if (a > 10) {
        //The following code is problematic (see below), but is just here for an example
        StringArrayStack *ptr = new StringArrayStack();
        ///some code here
        delete ptr;
    }
}

When the initial frame for foo is created and space for s is reserved, C++ will execute the constructor and give the constructor a this pointer to the memory in the call-stack. Similarly, when the expression new StringArrayStack() is executed, it will allocate space in the heap for the structure and then call the constructor.

The destructor is executed by C++ whenever the memory for the structure is being reclaimed. So the delete ptr statement will first cause C++ to call the destructor, and then after that it will mark that the memory is now unused and can be potentially given out to the next call to new. Similarly, when the foo function returns, C++ will remove the frame for foo. But before removing the frame for foo, C++ will call the destructor for s, since the memory is being reclaimed.

In modern C++, it is very strongly recommended to manage memory as follows: First, always use the call stack if possible since it is automatically cleaned up Secondly, if you must use the heap, use a memory management technique called RAII or Resource Aquisition is Initialization. This comment is a good overview of RAII. Essentially, the technique is to tie the allocation and deallocation of data to the call stack, even if data lives in the heap. For example, in the foo function above which declares StringArrayStack s; on the call stack, even though the StringArrayStack constructor dynamically allocates memory for the array in the heap, this array heap memory is still tied to the call stack, since it is allocated in the constructor and deallocated in the destructor. Since C++ automatically calls the destructor for s when foo returns, the array's memory will be deallocated exactly when s is reclaimed.

The allocation with new inside the if (a > 10) block inside foo is an example of a non-RAII technique and actually has a bug. It is not RAII because the allocation and de-allocation is not tied to the call stack. For the bug, consider the possibility that the code inside the if (a > 10) block throws an exception. If it does, the delete ptr statement will not execute and memory will leak.

Sometimes it is helpful to allocate memory in the heap directly inside a function using new, but unlike the example above in the foo function, we should follow RAII and tie the memory allocation to the call-stack. To do so, we need to create a struct which will allocate the memory during the constructor and free the memory during the destructor. Thankfully, such a structure has already been written for us called a smart pointer. Initially, smart pointers were only in boost (boost smart pointers). Boost is almost a requirement for writing modern C++, since it contains many, many "best practices" utilities. When programming, you want to set yourself up for falling into the pit of success, which is something that C++ does not do well. The Boost libraries are an attempt to fix this failing of C++, by providing a large number of utilities that if you use them have you fall into the pit of success. Personally, I believe that this design issue of not providing a means to fall into the pit of success will eventually cause C++ to very gradually fade out of use and is why so many people are excited for languages like Rust.

Back to smart pointers, everyone considered them so vital to proper use of C++ that they were added directly into the language in C++11, the revision of C++ released in 2011. See shared_ptr for a reference. We could re-write the above foo function to instead use shared_ptr as

void foo(int a) {
    StringArrayStack s;

    if (a > 10) {
        std::shared_ptr<StringArrayStack> ptr = std::make_shared<StringArrayStack>();
        ///some code here using ptr
    }
}

The important thing to remember about a shared_ptr is, to quote the reference:

std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer.
Several shared_ptr objects may own the same object. The object is destroyed and its memory
deallocated when either of the following happens:
    * the last remaining shared_ptr owning the object is destroyed. 
    * the last remaining shared_ptr owning the object is assigned another pointer via operator= or reset(). 

It is not mentioned, but the object (also struct) they are talking about will live on the heap. The nice thing about shared_ptrs is that they can be used to keep structs around longer than a single function but still tie their lifetime to the call stack. For example,

#include <memory>
#include <iostream>
using namespace std;

struct MyTestStruct {
    private:
        int a;

    public:
        MyTestStruct() {
            a = 50;
            cout << "MyTestStruct constructor!" << endl;
        }

        ~MyTestStruct() {
            cout << "MyTestStruct destructor!" << endl;
        }

        void set_a(int newA) {
            a = newA;
        }

        int get_a() {
            return a;
        }
};


shared_ptr<MyTestStruct> foo(int x) {
    cout << "Starting foo function for x = " << x << endl;

    shared_ptr<MyTestStruct> ptr = make_shared<MyTestStruct>();
    ptr->set_a(x + 1000);

    cout << "About to return from foo" << endl;
    return ptr;
}

void bar() {
    cout << "Starting bar!" << endl;

    shared_ptr<MyTestStruct> test1 = foo(3);
    shared_ptr<MyTestStruct> test2 = foo(8);

    cout << "test1 a " << test1->get_a() << endl;
    cout << "test2 a " << test2->get_a() << endl;

    cout << "About to return from bar!" << endl;
}

int main() {
    cout << "Starting main" << endl;

    bar();

    cout << "About to return from main" << endl;
}

You should run the above code and see the result. (For me, I needed to use g++ -std=c++11 test.cpp to compile, may vary depending on your compiler version.) In particular, notice where in the sequence the destructors are running. That is, the structures live until the end of the bar function. This means that in the middle of the bar function, we can use the structure for whatever purposes, including passing it to other functions and so on. When the bar function returns, the structure will be automatically reclaimed.

Exercises

No specific exercises, but you should execute the above test code and make sure you understand how each line is printed.