C++ Core Guidelines: Rules for Smart Pointers

Contents[Show]

There were a lot of C++ experts who said that smart pointers were the most important feature of C++11. Today, I will write about smart pointers in C++.

The C++ core guidelines has thirteen rules for smart pointers. Half of them deal with their owner semantic; half of them with the question: How should you pass a shared pointer to a function?

gold 513062 640

Here is an overview of the rules.

The first five rules (R.20 - R.24) are quite obvious. I have written a few posts about them. Let me paraphrase the rules and refer to my previous posts.

An std::unique_ptr is an exclusive owner of its resource; therefore, you can not copy it but only move it. In contrast, an std::shared_pointer shares ownership. If you copy or copy assign a shared pointer, the reference counter will automatically be increased; if you a delete or reset a shared pointer, the reference counter will be decreased. If the reference counter becomes zero, the underlying resource will be deleted. Because of this management overhead, you should use an std::unique_ptr, if possible (R.21).

This overhead becomes in particular true if you create an std::shared_ptr. Creating an std::shared_ptr requires the allocation of the resource and the reference counter which is in sum quite an expensive job; therefore, you should use the factory function std::make_shared (R.22). std::make_shared makes only one allocation. This is a big performance improvement for std::shared_ptr. In compared in the post "Memory and Performance Overhead of Shared Pointers" the differences between the creation and deletion of raw pointers and shared pointers including the factory functions std::make_shared and std::make_unique.

There is an additional, important reason to create an std::shared_ptr with std::make_shared, and an std::unique_ptr with std::make_unique: no memory leak (R.22 and R.23). Using two invocations of std::shared_ptr or std::unique_ptr in one expression can cause a memory leak, if an exception happens. Read the details about this issue in my last post: C++ Core Guidelines: Rules for Allocating and Deallocating (R.13).

To be honest, an std::weak_ptr is not a smart pointer. An std::weak_ptr is no owner and lends only the resource from its std::shared_ptr. Its interface is quite limited. By using the method lock on an std::weak_ptr, you can lift an std::weak_ptr to an std::shared_ptr. Of course, you have a question: Why do we need an std::weak_ptr? An std::weak_ptr helps to break the cycles of std::shared_ptr (R.24). These cycles are the reason, an std::shared_ptr will not automatically release its resource. Or to say it the other way around. If you have a cycle of shared pointers, you will have a memory leak. Read the details to std::weak_ptr and how you can use them to overcome memoy leaks with std::shared_ptr in my previous post std::weak_ptr.

Now I'm done with my summary of smart pointers. That is more or less general knowledge to smart pointers. This will not hold for the remaining rules. They deal with the question: How should you pass a shared pointer to a function?

R.30: Take smart pointers as parameters only to explicitly express lifetime semantics

This rule is a little bit tricky. If you pass a smart pointer as a parameter to a function and you use in this function only the underlying resource of the smart pointer, you do something wrong. In this case, you should use a pointer or a reference as a function parameter, because you don't the lifetime semantic of a smart pointer.

Let me give you an example to the quite sophisticated lifetime management of a smart pointer.

 

// lifetimeSemantic.cpp

#include <iostream>
#include <memory>

void asSmartPointerGood(std::shared_ptr<int>& shr){
  std::cout << "shr.use_count(): " << shr.use_count() << std::endl;  // (3)
  shr.reset(new int(2011));                                          // (5)
  std::cout << "shr.use_count(): " << shr.use_count() << std::endl;  // (4)
}

void asSmartPointerBad(std::shared_ptr<int>& shr){
  // doSomethingWith(*shr);
  *shr += 19;
}

int main(){
  
  std::cout << std::endl;
  
  auto firSha = std::make_shared<int>(1998);
  auto secSha = firSha;
  std::cout << "firSha.use_count(): " << firSha.use_count() << std::endl;  // (1)
  
  std::cout << std::endl;
  
  asSmartPointerGood(firSha);                                              // (2)
  
  std::cout << std::endl;
  
  std::cout << "*firSha: " << *firSha << std::endl;
  std::cout << "firSha.use_count(): " << firSha.use_count() << std::endl;
  
  std::cout << std::endl;
  
  std::cout << "*secSha: " << *secSha << std::endl;
  std::cout << "secSha.use_count(): " << secSha.use_count() << std::endl;
  
  std::cout << std::endl;
  
  asSmartPointerBad(secSha);                                              // (6)
  std::cout << "*secSha: " << *secSha << std::endl;
  
  std::cout << std::endl;
  
}

 

I will start with the good case for an std::shared_ptr. The reference counter in line (1) is 2 because I used the shared pointer firSha to copy-initalise secSha. Let's have a closer look at the invocation of the function asSmartPointerGood (2).  First (3), the reference count of shr is 2 and then, it becomes 1 in the line (4).  What happened in line (5)? I reset shr to the new resource: new int(2011). Consequentially, both the shared pointer firSha und secSha are immediately shared owner of different resources. You can observe the behaviour in the screenshot.

lifetimeSemantic

If you invoke reset on a shared pointer, magic happens under the hood.

  • If you invoke reset without an argument, the reference counter will be decreased by one.
  • If you invoke reset with an argument and the reference counter was at least 2, you will get two independent shared pointer owning different resources. This is a kind of deep copy of shared pointers.
  • If you invoke reset with or without an argument and the reference counter becomes 0, the resource will be released.

This magic is not necessary if you only interested in the underlying resource of the shared pointer; therefore, a pointer or a reference is the right kind of parameter for the function asSmartPointerBad (6).

Further information

Have a look also at a recent post by Bartek F. about a situation where weak_ptr prevents from full memory cleanup: How a weak_ptr might prevent full memory cleanup of managed object.

What's next?

There are six rules left for passing smart pointers to functions. So you know, what I will write about in my next post.

 

 

Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter, Franco Amato, and Carlos Gomes Martinho.

 

Get your e-book at leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 550 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 213

All 946360

Currently are 204 guests and no members online

Kubik-Rubik Joomla! Extensions