0% found this document useful (0 votes)
101 views5 pages

Smart Pointers & RAII - Key Concepts and Patterns

The document discusses key concepts and patterns related to smart pointers and RAII (Resource Acquisition Is Initialization) in C++. It emphasizes the importance of managing resource lifetimes through smart pointers like std::unique_ptr, std::shared_ptr, and std::weak_ptr, while advocating for the Rule of Zero to simplify resource management. Best practices and anti-patterns are also outlined to ensure efficient and safe memory management in C++ programming.

Uploaded by

rishitguleria4
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
101 views5 pages

Smart Pointers & RAII - Key Concepts and Patterns

The document discusses key concepts and patterns related to smart pointers and RAII (Resource Acquisition Is Initialization) in C++. It emphasizes the importance of managing resource lifetimes through smart pointers like std::unique_ptr, std::shared_ptr, and std::weak_ptr, while advocating for the Rule of Zero to simplify resource management. Best practices and anti-patterns are also outlined to ensure efficient and safe memory management in C++ programming.

Uploaded by

rishitguleria4
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 5

Smart Pointers & RAII – Key Concepts and Patterns

RAII (Resource Acquisition Is Initialization)


• Principle: Tie every resource (heap memory, file handle, socket, mutex lock, etc.) to an object’s
lifetime. Acquire the resource in a constructor and release it in the destructor 1 . This ensures
resources are freed automatically when the object goes out of scope.
• Guarantees: RAII makes resource availability a class invariant and uses stack unwinding on
exceptions to release resources in reverse order of acquisition 2 . This eliminates leaks and
ensures exception safety.
• Pattern: Encapsulate each resource in a class: constructor acquires (or throws if it fails), destructor
releases without throwing. Always use such RAII classes as local (stack) or managed objects so their
lifetime is bounded by scope 3 .

Rule of Zero (from C++ Core Guidelines)


• Guideline: “Classes that declare custom destructors, copy/move constructors or copy/move
assignment operators should deal exclusively with ownership. Other classes should not declare
those functions” 4 . In practice, if a class only holds RAII members, it shouldn’t write its own
destructor or copy/move – just use the defaults.
• Rationale: Let the compiler generate special members automatically unless you need custom
resource management. This keeps code simple and avoids subtle errors with special-member
semantics 4 . (E.g. a class with a std::unique_ptr member needs no custom destructor or
copy; the default move/copy behavior works correctly.)

std::unique_ptr<T> (Exclusive Ownership)


• Ownership Model: A std::unique_ptr<T> exclusively owns a T* . When the unique_ptr is
destroyed or reset, it automatically deletes the object (using delete or a custom deleter) 5 . This
enforces RAII for single objects or arrays.
• No Overhead: It adds essentially no runtime overhead over raw pointers, since there’s no reference
counting or additional control block 6 . It’s as cheap to move/use as a raw pointer.
• Move-Only: unique_ptr is movable but not copyable. Copy operations are deleted, but move-
construction/assignment transfer ownership (the old pointer is set to null) 7 . This guarantees there
is always exactly one owner.
• Best Practices: Use std::make_unique<T>() to construct objects (combining allocation and
initialization safely) 8 . Prefer unique_ptr by default – it expresses intent (sole ownership) and
defers sharing decisions. As Herb Sutter notes: “When in doubt, prefer unique_ptr by default…” 9 .
• Usage Examples:
• Stack allocation pattern:

1
void foo() {
auto buf = std::make_unique<char[]>(1024); // buffer deleted when foo()
exits
// ... use buf.get() ...
}

• Parameter transfer: A function that takes ownership of a resource can accept a


std::unique_ptr<T> (move semantics), making ownership transfer explicit.

std::shared_ptr<T> (Shared Ownership)


• Ownership Model: std::shared_ptr allows shared ownership of a heap object. Internally it
maintains a control block with an atomic reference count. Each copy of a shared_ptr increments
the count, and each destruction decrements it. When the count reaches zero, the object (and the
control block) are deleted 10 .
• Thread Safety & Overhead: The reference count is updated in a thread-safe manner (using
atomics). While this makes shared_ptr safe to copy in multiple threads, it incurs overhead: atomic
operations are slower than raw pointer copies 11 6 . Thus, prefer it only when sharing is truly
needed.
• Factory Functions: Use std::make_shared<T>(...) to create a shared_ptr . This performs a
single allocation for the object and its control block (improving locality and speed) 8 . Similarly, use
std::make_unique for unique_ptr to avoid separate allocations.
• Conversion: You can construct a shared_ptr by moving a unique_ptr into it when shared
ownership becomes necessary.
• Usage Example:

