Modern C++: Smart Pointers

Taming the Beast of Memory Management

Dagang Wei
4 min readJun 23, 2024

This blog post is part of the series Modern C++.

Introduction

Memory leaks. Dangling pointers. Manual delete calls. These are the nightmares of C++ developers who have wrestled with the intricacies of raw pointers and manual memory management. Fortunately, modern C++ offers a powerful tool to banish these issues: smart pointers.

What are Smart Pointers?

Smart pointers are C++ classes that act like pointers but provide automatic memory management. They wrap around a raw pointer and take care of deallocating the associated memory when it’s no longer needed.

Why Smart Pointers?

In traditional C++, manual memory management using raw pointers can be error-prone. Developers need to remember to delete allocated memory, which can lead to memory leaks if forgotten. Smart pointers solve this problem by automatically managing the lifetime of dynamically allocated objects. Here are the benefits of smart pointers:

  • Memory Safety: Dramatically reduce the risk of memory leaks and dangling pointers.
  • Exception Safety: Ensure proper resource cleanup even in the face of exceptions.
  • RAII (Resource Acquisition Is Initialization): Align resource lifetime with object lifetime, making code cleaner and more predictable.
  • Expressiveness: Clearly communicate ownership semantics.
  • Standard Library Integration: Work seamlessly with STL containers and algorithms.

The Big Three Smart Pointers

Exclusive Ownership: unique_ptr

A unique_ptr is the sole owner of the underlying resource. It cannot be copied, only moved to transfer ownership.

std::unique_ptr<std::string> message = std::make_unique<std::string>("Hello, unique_ptr!");
std::cout << *message << std::endl;

// Transfer ownership
std::unique_ptr<std::string> anotherMessage = std::move(message);
std::cout << *anotherMessage << std::endl;

// message is now null

Shared Ownership: shared_ptr

Multiple shared_ptr objects can manage the same resource. They keep track of the reference count and automatically deallocate the resource when the last shared_ptr goes out of scope.

std::shared_ptr<int> shared1 = std::make_shared<int>(42);

// Both shared1 and shared2 point to the same int
std::shared_ptr<int> shared2 = shared1;

std::cout << *shared1 << " " << *shared2 << std::endl; // Output: 42 42

Non-Owning Observer: weak_ptr

A weak_ptr observes a resource managed by a shared_ptr without affecting its lifetime. It's used to break potential circular dependencies and check if a resource is still valid.

std::shared_ptr<int> shared = std::make_shared<int>(99);
std::weak_ptr<int> weak = shared;

// Check if resource still exists
if (std::shared_ptr<int> ptr = weak.lock()) {
std::cout << *ptr << std::endl;
} else {
std::cout << "Resource has been deleted." << std::endl;
}

Best Practices and Caveats

  • Use unique_ptr as the default choice for exclusive ownership semantics.
  • Use shared_ptr when you need shared ownership.
  • Use weak_ptr to break circular dependencies or when you need a non-owning reference to a shared_ptr managed object.
  • Prefer make_unique and make_shared factory functions over direct use of new.
  • Avoid using raw pointers for ownership, use them only for non-owning references.
  • Be cautious with shared_ptr. If objects reference each other cyclically, memory won't be released. Use weak_ptr to break such cycles.
  • shared_ptr has a small overhead due to reference counting, but the safety benefits usually outweigh this minor cost.

Advanced Topic: Transfer Ownership of unique_ptr

Why transfer onwership?

Sometimes you need to transfer the ownership of a unique_ptr. Note that the fundamental concept of unique_ptr is exclusive ownership – there can only be one unique_ptr managing a specific resource at any time. Attempting to directly copy a unique_ptr (e.g., std::unique_ptr<int> ptr2 = ptr1;) will result in a compilation error, as the copy constructor of unique_ptr is explicitly deleted.

Here is how we deal with ownership transfer of unique_ptr in different scenarios:

  • Object Handoffs Within Scope: When you want one object to take over ownership of a resource from another within the same scope, std::move the unique_ptr to transfer ownership explicitly.
  • Function Returns: The compiler often optimizes the return of unique_ptr from functions, eliminating the need for explicit moves. This makes it safe and efficient to return ownership of dynamically allocated objects.
  • Container Insertion: Some STL containers (like std::vector) take ownership of elements, requiring you to move the unique_ptr into the container.

Example

#include <iostream>
#include <memory>

std::unique_ptr<int> createUniquePtr() {
return std::make_unique<int>(123);
}

int main() {
std::unique_ptr<int> ptr1 = createUniquePtr(); // ptr1 owns the resource returned from the function.
std::cout << *ptr1 << std::endl; // Output: 123

// Transfer ownership (within same scope).
// It would be a compilation error without std::move.
std::unique_ptr<int> ptr2 = std::move(ptr1);
std::cout << *ptr2 << std::endl; // Output: 123

// ptr1 is now null
if (ptr1) {
std::cout << "ptr1 still owns a resource" << std::endl;
} else {
std::cout << "ptr1 is null" << std::endl; // This line will execute
}

return 0;
}

Output:

123
123
ptr1 is null

Conclusion

Smart pointers are a fundamental part of modern C++ programming. They provide safer and more convenient memory management, helping developers write more robust and error-free code. By understanding and using unique_ptr, shared_ptr, and weak_ptr appropriately, you can significantly improve the quality and maintainability of your C++ projects. Remember, while smart pointers are powerful tools, they’re not a silver bullet for all memory management issues. It’s still important to understand the underlying concepts and use these tools judiciously. Happy coding!

--

--