Pointeurs intelligents

L'une des fonctionnalité majeure introduit par le C++11 est les smart pointers. C'est une reprise de la librairie BOOST. L'utilisation de pointeurs est indispensable à une bonne conception (observer, injection de dépendance, factory, ...). Or, la bonne gestion des pointeurs devient un casse-tête à partir du moment que le pointeur est partagé.

Utiliser std::unique_ptr pour la gestion d'une ressource à propriété exclusive

Il remplace la précédente fonctionnalité std::auto_ptr qui souffrait des limites du C++98 (notion de déplacement inexistante, conteneurs non adaptés). std::unique_ptr est souvent utilisé comme type de retour dans les fonctions fabrique d'objet.


In [1]:
.rawInput


Using raw input
Out[1]:


In [2]:
#include <iostream>
#include <string>
#include <map>
#include <memory>
#include <functional>

class Animal
{
public:
    virtual ~Animal()
    {
        std::cout << "Animal dort" << std::endl;
    }
    virtual void crie() const = 0;
};

class Chien : public Animal
{
private:
    std::string nom;
public:
    Chien()
    : nom("Le chien")
    {}
    Chien(const std::string& pNom)
    : nom(pNom)
    {}
    virtual ~Chien() = default;
    virtual void crie() const override
    {
        std::cout << nom << " aboie" << std::endl;
    }
};

class Chat : public Animal
{
private:
    std::string nom;
public:
    Chat()
    : nom("Le chat")
    {}
    Chat(const std::string& pNom)
    : nom(pNom)
    {}
    virtual ~Chat() = default;
    virtual void crie() const override
    {
        std::cout << nom << " miaule" << std::endl;
    }
};

template <typename ...Ts>
class FabriqueAnimal
{
private:   
    using key_t = std::string;
    using fabrique_t = std::function<std::unique_ptr<Animal>(Ts...)>;
    std::map<key_t, fabrique_t> fabriqueMap;

    template <typename T>
    static auto creeFabrique()
    {
        return [](auto&& ...params){return std::make_unique<T>(std::forward<decltype(params)>(params)...);};
    }
  
public:
    FabriqueAnimal()
    {
        fabriqueMap["chien"] = creeFabrique<Chien>();
        fabriqueMap["chat"] = creeFabrique<Chat>();
    }
    
    std::unique_ptr<Animal> cree(const key_t& key, Ts&&... params) const
    {
        std::unique_ptr<Animal> animal = nullptr;
        const auto it = fabriqueMap.find(key);
        if (it != fabriqueMap.end())
        {
            animal = it->second(std::forward<Ts>(params)...);
        }
        return animal;
    }
};


Out[2]:


In [3]:
.rawInput


Not using raw input
Out[3]:


In [4]:
{
    const FabriqueAnimal<> animalerie;
    const std::unique_ptr<Animal> dog = animalerie.cree("chien");
    dog->crie();
}

std::cout << std::endl;

using FabriqueAnimalNomme = FabriqueAnimal<std::string>;
const FabriqueAnimalNomme refuge;
refuge.cree("chat", "nyan cat")->crie();


Le chien aboie
Animal dort

nyan cat miaule
Animal dort
Out[4]:
(void) @0x7fbf68825b18

Utiliser std::shared_ptr pour la gestion d'une ressource à propriété partagée

Un peu plus volumineux que std::unique_ptr, la fonctionnalité std::shared_ptr s'avère d'une grande efficacité lorsqu'il faut partager des pointeurs.


In [5]:
using vector_ptr_t = std::vector<std::shared_ptr<Animal>>;
vector_ptr_t vaccine;
{
    vector_ptr_t animaux;
    animaux.push_back(refuge.cree("chat", "nyan cat"));
    animaux.push_back(refuge.cree("chien", "duck hunt dog"));

    // copie des pointeurs
    for(auto&& animal : animaux)
    {
        vaccine.push_back(animal);
    }
    animaux.push_back(refuge.cree("chien", "lassie"));
}
for(auto&& animal : vaccine)
{
    animal->crie();
}


Animal dort
nyan cat miaule
duck hunt dog aboie
Out[5]:

Remarque: Il faut toutefois faire attention lors de l'utilisation de pointeurs bruts (exemple: this). Cependant, la STL propose des mécanismes pour manier ces pointeurs en toute fiabilité (std::enable_shared_from_this).