auto sp1 = std::make_shared<MyClass>(args…);


std::shared_ptr<MyClass> sp2 = sp1; // ref count++
// Object is deleted when both sp1 and sp2 go out of scope

• Caution: Avoid unnecessary use. If the object does not need to live beyond a single owner,
unique_ptr is preferable. Overusing shared_ptr for everything can lead to hidden
performance costs and maintenance issues 9 .

std::weak_ptr<T> (Non-owning Observer)


• Purpose: A std::weak_ptr<T> holds a non-owning reference to an object managed by
std::shared_ptr<T> . It does not affect the reference count. This is used to break reference
cycles or to have an optional reference to a shared object.
• Breaking Cycles: If two objects hold shared_ptr to each other, neither will be destroyed (circular
ownership). Replacing one side with weak_ptr breaks the cycle: the weak pointer does not keep
the object alive 12 . In Arthur O’Dwyer’s example, using a weak_ptr for one link allowed both
objects to be destroyed normally 12 13 .
• Usage: To access the object, convert the weak_ptr to a shared_ptr via lock() . If the original
object still exists, lock() returns a non-null shared_ptr ; otherwise it returns nullptr .

2
• Example:

std::shared_ptr<Node> child = std::make_shared<Node>();


std::weak_ptr<Node> parent = child; // observe child without owning
if(auto p = parent.lock()) {
// safe to use *p
}

• Benefit: weak_ptr prevents memory leaks in graphs or observer patterns by ensuring that objects
can be collected when no strong ( shared_ptr ) references remain 12 .

Custom UniquePtr / SharedPtr Insights


• UniquePtr Internals: A simplified UniquePtr<T> holds a raw T* and a deleter. Its destructor
deletes the pointer. Copy ctor/assign are deleted; the move ctor/assign steal the pointer and null out
the source. This minimal design is what makes unique_ptr efficient (just a pointer plus a possible
small deleter object).
• SharedPtr Internals: A SharedPtr<T> uses a control block containing two counters: the strong
count (number of shared_ptr s) and the weak count ( weak_ptr s). Copying a shared_ptr
atomically increments the strong count; destroying one decrements it. When the strong count drops
to zero, the object is deleted. When both counts drop to zero, the control block frees itself. The
atomic updates and extra block incur overhead compared to unique_ptr .
• WeakPtr Internals: A WeakPtr<T> shares the control block but only increments the weak count. It
never prolongs the object’s life.
• Aliasing Constructor: Advanced use: std::shared_ptr supports an aliasing constructor, letting
two shared_ptr s share ownership of one object while one actually points to a sub-object. (Useful
in some designs, but use carefully.)
• Thread Safety Guarantee: By standard, reference counting in std::shared_ptr is thread-safe:
different threads can safely copy/destroy shared_ptr instances to the same object without
external locking. (However, modifying the same shared_ptr instance from multiple threads
requires synchronization.)

Best Practices
• Prefer unique_ptr by Default: As Herb Sutter summarizes, “prefer unique_ptr by default” 9 .
Only switch to shared_ptr when you truly need shared ownership. A unique_ptr can later be
moved into a shared_ptr if sharing becomes necessary, so start simple. 9 6

• Use Factory Functions: Always create smart pointers with std::make_unique or


std::make_shared rather than using new directly 8 . This avoids potential leaks if an exception
is thrown between allocations and combines allocations efficiently.
• Pass and Return by Value Judiciously: Don’t pass or return smart pointers unless ownership
semantics demand it. For non-owning function parameters, use raw pointers or references. For
owning transfers, pass unique_ptr by value (it conveys “I take ownership”) or return it.
• Break Cycles with weak_ptr : In any graph or bidirectional linkage, use weak_ptr for the “back”
references to avoid cycles 12 .

