Shared and Weak Pointers: Subtle Connections

Shared and Weak Pointers: Subtle Connections

Due to the reference counting mechanism, shared pointers are prone to forming reference circles which can lead to memory leaks. To address this problem, shared pointers have a counterpart - weak pointers.

Let's consider an implementation of the listener pattern. There is an 𝙴𝚟𝚎𝚗𝚝𝚂𝚘𝚞𝚛𝚌𝚎 class that produces events and a 𝙻𝚒𝚜𝚝𝚎𝚗𝚎𝚛 class that consumes those events. When an event is raised, the listener gets notified through the 𝙻𝚒𝚜𝚝𝚎𝚗𝚎𝚛::𝚗𝚘𝚝𝚒𝚏𝚢() method and should pull updates from 𝙴𝚟𝚎𝚗𝚝𝚂𝚘𝚞𝚛𝚌𝚎 using the 𝙴𝚟𝚎𝚗𝚝𝚂𝚘𝚞𝚛𝚌𝚎::𝚐𝚎𝚝𝚄𝚙𝚍𝚊𝚝𝚎𝚜() method.

The dependency between these two classes can be represented in the following diagram:

Article content

In this setup, the objects form a typical reference circle. If only shared pointers are used, the memory cannot be deallocated, even when there are no external references. To prevent this, 𝙴𝚟𝚎𝚗𝚝𝚂𝚘𝚞𝚛𝚌𝚎 holds a shared pointer to listeners, while 𝙻𝚒𝚜𝚝𝚎𝚗𝚎𝚛 holds only a weak pointer to 𝙴𝚟𝚎𝚗𝚝𝚂𝚘𝚞𝚛𝚌𝚎.

class EventSource {
public:
  std::vector<std::string> getUpdates() const;
private:
  std::vector<std::shared_ptr<Listener>> m_listeners;
};        
class Listener {
public:
  Listener(std::weak_ptr<EventSource> source);
  virtual void notify() = 0;
protected:
  std::weak_ptr<EventSource> getSource();
private:
  std::weak_ptr<EventSource> m_source;
};        

Creating a new copy of 𝚜𝚝𝚍::𝚠𝚎𝚊𝚔_𝚙𝚝𝚛 does not increase the reference count. When you need actual access to the object, a 𝚜𝚝𝚍::𝚜𝚑𝚊𝚛𝚎𝚍_𝚙𝚝𝚛 should be obtained, as shown in a possible implementation of 𝙻𝚒𝚜𝚝𝚎𝚗𝚎𝚛::𝚗𝚘𝚝𝚒𝚏𝚢():

void ConcreteListener::notify() {
  std::shared_ptr<EventSource> source = getSource().lock();
  if (!source) {
    return;
  }
  auto updates = source->getUpdates();
  // ...
}        

To obtain a shared pointer from a weak pointer, follow these steps:

  1. Call 𝚜𝚝𝚍::𝚠𝚎𝚊𝚔_𝚙𝚝𝚛::𝚕𝚘𝚌𝚔().
  2. Verify that the result is a valid 𝚜𝚝𝚍::𝚜𝚑𝚊𝚛𝚎𝚍_𝚙𝚝𝚛 object.
  3. Use the pointer.

To view or add a comment, sign in

More articles by Nikolai Kutiavin

  • Golang generic implementation for Producer-Consumer

    Producer-consumer design pattern is well-known in software development. In a few words, one object produces values, and…

    6 Comments
  • Factory Method in Python with blackjack and ̵h̵o̵o̵k̵e̵r̵s̵ decorators.

    Design patterns help organize code in a better way, improving readability and maintainability. Many design patterns can…

    8 Comments
  • Non-blocking synchronization for std::vector

    In my previous post, I described how to protect a std::vector using std::mutex. It was straightforward.

    13 Comments
  • Unit-test in C++: what should you know.

    Unit tests are important for a single reason - they prove that a single component works as expected in isolation. If a…

    9 Comments
  • C++ transactional memory

    Sometimes I feel like an archaeologist, and today I’ll share one of my findings: an old proposal for the C++ standard…

    24 Comments
  • Behavior vs Data concurrency protection

    Multithreading is one of the hottest topics in C++. However, the most crucial question isn't just about running code…

  • Why preprocessor directives are evil

    I'll start with a simple quiz: Do you think the code below is correct? Does it make more sense now? Preprocessor…

    42 Comments
  • Perl with classes

    Originally developed as a procedural language, Perl was later adapted to support modern Object-Oriented Programming…

    4 Comments
  • r-value reference: when and why?

    C++ has two commonly used types of references: l-value and r-value. An l-value reference points to an object with a…

    6 Comments
  • Network transport protocol: reliability and message-orientation out of the box

    If I asked you to name a reliable network protocol for the transport layer, what would be the first one that comes to…

    4 Comments

Insights from the community

Others also viewed

Explore topics