Join us on Facebook!
— Written by Triangles on December 24, 2018 • updated on January 19, 2019 • ID 69 —
Different options with different meanings.
In my previous article A beginner's look at smart pointers in modern C++ I took a trip to the convoluted land of C++ smart pointers. Now it's time to see how they behave in real world applications, along with common pitfalls and best practices.
In this article I will show you how to pass and return smart pointers to/from functions, operations that require some planning. There are many ways of doing it and picking the right one is not always straightforward. Luckily for us C++ experts have guidelines that shed some light on this task.
I also assume that you are kind of familiar with move semantics. I have written an article about it if you aren't.
Smart pointers can be passed to functions in five or seven different ways:
void f(std::unique_ptr<Object>); // (1)
void f(std::shared_ptr<Object>); // (2)
void f(std::weak_ptr<Object>); // (3)
void f(std::unique_ptr<Object>&); // (4)
void f(std::shared_ptr<Object>&); // (5) also const &
void f(Object&); // (6) also const &
void f(Object*); // (7) also const *
Yes, even (6) and (7) are an option. Which one to choose? According to the C++ Core Guidelines a function should take a smart pointer as parameter only if it examines/manipulates the smart pointer itself.
As you may know, a smart pointer is a class that provides several methods and features. For example you can count the references of a std::shared_ptr
or increase them by making a copy; you can move data from a std::unique_ptr
to another one (change of ownership); you can empty a smart pointer and so on. Your function should accept a smart pointer if you expect that it will do one of those things.
Conversely, a function should accept raw pointers or references if it just needs to operate on the underlying object without altering the smart pointer. Let's dig deeper.
Pass smart pointers by value to lend their ownership to the function, that is when the function wants its own copy of the smart pointer in order to operate on it. Different smart pointers require different strategies:
A std::unique_ptr
can't be passed by value because it can't be copied, so it is usually moved around with the special function std::move
from the Standard Library. This is move semantics in action:
std::unique_ptr<Object> up = std::make_unique<Object>();
function(std::move(up)); // Usage of (1)
You have just moved the ownership of the dynamically-allocated Object
from up
to the function parameter. Remember that now up
is a hollow object. This is known as a sink: the ownership of the dynamically-allocated resource flows down an imaginary sink from one point to another;
There's no need to move anything with std::shared_ptr
: it can be passed by value (i.e. can be copied). Just remember that its reference count increases when you do it;
std::weak_ptr
can be passed by value as well. Do it when the function needs to create a new std::shared_ptr
out of it, which would increase the reference count:
void f(std::weak_ptr<Object> wp)
{
if (std::shared_ptr<Object> sp = wp.lock())
sp->doSomething(); // I have a new shared_ptr that points to Object
}
Pass by reference when the function is supposed to modify the ownership of existing smart pointers. More specifically:
pass a non-const
reference to std::unique_ptr
if the function might modify it, e.g. delete it, make it refer to a different object and so on. Don't pass it as const
as the function can't do anything with it: see (6) and (7) instead;
the same applies to std::shared_ptr
, but you can pass a const
reference if the function will only read from it (e.g. get the number of references) or it will make a local copy out of it and share ownership;
I didn't find a real use case for passing std::weak_ptr
by reference so far. Suggestions are welcome :)
Go with a simpler raw pointer (can be null) or a reference (can't be null) when your function just needs to inspect the underlying object or do something with it without messing with the smart pointer. Both std::unique_ptr
and std::shared_ptr
have the get()
member function that returns the stored pointer. For example:
std::unique_ptr<Object> pu = std::make_unique<Object>();
function(*pu.get()); // Usage of (6)
function(pu.get()); // Usage of (7)
A std::weak_ptr
must be converted to a std::shared_ptr
first in order to take the stored pointer.
You should follow the same logic above: return smart pointers if the caller wants to manipulate the smart pointer itself, return raw pointers/references if the caller just needs a handle to the underlying object.
If you really need to return smart pointers from a function, take it easy and always return by value. That is:
std::unique_ptr<Object> getUnique();
std::shared_ptr<Object> getShared();
std::weak_ptr<Object> getWeak();
There are at least three good reasons for this:
std::shared_ptr
by reference doesn't properly increment the reference count, which opens up the risk of deleting something at the wrong time.As always, a std::unique_ptr
can't be copied. It has to be moved instead:
c++
std::unique_ptr<Object> getUnique() { return std::move(std::make_unique<Object>()); }
Thanks to point 2. you don't need move anything when returning a std::unique_ptr
:
std::unique_ptr<Object> getUnique()
{
std::unique_ptr<Object> p = std::make_unique<Object>();
return p;
// also return std::make_unique<Object>();
}
You should never do fancy tricks with pointers and references you get()
from smart pointers: don't delete
them, don't create new smart pointers out of them, or more generally: don't mess with their ownership. Whenever a function returns a raw pointer/reference or take it as parameter, you should consider it as owned by someone else, somewhere else in the code base. You can definitely operate on it but the ownership still belongs to the smart pointer that originally returned the raw pointer/reference to its dynamically-allocated resource.
C++ Core Guidelines
StackOverflow - How do I pass a unique_ptr argument to a constructor or a function?
cppreference.com - std::unique_ptr
cppreference.com - std::shared_ptr
Wikipedia - Smart pointer
Rufflewind's Scratchpad - A basic introduction to unique_ptr
Sutter’s Mill - GotW #102: Exception-Safe Function Calls
Sutter’s Mill - GotW #91 Solution: Smart Pointer Parameters
You should never std::move a return value as this can prevent RVO.