Improving Stability with Modern C++, Part 4 — Memory Management

Ralph Kootker
FactSet
Published in
4 min readDec 28, 2021

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

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

--

--

Ralph Kootker
FactSet
Writer for

I publish on behalf of others or myself. Please carefully look at the acknowledgements at the bottom of each article