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.