New Age C++

C++ Pointers Get Smart

C++ pointers earned a bad reputation for causing memory leaks, but new smart pointers release memory as it stops being referenced.

Recent announcements made by some big players in the industry about C++ enablement -- like Google's Android NDK, the Apple-sponsored Clang compiler or the inclusion of C++ as a first-class citizen in the Windows 8 application tooling -- have raised eyebrows. But despite these developments, C++ still isn't seen as a "hot" or "modern" language by many; by others it's viewed as too complex.

The reality, though, is that C++ has been quietly evolving during the last decade, changing for the better. The biggest change is that many of its complexities have been overcome, turning it into a much simpler language.

In addition, the traditional advantages of C++ are making it more appealing to developers. For example, the interest in tablets is providing a tailwind for C++, since they're more limited than traditional PCs: They run on batteries that need to last, making every CPU cycle matter; leveraging the GPU is critical; and getting highly responsive applications -- a main advantage of native code -- is also a must.

This column's purpose is to show you how to use those advantages. I'll explain how coding in C++ looks today, and demystify old criticisms one at a time. Consider this your C++ classroom, where you'll learn about what this powerful language can do for your applications.

This first installment addresses the most often-mentioned issue regarding C++: memory management. Fortunately, that is no longer the stumbling block it once was, through a technique known as smart pointers. Smart pointers are now part of C++11 (the latest ISO C++ standard ratified and published last year.)

Let's see smart pointers in action: I'll define a class my_class, as in Listing 1.

This class exposes a constructor and a destructor, both tracing messages to let you know each time an instance is created or dropped. Each instance gets a unique id during construction, which is retrieved by calling get_id().

Here's when the fun begins: I'll define a function foo() containing three pointers to my_class dynamic instances.

// include this header to use C++ smart pointers.
#include <memory>

void foo() {
    shared_ptr<my_class> p1 = make_shared<my_class>(),
        p2 = p1,
        p3 = make_shared<my_class>();

    cout << "p1 points to instance #" << p1->get_id() << endl;
    cout << "p2 points to instance #" << p2->get_id() << endl;
    cout << "p3 points to instance #" << (*p3).get_id() << endl;
}

These pointers weren't declared as raw, C-styled ones, but as shared_ptr to my_class. Shared_ptr is a generic class (known as template class in C++) which models a traditional pointer by exposing the same operators:

  • Dereference (e.g., *p). In C/C++ jargon, dereference means "get the value pointed by" (pointer p in this case).
  • Selection. For example, p->get_id(), or "select member get_id() at the value pointed by p."

Shared_ptr also offers other value-added services that raw pointers don't. For instance, my main method contains only a call to the function foo():

int main(int argc, char* argv[])
{
    foo();
}

I run it and get:

Instance 1 is being created.
Instance 2 is being created.
p1 points to instance #1
p2 points to instance #1
p3 points to instance #2
Instance 2 is being destroyed.
Instance 1 is being destroyed.

Note the last two traces: The destructor was called for the two created instances, despite the fact that I didn't have any delete command. This happened when foo() returned, as the pointer variables lost visibility.

There are a few more details to note:

  • If I'd tried to explicitly delete a smart pointer, the program wouldn't have compiled. Delete only works with raw pointers. Smart pointers, instead, are expected to release memory implicitly.
  • shared_ptr<T> handles reference counting. In my code, both p1 and p2 point to the same instance. This instance was deleted once both pointers lost visibility -- not before -- to keep the pointers from becoming inconsistent.
  • I initialized these pointers by calling the template function make_shared<T>() with no arguments. This function creates an instance of T by calling its default constructor and returns a shared_ptr<T>. But what happens if you need the instance to be created using a constructor other than the default? Just give the alternative constructor parameters to the function make_shared, which will invoke the corresponding constructor.

The latest version of C++ repurposed the keyword auto to make it work like var in C#. Thus, I could have defined these smart pointers in foo() as:

void foo()
{
   auto p1 = make_shared<my_class>(),
        p2 = p1,
        p3 = make_shared<my_class>();

    cout << "p1 points to instance #" << p1->get_id() << endl;
    cout << "p2 points to instance #" << p2->get_id() << endl;
    cout << "p3 points to instance #" << (*p3).get_id() << endl;
}

The p1, p2 and p3 types are inferred at compile time, so the output is the same.

While shared_ptr is useful in those cases when the ownership of a dynamic instance doesn't belong to a single pointer, there's another smart pointer type for cases when such ownership doesn't need to be shared. In our example, p3 is the only pointer pointing to the second instance of my_class. Therefore, I'll redefine foo() this way (it doesn't change the console output):

void foo()
{
    auto p1 = make_shared<my_class>(), p2 = p1;
    // p3 is unique now (instead of shared).
    auto p3 = unique_ptr<my_class>(new my_class());

    cout << "p1 points to instance #" << p1->get_id() << endl;
    cout << "p2 points to instance #" << p2->get_id() << endl;
    cout << "p3 points to instance #" << (*p3).get_id() << endl;
}

When a unique_ptr is assigned to another one, the pointed instance is "moved" from the pointer at the right side of the assignment to the pointer at the left. Thereafter, the pointer at the right side starts pointing to nullptr (a new reserved word that supersedes NULL usage). There's no reference counting with unique_ptrs. Therefore, they're lighter than shared ones.

There's a special case where leaks can't be avoided, which is the case of circular references between shared pointers. Since each one has at least another one pointing to it, their counters never reach 0 and none of them are released when these pointer variables lose visibility; the leak just happens.

Dealing with this requires a third kind of smart pointer, called weak_ptr<T>. A weak_ptr can point to a shared resource without incrementing its reference counter. I'll show an example in an upcoming installment.

While C++ smart pointers seem to imitate the typical garbage collection mechanisms available in managed code, they release memory deterministically. In this column's example, as soon as foo() ends and control's handed back to main(), the destruction of the instances happens in between. In managed code, the garbage collection system determines when memory is released.

Join the Adventure
Whether you worked with C++ in the 90s and moved to a modern, managed language or you still code in C++ today but haven't hopped on the modern C++ train yet, my goal is to show you how much C++ has evolved in terms of elegance, expressive power and conciseness. This progress has closed the gap between C++ and modern languages without sacrificing either the kind of abstraction that object-oriented programming delivers, or its native ability to run directly on the physical machine. I hope you'll join me on this adventure of discovery.

About the Author

Diego Dagum is a software architect and developer with more than 20 years of experience. He can be reached at [email protected].

comments powered by Disqus

Featured

Subscribe on YouTube