Smart Pointers in C++11

C++11 introduced smart pointers as part of the standard library, inside the <memory> header file. Smart pointers reduce some of the manual programming work associated with memory management. They provide automated ways of detecting when to delete pointers, and try to protect developers from introducing memory leaks. Here are some of the core concepts and usages I've seen so far:

Unique Pointers

Unique pointers are pretty easy, they make sure that there is only one reference to the data at a time and that everything is cleaned up properly when the pointer goes out of scope. You cannot make a copy of a unique pointer, the compiler won't allow it. After a unique pointer is created, you can treat it essentially exactly like a vanilla pointer.

Initialization

Creating a unique pointer is pretty straight forward, either by constructing it with a pointer value or using the std::make_unique function which will do the construction for us. The make_unique function is preferrable to avoid some subtle memory leaks in specific situations. You can learn more here.

#include <memory>
#include <string>
#include <iostream>

using namespace std;

int main() {

    // Both of these statments are functionally identical,
    // but the latter is the preferred way.
    unique_ptr<string> constructed_name_ptr(new string("aarondevelops"));
    unique_ptr<string> generated_name_ptr = make_unique<string>("aarondevelops");
}

Passing It Along

If you want to "copy" a unique pointer, you'll need to utilize the std::move function to change ownership:

unique_ptr<string> name_ptr(new string("aaron"));

// Copy the pointer reference to duplicate_name
// After this operation, name_ptr is automatically deleted.
unique_ptr<string> duplicate_name(std::move(name_ptr));

Null Checking

Finally, you can use standard boolean checks to see if a unique_ptr still has a valid reference in it:

unique_ptr<string> name_ptr(new string("aaron"));
unique_ptr<string> duplicate_name(std::move(name_ptr));

// Will evaluate to false
if(name_ptr) {

    cout << "name_ptr contains data!" << endl;
}

// Evaluates to true, and prints.
if(duplicate_name) {

    cout << "duplicate_name contains data!" << endl;
}

Shared Pointers

Shared pointers are similar to unique pointers, but like the name suggests, they allow us to share them. Internally, shared pointers will keep a count of how many references there are, and then automatically delete the data when there aren't anymore references. Kind of like a rudimentary garbage collector. Let's take a look how we can use them.

Creation and Usage

Creating shared pointers is nearly identical to unique pointers, the real difference in these is how we use them. Say we have some method which creates a pointer and returns it. We want to continue using that reference, perhaps even passing it along to other functions without continuing to std::move it like a unique pointer. That's where shared pointers shine:

#include <memory>
#include <string>
#include <iostream>

using namespace std;

void greet_user();
shared_ptr<string> get_name();
void print_greeting(shared_ptr<string> name);

int main() {

    greet_user();

    // At this point, all references to the shared_ptr for 
    // the user name have fallen out of scope. The ref count
    // for that shared_ptr would fall to zero, and the 
    // underlying data would be deleted. No cleanup needed!
    
    return 0;
}

void greet_user() {

    // Once the shared_ptr containing the name is returned,
    // it is assigned to a new pointer and the reference count
    // is incremented by one.
    shared_ptr<string> user_name = get_name();
    print_greeting(user_name);

    // After printing, the copy that was made for the print_greeting
    // function falls out of scope and is deleted, and the ref count
    // drops back to only one (user_name).
    cout << "Greeting printed and return, ref count: " 
            << user_name.use_count() << endl;
}

shared_ptr<string> get_name() {

    string name("");
    cout << "Please enter your name:" << endl;
    cin >> name;

    // At this point, we create the shared pointer.
    // The internal ref count is one.
    shared_ptr<string> name_ptr = make_shared<string>(name);
    cout << "User name pointer created, reference count: " 
            << name_ptr.use_count() << endl;

    // This method will return, and the currently allocated shared_ptr (name_ptr)
    // will fall out of scope, be deleted and decrease the reference count by one.
    return name_ptr;
}

void print_greeting(shared_ptr<string> name) {

    // CPP is pass-by-value, meaning the shared_ptr
    // is copied into the new reference "name" here.
    // At this point, we are at a total count of two.
    cout << "User name reference passed into print_greeting, "
            "total reference count: " << name.use_count() << endl;
    cout << "Hello, " << *name << "!" << endl;
}

That is quite a bit of information to digest! But let's take it a step at a time to understand what is happening here:

  1. In the main function, we call the greet_user() function that kicks off our example chain of events.
  2. greet_user() first calls get_name(), which is what creates the first shared_ptr. Remember, this is just like a vanilla pointer with some extra niceities on top.
  3. After getting back the pointer to the user's name, the greet_user() function passes that along to print_greeting(). So now we have two "pointers" in memory. The one that was returned by get_name() and the one that was copied to give to print_greeting().
  4. The print_greeting() function returns, and shortly after so does the greet_user() function. With these two falling out of scope, so does all our pointers to the user name data. The last shared_ptr alive realizes this as it's being deleted, and so it also deletes the data! No memory leaks, and no manual management by the programmer!

Conclusion

The <memory> library provided in C++11 gives us some useful data structures to manage memory without a lot of extra programming effort. These are very useful features that every developer should get acquainted with and use as much as possible.