Mentions légales du service

Skip to content
Snippets Groups Projects

Add a paragraph about smart pointers efficiency, citing the reference given in MR29.

1 file
+ 14
0
Compare changes
  • Side-by-side
  • Inline
%% Cell type:markdown id: tags:
# [Getting started in C++](./) - [Useful concepts and STL](./0-main.ipynb) - [Smart pointers](./6-SmartPointers.ipynb)
%% Cell type:markdown id: tags:
<h1>Table of contents<span class="tocSkip"></span></h1>
<div class="toc"><ul class="toc-item"><li><span><a href="#Introduction" data-toc-modified-id="Introduction-1">Introduction</a></span></li><li><span><a href="#unique_ptr" data-toc-modified-id="unique_ptr-2"><code>unique_ptr</code></a></span><ul class="toc-item"><li><span><a href="#Usage-to-store-data-in-a-class" data-toc-modified-id="Usage-to-store-data-in-a-class-2.1">Usage to store data in a class</a></span></li><li><span><a href="#Releasing-a-unique_ptr" data-toc-modified-id="Releasing-a-unique_ptr-2.2">Releasing a <code>unique_ptr</code></a></span></li></ul></li><li><span><a href="#shared_ptr" data-toc-modified-id="shared_ptr-3"><code>shared_ptr</code></a></span></li><li><span><a href="#Efficient-storage-with-vectors-of-smart-pointers" data-toc-modified-id="Efficient-storage-with-vectors-of-smart-pointers-4">Efficient storage with vectors of smart pointers</a></span><ul class="toc-item"><li><ul class="toc-item"><li><span><a href="#Using-a-trait-as-syntactic-sugar" data-toc-modified-id="Using-a-trait-as-syntactic-sugar-4.0.1">Using a trait as syntactic sugar</a></span></li></ul></li></ul></li></ul></div>
%% Cell type:markdown id: tags:
## Introduction
In short, **smart pointers** are the application of [RAII](./2-RAII.ipynb) to pointers: objects which handle more nicely the acquisition and release of dynamic allocation.
There are many ways to define the behaviour of a smart pointer (the dedicated chapter in \cite{Alexandrescu2001} is a very interesting read for this, especially as it uses heavily the template [policies](../4-Templates/5-MoreAdvanced.ipynb#Policies) to implement his):
* How the pointer might be copied (or not).
* When is the memory freed.
* Whether `if (ptr)` syntax is accepted
* ...
The STL made the choice of providing two (and a half in fact...) kinds of smart pointers (introduced in C++ 11):
* **unique pointers**
* **shared pointers** (and the **weak** ones that goes along with them).
One should also mention for legacy the first attempt: **auto pointers**, which were removed in C++ 17: you might encounter them in some libraries, but by all means don't use them yourself (look for *sink effect* on the Web if you want to know why).
By design all smart pointers keep the whole syntax semantic:
* `*` to dereference the (now smart) pointer.
* `->` to access an attribute of the underlying object.
Smart pointers are clearly a very good way to handle the ownership of a given object.
This does not mean they supersede entirely ordinary (often called **raw** or **dumb**) pointers: raw pointers might be a good choice to pass an object as a function parameter (see the discussion for the third question in this [Herb Sutter's post blog](https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/)). The raw pointer behind a smart pointer may be accessed through the `get()` method.
Both smart pointers exposed below may be constructed directly from a raw pointer; in this case they take the responsability of destroying the pointer:
%% Cell type:code id: tags:
``` C++17
#include <memory>
#include <iostream>
struct Foo
{
~Foo()
{
std::cout << "Destroy foo"<< std::endl;
}
};
{
Foo* raw = new Foo;
std::unique_ptr<Foo> unique(raw); // Now unique_ptr is responsible for pointer ownership: don't call delete
// on `raw`! Destructor of unique_ptr will call the `Foo` destructor.
}
```
%% Cell type:markdown id: tags:
## `unique_ptr`
This should be your first choice for a smart pointer.
The idea behind this smart pointer is that it can't be copied: there is exactly one instance of the smart pointer, and when this instance becomes out of scope the ressources are properly released.
In C++ 11 you had to use the classic `new` syntax to create one, but C++ 14 introduced a specific syntax `make_unique`:
%% Cell type:code id: tags:
``` C++17
#include <memory>
{
auto ptr = std::make_unique<int>(5);
}
```
%% Cell type:markdown id: tags:
The parenthesis takes the constructor arguments.
The smart pointer can't be copied, but it can be moved:
%% Cell type:code id: tags:
``` C++17
#include <memory>
{
auto ptr = std::make_unique<int>(5);
auto copy = ptr; // COMPILATION ERROR: can't be copied!
}
```
%% Cell type:code id: tags:
``` C++17
#include <memory>
#include <iostream>
{
auto ptr = std::make_unique<int>(5);
auto copy = std::move(ptr);
// std::cout << "Beware as now there are no guarantee upon the content of ptr: " << *ptr << std::endl;
// < This line is invalid (using `ptr` after move is undefined behaviour) and makes Xeus-cling crash
}
```
%% Cell type:markdown id: tags:
As usual with move semantics, beware in this second case: ptr is undefined after the `move` occurred... (this code run on [Coliru](http://coliru.stacked-crooked.com/a/a1aa87e64f64c9e8) leads to a more explicit segmentation fault).
### Usage to store data in a class
`std::unique_ptr` are a really good choice to store objects in a class, especially ones that do not have a default constructor. The underlying object may be accessed through reference or raw pointer; usually your class may look like:
%% Cell type:code id: tags:
``` C++17
#include <string>
// Class which will be stored in another one through a `unique_ptr`
class Content
{
public:
Content(std::string&& text); // notice: no default constructor!
const std::string& GetValue() const;
private:
std::string text_ = "";
};
```
%% Cell type:code id: tags:
``` C++17
Content::Content(std::string&& text)
: text_(text)
{ }
```
%% Cell type:code id: tags:
``` C++17
const std::string& Content::GetValue() const
{
return text_;
}
```
%% Cell type:code id: tags:
``` C++17
#include <memory>
class WithUniquePtr
{
public:
WithUniquePtr(std::string&& text);
const Content& GetContent() const; // adding `noexcept` would be even better but Xeus-cling
// doesn't like it!
private:
// A pointer of sort is required here:
// - No default constructor so `Content` can't be stored directly.
// - A reference would mean the object is effectively stored elsewhere; we assume
// we intend here to store the content in the current class.
std::unique_ptr<Content> content_ = nullptr;
};
```
%% Cell type:code id: tags:
``` C++17
WithUniquePtr::WithUniquePtr(std::string&& text)
: content_(std::make_unique<Content>(std::move(text)))
{ }
```
%% Cell type:code id: tags:
``` C++17
#include <cassert>
const Content& WithUniquePtr::GetContent() const
{
assert(content_ != nullptr);
return *content_;
}
```
%% Cell type:markdown id: tags:
Doing so:
* `Content` is stored by a `unique_ptr`, which will manage the destruction in due time of the object (when the `WithUniquePtr` object will be destroyed).
* `Content` object might be manipulated through its reference; end-user don't even need to know ressource was stored through a (smart) pointer:
%% Cell type:code id: tags:
``` C++17
#include <iostream>
void PrintContent(const Content& content)
{
std::cout << content.GetValue() << std::endl;
}
```
%% Cell type:code id: tags:
``` C++17
{
WithUniquePtr obj("My priceless text here!");
decltype(auto) content = obj.GetContent();
PrintContent(content);
}
```
%% Cell type:markdown id: tags:
### Releasing a `unique_ptr`
To free manually the content of a `unique_ptr`:
* Use `release()` method:
%% Cell type:code id: tags:
``` C++17
{
auto ptr = std::make_unique<int>(5);
ptr.release(); // Beware: `.` and not `->` as it is a method of the smart pointer class, not of the
// underlying class!
}
```
%% Cell type:markdown id: tags:
* Or assign `nullptr` to the pointer
%% Cell type:code id: tags:
``` C++17
{
auto ptr = std::make_unique<int>(5);
ptr = nullptr;
}
```
%% Cell type:markdown id: tags:
## `shared_ptr`
The philosophy of `shared_ptr` is different: this kind of smart pointers is fully copyable, and each time a copy is issued an internal counter is incremented (and decremented each time a copy is destroyed). When this counter reaches 0, the underlying object is properly destroyed.
As for `unique_ptr`, there is a specific syntax to build them (properly named `make_shared`...); it was introduced earlier (C++ 11) and is not just cosmetic: the compiler is then able to store the counter more cleverly if you use `make_shared` rather than `new` (so make it so!).
%% Cell type:code id: tags:
``` C++17
#include <iostream>
#include <memory>
{
std::shared_ptr<double> ptr = std::make_shared<double>(5.);
auto ptr2 = ptr;
std::cout << "Nptr = " << ptr.use_count() << std::endl;
//< Notice the `.`: we access a method from std::shared_ptr, not from the type encapsulated
// by the pointer!
}
```
%% Cell type:markdown id: tags:
`shared_ptr` are clearly useful, but you should always wonder first if you really need them: for most uses a `unique_ptr` eventually seconded by raw pointers extracted by `get()` is enough.
There is also a risk of not releasing properly the memory is there is a circular dependancy between two `shared_ptr`. A variation of this pointer named `weak_ptr` enables to circumvent this issue, but is a bit tedious to put into motion. I have written in [appendix](../7-Appendix/WeakPtr.ipynb) to describe how to do so.
%% Cell type:markdown id: tags:
## Efficient storage with vectors of smart pointers
* `std::vector` are cool, but the copy when capacity is exceeded might be very costly for some objects. Moreover, it forces you to provide copy behaviour to your classes intended to be stored in `std::vector`, which is not a good idea if you do not want them to be copied.
* An idea could be to use pointers: copy is cheap, and there is no need to copy the underlying objects when the capacity is exceeded. Another good point is that a same object might be stored in two different containers, and the modifications given in one of this is immediately "seen" by the other (as the underlying object is the same).
However, when this `std::vector` of pointers is destroyed the objects inside aren't properly deleted, provoking memory leaks.
The way to combine advantages without retaining the flaws is to use a vector of smart pointers:
%% Cell type:code id: tags:
``` C++17
#include <array>
class NotCopyable
{
public:
NotCopyable(double value);
~NotCopyable();
NotCopyable(const NotCopyable& ) = delete;
NotCopyable& operator=(const NotCopyable& ) = delete;
NotCopyable(NotCopyable&& ) = delete;
NotCopyable& operator=(NotCopyable&& ) = delete;
private:
std::array<double, 1000> data_;
};
```
%% Cell type:code id: tags:
``` C++17
NotCopyable::NotCopyable(double value)
{
data_.fill(value);
}
```
%% Cell type:code id: tags:
``` C++17
#include <iostream>
NotCopyable::~NotCopyable()
{
std::cout << "Call to NotCopyable destructor!" << std::endl;
}
```
%% Cell type:code id: tags:
``` C++17
#include <vector>
#include <iostream>
{
std::vector<std::unique_ptr<NotCopyable>> list;
for (double x = 0.; x < 8.; x += 1.1)
{
std::cout << "Capacity = " << list.capacity() << std::endl;
list.emplace_back(std::make_unique<NotCopyable>(x)); // emplace_back is like push_back for rvalues
}
}
```
%% Cell type:markdown id: tags:
Doing so:
- The `NotCopyable` are properly stored in a container.
- No costly copy occurred: there were just few moves of `unique_ptr` when the capacity was exceeded.
- The memory is properly freed when the `list` becomes out of scope.
- And as we saw in previous section, the underlying data remains accessible through reference or raw pointer if needed.
%% Cell type:markdown id: tags:
#### Using a trait as syntactic sugar
I like to create aliases in my classes to provide more readable code:
%% Cell type:code id: tags:
``` C++17
#include <array>
#include <vector>
class NotCopyable2
{
public:
// Trait to alias the vector of smart pointers.
using vector_unique_ptr = std::vector<std::unique_ptr<NotCopyable2>>;
NotCopyable2(double value);
NotCopyable2(const NotCopyable2& ) = delete;
NotCopyable2& operator=(const NotCopyable2& ) = delete;
NotCopyable2(NotCopyable2&& ) = delete;
NotCopyable2& operator=(NotCopyable2&& ) = delete;
private:
std::array<double, 1000> data_; // not copying it too much would be nice!
};
```
%% Cell type:code id: tags:
``` C++17
NotCopyable2::NotCopyable2(double value)
{
data_.fill(value);
}
```
%% Cell type:code id: tags:
``` C++17
#include <iostream>
#include<vector>
{
// Use the alias
NotCopyable2::vector_unique_ptr list;
// or not: it amounts to the same!
std::vector<std::unique_ptr<NotCopyable2>> list2;
// std::boolalpha is just a stream manipulator to write 'true' or 'false' for a boolean
std::cout << std::boolalpha << std::is_same<NotCopyable2::vector_unique_ptr, std::vector<std::unique_ptr<NotCopyable2>>>() << std::endl;
}
```
%% Cell type:markdown id: tags:
This simplifies the reading, especially if templates are also involved...
%% Cell type:markdown id: tags:
## The cost of using smart pointers
This [article](https://www.modernescpp.com/index.php/memory-and-performance-overhead-of-smart-pointer) provides an analysis of the cost involved both in performance and memory when using a smart pointer. To put in the nutshell, its conclusions are:
- `std::unique_ptr` bears no overhead in memory (except in a very edge case you can safely ignore for now) and little overhead in performance, at least for the latter when compiler optimizations are enabled (we will talk about them in a later [notebook](../6-InRealEnvironment/3-Compilers.ipynb)).
- `std::shared_ptr` incurs a memory overhead (to keep the reference count) and a performance overhead as well (which is partially mitigated if the memory was allocated through `std::make_shared`). The performance overhead is especially important when no optimizations are involved.
This highlights once more what we said earlier:
- Use smart pointers: RAII is simply too precious to manage properly your ressources!
- Use `std::unique_ptr` wherever you can, and use `std::shared_ptr` when you really need several instances of the same ressource.
%% Cell type:markdown id: tags:
# References
[<a id="cit-Alexandrescu2001" href="#call-Alexandrescu2001">Alexandrescu2001</a>] Andrei Alexandrescu, ``_Modern C++ Design: Generic Programming and Design Patterns applied_'', 01 2001.
%% Cell type:markdown id: tags:
© _CNRS 2016_ - _Inria 2018-2021_
_This notebook is an adaptation of a lecture prepared by David Chamont (CNRS) under the terms of the licence [Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](http://creativecommons.org/licenses/by-nc-sa/4.0/)_
_The present version has been written by Sébastien Gilles and Vincent Rouvreau (Inria)_
Loading