Quelques ressources sur le C++ moderne (C++ 11 et C++ 14)

C++

C++A l’occasion d’une nouvelle session de la formation sur les nouveautés de C++ 11 et C++ 14, j’ai mis fait une mise à jour d’articles sur ces nouvelles versions et sur l’écriture de C++ moderne.

Surcharger l’opérateur new pour empêcher l’allocation dynamique d’un objet en C++

Parfois, il est nécessaire de restreindre des objets pour qu’ils ne puissent être utilisés qu’au travers de variables locales. Un cas typique est une classe qui utilise des ressources, allouées logiquement dans son constructeur, et libérées dans son destructeur.

Voici un exemple que j’utilise parfois dans les formations au C++.

Tout va bien si le programme détruit les objets lorsqu’ils n’en a plus besoin.
C’est le cas si on emploie ces objets à l’aide de variables locales : lorsque le programme quitte la portée dans laquelle la variable existe (nous présumons bien sûr que la variable n’est pas statique), l’objet est détruit, et le passage dans le destructeur libère les ressources allouées dans le constructeur.

Le risque est qu’un développeur crée un objet en utilisant une allocation dynamique, car il pourrait ensuite oublier de détruire cet objet, bloquant ainsi les ressources.

Une solution pour supprimer ce risque consiste tout simplement à surcharger l’opérateur new de notre classe, de façon à empêcher l’allocation dynamique d’objets de cette classe. Voici un petit exemple qui montre le principe :

class maClasse {
public:
      string chaine;
      maClasse() {
// allocation des ressources
     chaine = "Les ressources sont allouees";
};
~maClasse() {
// libération des ressources
      cout << "Les ressources sont liberees" << endl;
}
void *operator new (size_t taille) {
throw 0;
return new int; // peut renvoyer n'importe quoi : on ne passera jamais ici
}
};
int main(int argc, char* argv[]) {
{ 
// bloc pour montrer la réservation et la libération des ressources
       maClasse C1;
      cout << C1.chaine << endl;
}
try {
      maClasse * pC = new maClasse;
      cout << pC->chaine << endl;
}
catch (int e) {
      cout << "Exception : " << e << endl;
}
int attente;
cin >> attente;
return 0;
}

La sortie nous montre que nous avons bien utilisé la variable locale, en allouant, puis en libérant les ressources, mais que l’utilisation d’une allocation dynamique échoue :

$ ./essai
Les ressources sont allouees
Les ressources sont liberees
Exception : 0

 

La magie du C++

Non vous ne rêvez pas, j’ai bien utilisé le mot magie dans le titre d’un article sur le blog On ne fait pas de la magie ! Mais vous vous doutez bien qu’il y a anguille sous roche et qu’on va faire de l’anti-magie.

L’une des opinions courantes sur le C++ est que c’est un langage difficile à employer, car il est facile de commettre des erreurs de programmation qui se traduisent par des fuites mémoires ou l’accès à des objets qui n’existent plus. En contrepartie, il est souvent également admis que le code C++ est plus rapide à l’exécution que du code Java ou C#, pour lesquels les fuites mémoires n’existent pas.

Quelles sont les raisons sous-jacentes de ces opinions ? La principale est que le développeur C++ doit gérer lui même la durée de vie des objets qu’il utilise. Cela se traduit souvent par la contrainte d’avoir à écrire un constructeur copie ou à surcharger l’opérateur d’affectation d’une classe. Mais cela lui offre la souplesse de maitriser l’instant exact où une ressource est libérée, qu’elle soit une ressource mémoire ou une connexion à une base de données.

Notamment, cela permet d’écrire une application en utilisant le principe RAII (Resource Acquisition Is Initialisation) sans avoir à veiller à chaque utilisation de sa classe à l’encapsuler avec une instruction using ou à ne pas oublier une clause finally d’un bloc try. C++ ne dispose pas de using ni de finally parce qu’il n’en a tout simplement pas besoin …

De ce fait, le développeur a moins d’instructions à écrire et l’application moins de code à exécuter : il n’y a pas de besoin de code supplémentaire exécuté dans un thread séparé pour faire le ménage dans les objets à détruire. Cerise sur le gâteau, les performances d’une application C++ sont déterministes, alors qu’une application Java ou C# peut présenter des ralentissements aléatoires déclenchées par le passage du garbage collector.

Ces opinions risquent de se renforcer à juste titre avec l’utilisation judicieuse des nouvelles fonctionnalités du C++ 11, maintenant largement adopté par les principaux compilateurs actuels. En effet, les nouveaux types de smart pointers, comme unique_ptr ou shared_ptr, ou la notion de RValues, qui permet d’écrire un constructeur move vont continuer à faciliter l’écriture d’application plus efficaces, en réduisant le nombre de lignes de code source écrites par le développeur, ainsi que le nombre d’instructions machine exécutées.

Le constructeur move est particulièrement efficace de ce point de vue, en évitant la création d’objets temporaires qui peuvent nécessiter des tailles mémoire importantes et des temps d’initialisation conséquents.

Pour obtenir ces résultats, qui ne sont pas de la magie, mais l’utilisation de mécanismes très bien pensés, le développeur C++ doit consacrer un temps d’apprentissage pour maitriser ces nouvelles notions. C’est le message que, pour ma part, je fais passer dans les formations C++, notamment la formation aux nouveautés C++ 11, que j’anime.