From C to C++98 to “Modern C++”, memory management ‘best practice’ has been one of the most changeable things in this family of languages. C’s basic memalloc
and free
functions for allocating and releasing heap memory were replaced with C++’s more sophisticated new
and delete
keywords, which, in turn, are being supplanted by the so-called ‘smart’ pointers and the make_shared
and make_unique
template functions.
The main driver for these changes is to make it easier for programmers to ensure that memory allocations are properly released when the memory is no longer required by the programmer. This became more difficult to guarantee as execution flows became more complex with features such as exceptions and multi-threading. These features make programs very non-linear, and the basic cycle of “acquire resource / use resource / release resource” becomes almost impossible to track.
Languages like Java use Garbage Collection to solve this problem. While GC is available in C++, it is rarely used (usually for performance reasons – possibly a topic for another post). Instead, the general philosophy in C++ is to determine when memory (and other resources) are no longer required, and deallocate at that point.
Modern C++ smart pointers are template classes that ‘wrap’ a ‘raw’ pointer to a dynamically allocated object, and ensure that the object is safely deleted when it is no longer being used. They come in three ‘flavours’:
std::unique_ptr
– a pointer that uniquely ‘owns’ an object; there is always exactly one reference to the owned object, and it is deleted when the pointer goes out of scope; unique pointers can be moved, but never copiedstd::shared_ptr
– a pointer that shares ownership of an object with other shared pointers; shared pointers use a reference-counting scheme so that the managed object is deleted when its last shared pointer goes out of scope; shared pointers may be freely copied and movedstd::weak_ptr
– a pointer that ‘weakly’ references an object managed by one or more shared pointers; weak pointers do not participate in reference counting, and are used to break circular dependencies between shared pointers, which cause memory leaks
The semantics of unique pointers are quite easy to demonstrate. Note how the pointer can be moved, but not copied:
std::unique_ptr my_func(std::unique_ptr p)
{
std::cout << *p << std::endl;
return p;
}
auto a = std::make_unique("the quick, brown fox...");
// 'a' points to a std::string object on the heap.
auto b = my_func(std::move(a));
// now, the string has been printed to std::out,
// 'b' points to the string, and 'a' is nullptr
b = nullptr;
// string is properly deleted (also happens when `b`
// goes out of scope)
Because the object can only ever be accessed via exactly one variable, unique pointers are great for multithreaded situations to help avoid race conditions (in addition to ensuring that the object is properly deleted once the owning variable goes out of scope). If a unique pointer is used to pass an object from the scope of one thread to another, there is no possibility of it being accessed by more than one thread at a time.
Shared pointers offer more flexibility, but this comes at a cost, both in terms of performance and flexibility. As the name implies, multiple shared pointers to a single object have shared ownership of that object, which is only deleted when the last shared pointer to the object goes out of scope.
This is reasonably easy to illustrate:
auto a = std::make_shared("abc");
// `a` is a shared pointer to a std::string
auto b = a;
// `b` points to the same string as a
a = nullptr;
// `a` no longer points to anything, but the string
// still exists on the heap, pointed to by `b`
std::cout << *b << std::endl;
// dereference the pointer and print the string
b = nullptr;
// string has been deleted now; this would also happen
// when `b` went out of scope
This is achieved through the magic of reference counting. Every time a new shared pointer to an object is created, a counter for that object is incremented, and every time one goes out of scope (or no longer points to the object) the reference count is decremented. When the reference count reaches zero, the object is deleted normally.
The snag with shared pointers is the possibility of cyclic dependencies. If two objects hold shared pointers to each other, neither will ever be deleted. The std::weak_pointer
class fixes this problem.
A weak pointer holds a pointer to an object that is owned by one or more shared pointers, but does not participate in reference counting. A weak pointer cannot be used to access the object directly, but it can be ‘locked’, which generates a new shared pointer to the object (which can then be used as above). Locking fails if the object has been deleted because all of its shared pointers have gone out of scope. The cppreference.com page on weak pointers has a good example of this.
Another thing to remember about shared pointers is that the reference counting scheme has a small overhead cost in performance and memory usage. It’s the sort of thing that “old school developers” (like me) like to complain about, but if we were ‘hand rolling’ our own shared ownership scheme with the same capabilities, it would be difficult to beat the efficiency of the standard implementation.
The examples above use the make_unique
and make_shared
template functions instead of the older new
syntax. Smart pointers can be constructed with pointers created using new
, but the “make_…” functions are considered preferable because they give the compiler more options to optimise memory allocation. Also, it’s a good idea to treat the allocation of memory and using it with a smart pointer as a single, atomic operation. If an object is being managed by smart pointers, it should almost never be used without smart pointers.
That’s a rather neat segue into one final point: these developments in C++ are s progress towards a programming idiom called Resource Acquisition is Initialisation or RAII (oh, all right, ‘initialization’ for my friends across the water). The essential principle here is that ownership of resources (like memory) is atomically associated with the lifetime of an object.
Smart pointers (along with the make_unique
and make_shared
template functions) support good RAII principles by making memory allocation almost as simple as using a stack variable, while retaining the dynamic flexibility of heap allocations.