Idiomes et design patterns fournissent tous deux des solutions éprouvées à des problèmes récurrents du développement logiciel, mais ils diffèrent par leur portée, leur granularité et leur degré de formalisation :
- Portée:
- Idiomes: les idiomes sont de petites techniques ou motifs de codage propres à un langage, qui répondent à des défis de programmation spécifiques au sein d'un langage donné. Ils consistent souvent à exploiter les fonctionnalités ou conventions du langage pour obtenir le résultat souhaité de manière efficace.
- Design patterns: les design patterns sont des solutions architecturales de plus haut niveau, indépendantes du langage, à des problèmes de conception courants en génie logiciel. Ils fournissent des gabarits réutilisables pour résoudre des problèmes de conception et promouvoir les bonnes pratiques.
- Granularité:
- Idiomes: les idiomes tendent à être plus fins et centrés sur des constructions ou techniques de codage spécifiques au sein d'un seul langage de programmation. Ils impliquent souvent de manipuler les fonctionnalités ou la syntaxe du langage pour atteindre des objectifs particuliers.
- Design patterns: les design patterns sont plus larges et traitent de concepts de conception plus vastes et des relations entre composants au sein d'un système logiciel. Ils fournissent des gabarits pour organiser et structurer le code à un niveau d'abstraction plus élevé.
- Formalisation:
- Idiomes: les idiomes sont typiquement informels et se transmettent par l'expérience, les revues de code ou la littérature de programmation au sein d'une communauté donnée. Ils n'ont pas toujours de nom formel ni de documentation.
- Design patterns: les design patterns sont des solutions plus formalisées et bien documentées à des problèmes de conception courants. Ils ont souvent des noms reconnus, des descriptions et des directives d'implémentation exposées dans des ouvrages comme le livre du Gang of Four (GoF) « Design Patterns: Elements of Reusable Object-Oriented Software ».
Dans ce billet, nous allons explorer ces idiomes C++ courants :
- 1 - RAII
- 2 - Pimpl
- 3 - Curiously Recurring Template Pattern (CRTP)
- 4 - Copy-and-swap
- 5 - Type Erasure
- 6 - Non-Virtual Interface (NVI)
- 7 - SFINAE (Substitution Failure Is Not An Error)
La première partie se concentre sur les idiomes RAII et Pimpl :
RAII (Resource Acquisition Is Initialization)
En C++, l'un des idiomes les plus couramment utilisés est RAII (Resource Acquisition Is Initialization, « l'acquisition de ressource est une initialisation »). RAII est une technique puissante et largement adoptée pour gérer des ressources telles que la mémoire, les descripteurs de fichiers, les connexions réseau et les verrous de manière déterministe et sûre face aux exceptions. Elle lie la durée de vie des ressources à celle des objets, garantissant que les ressources sont correctement acquises et libérées.
Voici comment fonctionne RAII et où il est couramment utilisé :
1-Gestion de la mémoire: RAII est massivement utilisé en C++ pour gérer l'allocation de mémoire dynamique à l'aide de pointeurs, en particulier avec std::unique_ptr et std::shared_ptr de la bibliothèque standard C++. Ces classes de pointeurs intelligents garantissent que la mémoire est automatiquement libérée lorsque l'objet contenant le pointeur sort de sa portée.
{
std::shared_ptr<int> ptr(new int); // Resource acquired
// Use ptr
} // Resource released automatically when ptr goes out of scope
2-Gestion des fichiers: RAII est employé pour gérer les ressources fichiers, en garantissant que les fichiers sont correctement fermés après usage, même en présence d'exceptions ou de retours anticipés.
{
std::ifstream file("example.txt"); // Resource acquired
// Read from file
} // Resource released automatically when file goes out of scope
3-Mécanismes de verrouillage: RAII est utilisé pour gérer les verrous afin de garantir que les sections critiques du code sont correctement synchronisées et que les verrous sont libérés lorsqu'ils ne sont plus nécessaires.
{
std::ifstream file("example.txt"); // Resource acquired
// Read from file
} // Resource released automatically when file goes out of scope
4-Gestion des ressources dans des classes personnalisées: les développeurs implémentent souvent RAII dans leurs propres classes pour gérer des ressources personnalisées telles que les connexions à des bases de données, les sockets réseau et les ressources GPU.
RAII améliore la sûreté des ressources, réduit le risque de fuites de ressources et de pointeurs pendants, et simplifie la gestion des ressources en les liant à la durée de vie des objets. Il est considéré comme l'une des pierres angulaires de la programmation C++ moderne et est largement utilisé dans tout le langage et sa bibliothèque standard.
Pimpl
L'idiome Pimpl, qui signifie « Pointer to Implementation » (pointeur vers l'implémentation), est une technique de conception C++ utilisée pour masquer les détails d'implémentation d'une classe à ses utilisateurs. Elle aide à améliorer l'encapsulation, à réduire les dépendances à la compilation et à limiter les recompilations lorsque les détails d'implémentation changent.
Voici comment fonctionne l'idiome Pimpl :
- Séparation de l'interface et de l'implémentation: avec l'idiome Pimpl, l'interface publique d'une classe est définie dans son fichier d'en-tête, tandis que les détails d'implémentation privés sont encapsulés dans un fichier d'implémentation séparé. Cette séparation permet aux clients de la classe d'interagir uniquement avec son interface publique, en masquant la complexité de l'implémentation.
- Pointeur vers l'implémentation: au lieu d'inclure directement les détails d'implémentation dans la déclaration de la classe, celle-ci contient un pointeur vers une classe d'implémentation opaque (c'est-à-dire déclarée de manière anticipée). Ce pointeur est typiquement déclaré comme membre privé de la classe.
- Déclaration anticipée: comme les détails d'implémentation sont masqués aux utilisateurs de la classe, seule la déclaration de la classe d'implémentation est nécessaire dans le fichier d'en-tête. Cela est réalisé grâce à une déclaration anticipée (
class Impl;), ce qui évite d'inclure les détails d'implémentation dans l'en-tête. - Dépendances de compilation réduites: en séparant l'interface de l'implémentation et en utilisant des déclarations anticipées, les modifications des détails d'implémentation (ajout, suppression ou modification de membres privés) ne nécessitent pas la recompilation du code qui utilise l'interface de la classe. Cela peut réduire sensiblement les temps de build, en particulier dans les grandes bases de code.
- Allocation de mémoire dynamique: typiquement, la classe d'implémentation est allouée dynamiquement sur le tas à l'aide de
new, et le pointeur vers elle est géré par la classe publique. Cela permet le polymorphisme dynamique et l'utilisation des design patterns « bridge » et « strategy ».
Voici un exemple simplifié illustrant l'idiome Pimpl :
// MyClass.h (Header file)
#pragma once
class MyClass {
public:
MyClass();
~MyClass();
void doSomething();
private:
class Impl; // Forward declaration
Impl* pImpl; // Pointer to implementation
};
// MyClass.cpp (Implementation file)
#include "MyClass.h"
class MyClass::Impl {
public:
void doInternalWork() {
// Implementation details...
}
};
MyClass::MyClass() : pImpl(new Impl()) {}
MyClass::~MyClass() {
delete pImpl;
}
void MyClass::doSomething() {
pImpl->doInternalWork();
}
En employant l'idiome Pimpl, les détails d'implémentation de MyClass sont masqués aux utilisateurs de la classe, et les modifications de l'implémentation peuvent être effectuées sans affecter le code client ni exiger la recompilation des fichiers dépendants. Cela améliore la modularité, la maintenabilité et les temps de build des projets C++.