3
• Employ Rule of Zero: In your own classes, prefer to hold resources in smart-pointer members rather
than writing manual copy/move/destructors. Following Rule of Zero 4 means you often need no
special member functions at all.
• RAII for All Resources: Extend the RAII pattern beyond memory. Wrap file descriptors, sockets,
mutex locks, GPU buffers, etc., in objects whose destructors release the resource. (Examples:
std::lock_guard<std::mutex> , or a custom class that closes a file in its destructor.) This
practice prevents resource leaks in all realms of system or high-performance programming 1 .

Anti-Patterns to Avoid
• Overusing shared_ptr : Treat shared_ptr as a tool only for when multiple ownership is
genuinely required. Using it everywhere leads to hidden costs. (For example, throwing
shared_ptr<T> into STL containers or globals when a unique_ptr or raw pointer would suffice
is wasteful.) 9
• Manual new without Smart Pointers: Avoid owning raw pointers. If you allocate with new ,
immediately wrap it in a smart pointer. Otherwise an exception or return path could leak memory.
(Factory functions like make_unique prevent this common pitfall.) 8
• Circular shared_ptr : Never allow two objects to hold shared_ptr s to each other without a
weak_ptr . This cycle will keep them alive indefinitely (leak memory) 12 .
• Defining Destructors Unnecessarily: Don’t write a destructor just to debug or log unless needed.
Even a trivial destructor suppresses move operations (Rule of Five) 4 . If a class only holds smart
pointers, let defaults handle destruction (Rule of Zero).
• Ignoring get() / .release() : When interfacing with APIs that expect raw pointers, use get()
to observe the pointer or release() to give up ownership carefully. Misusing these can cause
double-deletes or leaks.

Examples and Use Cases


• Memory Management: In performance-critical code (systems or game engines), use
std::unique_ptr for heap allocations to get RAII with zero overhead 6 . For example, loading a
large data buffer:

void loadData() {
auto data = std::make_unique<uint8_t[]>(dataSize); // deleted
automatically
// fill data...
}

• Resource Handles: Wrap OS resources in RAII objects. E.g. a FileRAII class that opens a file in its
ctor and closes in its dtor, or use std::unique_ptr<FILE, decltype(&fclose)> as a quick
wrapper. This prevents forgetting to close files even on early returns.
• Shared Ownership Example: A subscriber list or cache might use std::shared_ptr . For
instance, a callback registry can hold std::shared_ptr<Listener> so listeners are kept alive as
long as registered.
• Breaking Cycles: In a tree or graph structure, child nodes might hold a std::weak_ptr to their
parent to avoid strong cycles. For example:

4
struct Node {
std::weak_ptr<Node> parent; // does not keep parent alive
std::shared_ptr<Node> child; // keeps child alive
};

This way the parent can be destroyed when appropriate even though the child points back to it.
• Locking: std::lock_guard<std::mutex> is a canonical RAII example in multithreading: it locks
the mutex on construction and automatically unlocks it on scope exit. This prevents deadlocks from
forgotten unlocks.

By adhering to RAII and using smart pointers judiciously (following the Rule of Zero and best practices), C++
code can be both safe from leaks and efficient. Smart pointers let you avoid manual delete entirely in
most code, shifting error-prone cleanup into well-tested library code (destructors and control blocks) 2

9 .

1 2 3 RAII - cppreference.com
https://en.cppreference.com/w/cpp/language/raii

4 The Rule of Zero in C++ - Fluent C++


https://www.fluentcpp.com/2019/04/23/the-rule-of-zero-zero-constructor-zero-calorie/

5 7 std::unique_ptr - cppreference.com
https://en.cppreference.com/w/cpp/memory/unique_ptr

6 9 GotW #89 Solution: Smart Pointers – Sutter’s Mill


https://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/

8 10 C++ Core Guidelines: Rules for Smart Pointers – MC++ BLOG


https://www.modernescpp.com/index.php/c-core-guidelines-rules-to-smart-pointers/

11 std::shared_ptr is an anti-pattern | Dmitry Danilov


https://ddanilov.me/shared-ptr-is-evil/

12 13 22.7 — Circular dependency issues with std::shared_ptr, and std::weak_ptr – Learn C++
https://www.learncpp.com/cpp-tutorial/circular-dependency-issues-with-stdshared_ptr-and-stdweak_ptr/

You might also like