Shared pointers as function arguments

This article complements the previous on unique pointers. I have noticed that there is some confusion around how and when to design a function to take a shared pointer as an argument. The goal of this article is to provide guidelines for the different use cases that you may face.

I just need access to the managed object

A common scenario you may face is that you have a shared pointer and you want to call a function that needs access to the managed object. I’ve seen many cases where this is solved by passing a const reference to the shared pointer, like this:

void foo(std::shared_ptr<T> ptr) {
  if (!ptr) return;
  bar(ptr);
}

// Don't do this if you only need access to the
// managed object.
void bar(const std::shared_ptr<T>& ptr) {
  auto i = ptr->getInt();
  auto f = ptr->getFloat();
  // Do something with i and f.
}

I am unsure to what goes through the mind of the developer who designs the bar function signature like this but I suspect it is often a result of breaking out a smaller helper function from the larger foo. A big drawback of doing this is that bar now only accepts an object T that is wrapped by a shared pointer. It cannot be reused for other scenarios.

The correct way to handle this is to design bar to accept a reference to an object of type T:

void foo(std::shared_ptr<T> ptr) {
  if (!ptr) return;
  bar(*ptr);
}

// Do this!
void bar(const T& t) {
  auto i = t.getInt();
  auto f = t.getFloat();
  // Do something with i and f.
}

But, what if I want to do some work even if the shared pointer is empty?

If an empty shared pointer, i.e. without a managed object, is a possibility for your use case, then you can’t use references as shown above.

Code that I’ve seen quite often handles this scenario somewhere in the lines of this:

void foo(std::shared_ptr<T> ptr) {
  bar(ptr);
}

// Don't do this if you only need access to the managed
// object (if one exist).
void bar(const std::shared_ptr<T>& ptr) {
  // Do some stuff (not involving ptr).
  if (ptr) {
    // Stuff that only happens if a managed object is present.
  }
  // Do some more stuff (also not involving ptr).
}

Here we see the const reference to the shared_ptr pattern again. This time however, I think the major reason for this design is that ”raw pointers are evil”, so it is tempting to reach for the const reference pattern. However, raw pointers do serve a purpose, they are very good at representing null (no object). And when passing the shared pointer by reference you still need to check if it is empty, so no win there.

Instead, use a raw pointer:

void foo(std::shared_ptr<T> ptr) {
  bar(ptr.get());
}

// Do this!
void bar(const T* ptr) {
  // Do some stuff (not involving ptr).
  if (ptr) {
    // Stuff that only happens if ptr is not a nullptr
  }
  // Do some more stuff (also not involving ptr).
}

I DO need shared ownership

Of course there will be scenarios where shared ownership is needed, otherwise you wouldn’t use a shared pointer, right? Here you do want to pass the shared pointer by value. Doing so will create a copy which increments the reference count (atomically) and help you keep the managed object alive for as long as it is needed.

void foo(std::shared_ptr<T> ptr) {
  // If ptr.use_count() is n here...
  bar(ptr);
}

// Do this if you need bar to share ownership of the
// managed object.
void bar(std::shared_ptr<T> ptr) {
  // ... ptr.use_count() is n+1 here.
}

But, I have a long call chain and only the last function needs shared ownership

It might be the case that you want to pass along the shared pointer via several different functions before ending up where shared ownership is really needed. Creating copies in each call, where each copy atomically increments and then decrements the reference count can be wasteful. So, how do you avoid making unnecessary copies?

Here is where move semantics comes into play. If you pass a shared pointer by rvalue it will be moved instead of copied, which keeps the reference count the same. This can be achieved by using std::move.

void foo(std::shared_ptr<T> ptr) {
  // ptr.use_count() is n
  bar(std::move(ptr)); // Move to bar
  // NOTE: ptr is empty here!
}

void bar(std::shared_ptr<T> ptr) {
  // ptr.use_count() is still n
  baz(std::move(ptr)); // Move to baz
}

void baz(std::shared_ptr<T> ptr) {
  // ptr.use_count() is still n
  qux(ptr); // Copy to qux
}

void qux(std::shared_ptr<T> ptr) {
  // ptr.use_count() is now n+1
}

I want the called function to be able to modify the shared pointer

You might want to call a function that modifies the shared pointer in some way, for example it might reset the pointer (replace the managed object with a different object). In that case, do what you would have done if you wanted to make any object modifiable, use a non-const reference.

void foo(std::shared_ptr<T> ptr) {
  bar(ptr); // Replaces the managed object.
}

void bar(std::shared_ptr<T>& ptr) {
  ptr.reset(new T);
}

I’m calling a function that only needs shared ownership sometimes

This is a special case that shouldn’t be very common, but it is possible that you want do design a function that under some circumstances needs to make a copy, i.e. share ownership, and sometimes doesn’t. Here you want to avoid making a copy if not really necessary. This can be done by passing by const reference (it does actually have a use case, who would have guessed!).

void foo(std::shared_ptr<T> ptr) {
  bar(ptr);
}

// Avoid taking a copy unless it is needed
void bar(const std::shared_ptr<T>& ptr) {
  if (needs_shared_ownership()) {
    auto ptr_copy = ptr;
  }
}

Now you’ve reached the end of the article. Hope this covers all use cases you have. If not, please let me know.

Happy coding!

Lämna en kommentar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *

Rulla till toppen