Utiliser std::weak_ptr pour les pointeurs partagés std::shared_ptr qui peuvent pendouiller

Les pointeurs sont aussi très utilisés dans le pattern observer. Ce pattern a pour particularité de conserver un pointeur sur l'objet observer mais sans pour autant détenir la ressource. Cependant, les observables doivent s'assurer que le pointeur n'est pas détruit avant d'y accéder. std::weak_ptr répond parfaitement à cette double exigence.

Ce type de pointeur est aussi très utilisé pour la création de cache (Des pointeurs sont mémorisés afin d'éviter une répetition de construction-destruction d'un même objet).


In [6]:
class Notifier
{
public:
    virtual ~Notifier() = default;
    virtual void Notify() = 0;
};

class HelloNotification : public Notifier
{
public:
    HelloNotification()
    {}
    
    virtual void Notify() override
    {
        std::cout << "Hello" << std::endl;
    }
};

class BonjourNotification : public Notifier
{
public:
    BonjourNotification()
    {}
    
    virtual void Notify() override
    {
        std::cout << "Bonjour" << std::endl;
    }
};

class Subject
{
private:
    using vector_ptr = std::vector<std::weak_ptr<Notifier>>; 
    vector_ptr notifiers;
    
    void NotifyAll()
    {
        // Notifier tous les observers
        for (auto&& n : notifiers)
        {
            std::shared_ptr<Notifier> ptr = n.lock();
            if (ptr != nullptr)
            {
                ptr->Notify();
            }
        }
    }
public:
    Subject()
    {}
    
    void Add(const std::weak_ptr<Notifier>& n)
    {
        notifiers.push_back(n);
    }
    
    void Change()
    {
        NotifyAll();
    }
    
    // Méthode de désenregistrement 
    // ... 
};

Subject s;
std::shared_ptr<Notifier> a = std::make_shared<HelloNotification>();
s.Add(a);
{
    // Execution sur un autre thread
    std::shared_ptr<Notifier> b = std::make_shared<BonjourNotification>();
    s.Add(b);
}
s.Change();


Hello
Out[6]:
(void) @0x7fbf68825b18

Utiliser std::make_unique et std::make_shared à la place de new

Pour deux raisons principalement :

  • c'est plus simple à écrire std::make_shared<Objet>() que std::shared_ptr<Objet>(new Objet())
  • c'est plus sûre vis à vis des exceptions. Par exemple, le code suivant produit une fuite mémoire si une exception se produit lors de l'évaluation de getCategory() :

    log(std::shared_ptr<String>(new String("Erreur")), getCategory())

    L'évaluation de new String("Erreur") est réalisée avant getCategory() et std::shared_ptr().

Utiliser les deleters personnalisés pour la libération de ressource qui ne se fait pas par delete

Les librairies C ou les bibliothèques graphiques disposent généralement de fonctions de création et de destruction d'objets. Les pointeurs intelligents permettent de définir l'appel à une fonction de destruction personnalisée.

Le conseil précédent indique d'utiliser une fonction de création à la place du constructeur de std::shared_ptr ou de std::unique_ptr. Heureusement, l'implémentation d'une méthode de création est assez simple.


In [7]:
.rawInput


Using raw input
Out[7]:


In [8]:
template <typename T>
void delete_with_log(T* ptr)
{
    std::cout << "Destruction " << *ptr << std::endl; 
    delete ptr;
}

template <typename T>
using unique_log_ptr = std::unique_ptr<T,decltype(&delete_with_log<T>)>;

template <typename T, typename ...Ts>
unique_log_ptr<T> make_unique_with_log(Ts&&... params)
{
    return unique_log_ptr<T>(new T(std::forward<Ts>(params)...), &delete_with_log<T>);
}

// pour un shared_ptr, la syntaxe est plus simple
template <typename T, typename ...Ts>
std::shared_ptr<T> make_shared_with_log(Ts&&... params)
{
    return std::shared_ptr<T>(new T(std::forward<Ts>(params)...), &delete_with_log<T>);
}


Out[8]:


In [9]:
.rawInput


Not using raw input
Out[9]:


In [10]:
make_unique_with_log<int>(42);
make_shared_with_log<std::string>(2, '0');


Destruction 42
Destruction 00
Out[10]:
(std::shared_ptr<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >) @0x7fbf4a21e1a0

In [ ]: