Like it!

Join me on Facebook!

Like it!

Move smart pointers in and out functions in modern C++

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.

Pass smart pointers to functions

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.

(1), (2), (3): pass by value to lend the ownership

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
    }
    

(4), (5): pass by reference to manipulate the ownership

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 :)

(6), (7): pass simple raw pointers/references

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.

Return smart pointers from functions

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:

  1. Once again, smart pointers are powered by move semantics: the dynamically-allocated resource they hold is moved around, not wastefully copied;
  2. modern compilers play the Return Value Optimization (RVO) trick. They are able to detect that you are returning an object by value, and they apply a sort of return shortcut to avoid useless copies. Starting from C++17, this is guaranteed by the standard. So even the smart pointer itself will be optimized out;
  3. returning 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>();
}

Don't mess with pointers and references!

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.

Sources

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

comments
Mike on December 30, 2018 at 12:15
In the last example you don't need std::move as the temporary is an rvalue which can directly moved into the receiving unique_ptr.
You should never std::move a return value as this can prevent RVO.
Triangles on January 01, 2019 at 17:12
@Mike excellent point. Thanks for the clarification, I'm going to update the article.
patatahooligan on January 02, 2019 at 12:53
If I'm not mistaken, for older versions of the standard where RVO was allowed but not mandatory, you might get an error for returning a unique pointer by value because the copy constructor is required to exist.
Andrew Haining on January 18, 2019 at 00:57
a returning value has always been an xrvalue, it's never been a good idea to std::move it, it's nothing to do with RVO except that std::move forces move sematics even when a more efficient RVO would be possible otherwise.
Abel on January 18, 2019 at 08:56
Just to be clear, there are some corner cases when moving is recommended for return value. (eg. ternary operator, returning a member of a local pair, struct... involving structured binding). But I agree with you @Mike that in general we should rely on RVO. (for cheap-to-copy objects copying over moving won't cause any/noticeable overhead if copy cannot be elided.)
barteroff on January 18, 2019 at 12:03
Hi! Thank for the nice article! Have you thought about creating an RSS feed for your blog? Would be awesome!