Xavier Lamorlette

C++ : Idiomes

Sommaire :

Copy and Swap

Implémentation de l'assignation par copie (copy assignment) par un passage de paramètre par valeur, qui appelle le constructeur par copie (copy constructor), puis par l'appel à une méthode swap(), qui échange les zones mémoires, sur ce paramètre.

Cette méthode swap() peut être utilisée pour le constructeur par mouvement (move constructor).

class A {
    A() {…}
    A(const A &) {…}

    A & operator =(A other) {
      swap(other);
      return * this;
    }

    A(A && other):
        A() {
      swap(other);
    }

    void swap(A & other) {…}
};

Double Dispatch

On ne peut pas utiliser le polymorphisme par héritage dans la résolution des surcharges de fonctions. Le code suivant ne fonctionne pas comme on pourrait l'espérer, car la résolution des surcharges est faite à la compilation :

class A {…};
class B: A {…};

void foo(A &) {}
void foo(B &) {}

{
    B b;
    A & a = b;
    foo(a);
}
Il faut utiliser le “double dispatch” :
class A {
    virtual void call_foo() {
        foo(* this);
    }
};

class B: A {
    void call_foo() override {
        foo(* this);
    }
};

void foo(A &) {…}
void foo(B &) {…}

{
    B b;
    A & a = b;
    a.call_foo();
}

Si foo est une classe, c'est le design pattern Visitor.

D'une manière plus générale, le multiple dispatch est le polymorphisme simultané sur plusieurs objets.

“« En masse » delegation”

class Proxy {
public:
    Proxy():
        impl(std::make_unique<Real_object>()) {
    }
    Real_object * operator->() {
        return impl;
    }
private:
    std::unique_ptr<Real_object> impl;
};

Ceci peut permettre d'étendre une classe, en faisant une sorte de design pattern Decorator :

class A {
public:
    int x;
};

class A_extended {
public:
    A_extended(A & a):
        a(a) {
    }
    A * operator -> () {
        return & a;
    }
    int sum(int y) const {
        return a.x + y;
    }
private:
    A & a;
};

{
    A a{12};
    A_extended a_extended(a);
    std::cout << a_extended->x << std::endl;
    std::cout << a_extended.sum(3) << std::endl;
}

Erase - Remove

Pour effacer efficacement plusieurs éléments d'un vecteur :

#include <algorithm>
#include <vector>
{
  std::vector<T> my_vector;
  T value;
  my_vector.erase(std::remove(my_vector.begin(), my_vector.end(), value), my_vector.end());
}

Ceci n'est plus nécessaire en C++20 grâce au nouvel algorithme std::erase (et aussi std::erase_if).

Mixin

Une classe mixin fournit des méthodes à d'autres classes sans en être parente.
En C++ : classe template qui hérite de la classe paramêtre template.

Le mixin est aussi appelé “Curiously Recurring Template Pattern”, “F-bound polymorphism”, ou “upside-down inheritance”.

Cela permet notamment d'implémenter :

Pour empêcher une mauvaise utilisation du mixin (empêcher de faire class Derived: Base<Other>), on peut rendre le constructeur de Base privé :

template <typename T>
class Base {
public:
    void interface() {
        static_cast<T *>(this)->implementation();
    }
private:
    Base() = default;
    friend T;
};

class Derived: Base<Derived> {
    void implementation();
};

PImpl

Cas particulier du design pattern Bridge.
“Pointer to implementation”, “compilation firewall” : cache les détails d'implémentation d'une classe dans une sous-classe dédiée que l'on accède via un “opaque pointer”.

Cela permet de :

Choix de conception : si on ne met dans la classe d'implémentation que les membres et méthodes privés, en laissant ceux publics dans la classe publique, on peut avoir des problèmes pour appeler des méthodes publiques depuis la classe d'implémentation. Par exemple, il faut rajouter dans la classe d'implémentation un pointeur vers la classe publique.

// A.hpp
class A {
public:
    A();
    ~A();

private:
    class Impl;
    std::unique_ptr<Impl> impl;
};

// A.cpp
class A::Impl {
…
};

A::A():
    impl(std::make_unique<Impl>()) {
}

A::~A() = default;

RAII

RAII: Resource Acquisition Is Initialisation: utilisation du destructeur, dont l'appel est garanti même en cas d'exception, pour libérer une ressource à la fin de la portée de l'instance. La ressource est un invariant de classe lié à son existence.

Note : lorsqu'il y a plusieurs ressources à gérer, les variables locales sont détruites dans l'ordre inverse de leur construction.

Ceci est utilisé par les std::shared_ptr et les std::unique_ptr.

Son utilisation est fortement recommandée pour le contrôle des mutex, des allocations dynamiques de mémoire, des fichiers et des sockets.

La dernière mise à jour de cette page date de mars 2022.

Le contenu de ce site est, en tant qu'œuvre originale de l'esprit, protégé par le droit d'auteur.
Pour tout commentaire, vous pouvez m'écrire à xavier.lamorlette@gmail.com.