C++ Core Guidelines: Passing Smart Pointers

Passing smart pointers is an important topic that is seldom addressed. This ends with the C++ core guidelines because they have six rules for passing std::shared_ptr and std::unique_ptr.

 

relay race

The six rules violate the import dry (don't repeat yourself) principle for software development. In the end, we have only four rules what makes our life as a software developer a lot easier. Here are the rules.

 Let's start with the first two rules for std::unique_ptr.

R.32: Take a unique_ptr<widget> parameter to express that a function assumes ownership of a widget

If a function should take ownership of a Widget, you should take the std::unique_ptr<Widget> by copy. The consequence is that the caller has to move the std::unique_ptr<Widget> to make the code run.

#include <memory>
#include <utility>

struct Widget{
    Widget(int){}
};

void sink(std::unique_ptr<Widget> uniqPtr){
    // do something with uniqPtr
}

int main(){
    auto uniqPtr = std::make_unique<Widget>(1998);
    
    sink(std::move(uniqPtr));      // (1)
    sink(uniqPtr);                 // (2) ERROR
}

 

The call (1) is fine but call (2) breaks because you can not copy an std::unique_ptr. If your function only wants to use the Widget, it should take its parameter by the pointer or by reference. The difference between a pointer and a reference is that a pointer can be a null pointer.

void useWidget(Widget* wid);
void useWidget(Widget& wid);

R.33: Take a unique_ptr<widget>& parameter to express that a function reseats the widget

Sometimes a function want's to reseat a Widget. In this use case, you should pass the std::unique_ptr<Widget> by a non-const reference.

#include <memory>
#include <utility>

struct Widget{
    Widget(int){}
};

void reseat(std::unique_ptr<Widget>& uniqPtr){
    uniqPtr.reset(new Widget(2003));   // (0)
    // do something with uniqPtr
}

int main(){
    auto uniqPtr = std::make_unique<Widget>(1998);
    
    reseat(std::move(uniqPtr));       // (1) ERROR
    reseat(uniqPtr);                  // (2) 
}

 

Now, the call (1) fails because you can not bind an rvalue to a non-const lvalue reference. This will not hold for the copy in (2). A lvalue can be bound to an lvalue reference. By the way. The call (0) will not only construct a new Widget(2003), it will also destruct the old Widget(1998).

The next three rules to std::shared_ptr are literally repetitions; therefore, I will make one out of them.

R.34: Take a shared_ptr<widget> parameter to express that a function is part owner, R.35: Take a shared_ptr<widget>& parameter to express that a function might reseat the shared pointer,  and R.36: Take a const shared_ptr<widget>& parameter to express that it might retain a reference count to the object ???

Here are the three function signatures, we have to deal with.

void share(std::shared_ptr<Widget> shaWid);
void reseat(std::shard_ptr<Widget>& shadWid);
void mayShare(const std::shared_ptr<Widget>& shaWid);

 

Let's look at each function signature in isolation. What does this mean from the function perspective?

  • void share(std::shared_ptr<Widget> shaWid): I'm for the lifetime of the function body a shared owner of the Widget. At the beginning of the function body, I will increase the reference counter; at the end of the function, I will decrease the reference counter; therefore, the Widget will stay alive, as long as I use it.
  • void reseat(std::shared_ptr<Widget>& shaWid): I'm not a shared owner of the Widget, because I will not change the reference counter. I have not guaranteed that the Widget will stay alive during the execution of my function, but I can reseat the resource. A non-const lvalue reference is more like: I borrow the resource and can reseat it. 
  • void mayShare(const std::shared_ptr<Widget>& shaWid): I only borrow the resource. Either can I extend the lifetime of the resource nor can I reseat the resource? To be honest, you should use a pointer (Widget*) or a reference (Widget&) as a parameter instead, because there is no added value in using a std::shared_ptr.

R.37: Do not pass a pointer or reference obtained from an aliased smart pointer

Let me present you with a short code snippet to make the rule clear.

void oldFunc(Widget* wid){
  // do something with wid
}

void shared(std::shared_ptr<Widget>& shaPtr){       // (2)
    
   oldFunc(*shaPtr);                                // (3)
   
   // do something with shaPtr
     
 }

auto globShared = std::make_shared<Widget>(2011);   // (1)


...

shared(globShared);                                 

 

globShared (1) is a globally shared pointer. The function shared takes it argument per reference (2). Therefore, the reference counter of shaPtr will no be increased and the function share will no extend the lifetime of Widget(2011). The issue begins with (3). oldFunc accepts a pointer to the Widget; therefore, oldFunc has no guarantee that the Widget will stay alive during its execution. oldFunc only borrows the Widget.

The cure is quite simple. You have to ensure that the reference count of globShared will be increased before the call to the function oldFunc. This means you have to make a copy of the std::shared_ptr:

  • Pass the std::shared_ptr by copy to the function shared:
     void shared(std::shared_ptr<Widget> shaPtr){
       
       oldFunc(*shaPtr);
       
       // do something with shaPtr
         
     } 
    
  • Make a copy of the shaPtr in the function shared:
     void shared(std::shared_ptr<Widget>& shaPtr){
       
       auto keepAlive = shaPtr;   
       oldFunc(*shaPtr);
       
       // do something with keepAlive or shaPtr
         
     } 
    

The same reasoning also applies to std::unique_ptr but I have no simple cure in mind because you can not copy an std::unique_ptr. I suggest you should clone your std::unique_ptr and, therefore, make a new std::unique_ptr.

What's next?

This was the last of my four posts about resource management in the C++ core guidelines. The C++ core guidelines have more than 50 rules for expressions and statements. I will have a closer look at my next post.

 

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschläger, Red Trip, Alexander Schwarz, Tornike Porchxidze, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Satish Vangipuram, and Michael Dunsky.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, and Said Mert Turkal.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Bookable (Online)

German

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

New

Contact Me

Modernes C++,

RainerGrimmSmall

Comments   

+6 #1 Boris 2018-01-02 09:45
Small correction to the R.37:
We should whether change the oldFunc as:
void oldFunc(const Widget& wid),

or change the oldFunc call as:
oldFunc(shaPtr.get());
Quote
+1 #2 Yoed 2020-03-16 05:20
About R.37,
The function shared is holding onto the shared_ptr reference which means the ref-counter won't drop to zero.

if the ref-count may drop to zero from a different thread, it can happen before the copy and you have much tougher problems.
Quote
-2 #3 Fred 2020-06-12 20:01
fix 39, it does not even compile
Quote
0 #4 Rainer Grimm 2020-06-13 16:46
Quoting Fred:
fix 39, it does not even compile

There is no 39 on this page. What should I fix?
Quote
0 #5 FeRD, Frank Dana 2021-01-27 05:52
I kind of find myself concurring with Yoed (comment #2) — unless the code in question is being run in a separate thread, it shouldn't be necessary to artificially increase a shared_ptr's reference count beyond the original reference, which was already counted (and remains valid) in the calling function.

IOW: In the R37 listing, the reference count for globShared is initialized to 1 when it's first constructed at (1). So, when shared(globShared) is called on the very last line, the reference count will STILL be (at least) 1. And since the execution of that block of code will be suspended until shared() returns, there's no real way the count can drop BELOW 1 during the execution of shared() and oldFunc(), since the original globShared reference can't possibly be destroyed until after shared() returns and the code that called it resumes... no?
Quote
0 #6 Ramakrishnan 2021-02-04 16:28
Hello , Can you also add passing a pointer as an RValue reference and its examples and impact.

std::shared_ptr && widget

Thanks.
Quote
0 #7 Roberto 2021-07-19 13:14
at the end of 36 " To be honest, you should use a pointer (Widget*) or a reference (Widget&) as a parameter instead, because there is no added value in using a std::shared_ptr."

and 37
" Do not pass a pointer or reference obtained from an aliased smart pointer"
Quote

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 4960

Yesterday 7071

Week 41936

Month 202610

All 6851302

Currently are 191 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments