Improving Stability with Modern C++, Part 4 — Memory Management
When we started learning C++, we were all taught that every new
needs a corresponding delete
. But sometimes we'd forget, or some code might throw an exception we weren't ready for, and then we'd leak memory. To try to solve that problem, there are widely used smart pointer classes that automatically call delete
for you when they go out of scope. This helps considerably but doesn't solve everything. Thanks to perfect forwarding in C++11, we can eliminate the new
call as well. With rare exceptions, C++ programmers should not have to write new
or delete
ever again.
Motivation
FactSetters have gotten a lot of mileage out of boost::shared_ptr
over the years. As of this writing, there are over 100,000 uses of it in our codebase. But we can do better now that C++11 is available. Here's an example smart pointer declaration prior to the modern standards:
boost::shared_ptr<std::string> foo(new std::string("Test"));
There are two separate allocations here: a new string, and the smart pointer’s control block containing the reference count. With C++11, we can do both at once:
auto foo = std::make_shared<std::string>("Test");
This function creates a new std::shared_ptr<std::string>
in one step by forwarding its arguments to the underlying class' constructor. It's both faster and safer, and now the programmer doesn't have to write new
or delete
. This efficiency improvement (coupled with better exception safety) is so compelling that we believe any further use of either new
or delete
is probably a code smell.
Should I replace all my pointers with smart pointers?
Certainly not. Aside from memory and exception safety, the smart pointer classes signal object ownership. C++11 removed the need for non-library code to manage memory, but not the need to keep track of who owns what. The object owning a resource is responsible for cleaning it up. As the names suggest, use std::make_shared
to create shared resources and std::make_unique
for resources with a singular owner. Perhaps more importantly, if the function you're calling will not participate in an object's ownership, passing non-owning raw pointers and references is still preferred.
Wait, did you just say raw pointers are good?
Yes, we did. Raw pointers owning a resource are the problem. But the vast majority of code doesn’t affect an object’s lifetime. It merely modifies or reads from it. In those cases, a reference (preferred) or raw pointer (if it could be null) is perfectly fine. Smart pointers should never appear in an interface unless you’re explicitly transferring or sharing object ownership.
Furthermore, it’s worth carefully considering whether an object’s ownership actually needs to be shared. The default use-case should be a std::unique_ptr
as a class member or in a top-level function, with references or raw pointers passed down the call stack. As long as the object will outlive all the places it's referenced, this should be sufficient. If that can't be guaranteed, or the resource handle really does need to be copied, then use std::shared_ptr
.
Working with legacy code
Smart pointer classes can provide ownership semantics for legacy code that deals only in raw pointers. They also accept custom deleters for objects with non-trivial cleanup functions. Some examples:
// Immediately assign a raw pointer output parameter to a smart pointer. Notice
// we're using unique_ptr directly instead of make_unique because the memory is
// already allocated.
Widget* tmp = nullptr;
widget_factory(TICKER_WIDGET, tmp);
std::unique_ptr<Widget> widget(tmp); // now owns the returned memory// Works on malloc/free too, pass a deleter argument
std::shared_ptr<Foo> bar(function_calling_malloc(), free);// C-style file management
std::unique_ptr<FILE, decltype(&fclose)> myfile(fopen("foo.txt", "r"), fclose);
What happened to std::auto_ptr
?
std::auto_ptr
was deprecated in C++11, with good reason. It was an early attempt at std::unique_ptr
before move semantics were added to the language. Its copy constructor behaved like a move, making it unusable with standard containers. Unless you're working with legacy code that requires it, you can safely forget about it. It was officially removed in C++17.
Where can I learn more?
Here are two videos on smart pointers that go into more detail. See the timestamp links to jump to the most relevant topics.
Back to the Basics! Essentials of Modern C++ Style by Herb Sutter
- Using raw pointers and references @12:09
- Passing smart pointers @19:04
- Summary on modern use of (smart) pointers @27:28
Curiously Recurring C++ Bugs at Facebook by Louis Brandy
- Is
std::shared_ptr
thread-safe? @20:10
What’s next?
In the next post in this series, we’ll revisit classes and the Rule of Three. The addition of move semantics has altered this rule in many ways.
Articles in this series
- Improving Stability with Modern C++, Part 3 — The
auto
Keyword - Improving Stability with Modern C++, Part 2 — Range-Based For-Loops
- Improving Stability with Modern C++, Part 1 — Getting Started
Acknowledgments
Special thanks to all that contributed to this blog post:
Author: Michael Kristofik
Managing Editor: Ralph Kootker
Reviewers: James Abbatiello, Anit Lulla, Jens Maurer, and Tim Severeijns