La dernière norme du C++ propose de nouvelles syntaxes destinées à améliorer la lisibilité et/ou les performances du code. L’une de ces nouvelles fonctionnalités du C++11 qui présente probablement le plus de difficultés à comprendre, aussi bien pour les débutants que pour les personnes plus expérimentées, est la sémantique de déplacement et les références de rvalue.
Cette série d'articles a été rédigée il y a quelques mois, mais je n'avais pas pris le temps de les mettre en forme pour blog. Ce premier article présente les problématiques rencontrées en C++03, le plan de l'ensemble des articles sera le suivant :
Voyons dans un premier temps quelques exemples de code écris en C++03 et les différents problèmes qu’ils peuvent poser.
La conception des classes en programmation orientée objet doit respecter un certain nombre de principes pour garantir la robustesse et l’évolutivité du code. Ainsi, il est classique de distinguer deux types de classes selon ce qu’elles représentent et comment elles sont manipulées : les objets à sémantique de valeur et les objets à sémantique de copie. L’une des différences entre ces deux types de classes concerne le constructeur par copie et l’opérateur d’affectation. Pour la sémantique de valeur, pour laquelle deux objets distincts peuvent représenter la même chose, cela a un sens de définir la copie. Pour la sémantique d’entité, pour laquelle deux objets distincts ne peuvent représenter la même chose, la copie n’a pas de sens et ne doit pas être définie.
Pour les détails, voir les différents liens :
Considérons une classe X qui manipule une ressource m_ressource, coûteuse à construire, à copier ou à libérer (par exemple la classe vector de la STL). Le pseudo-code suivant présente les étapes qui doivent être réalisées dans l’opérateur d’affectation :
X& X::operator= (X const& rhs) { // détruire la ressource actuelle // dupliquer la ressource rhs.m_ressource // l’attacher à this->m_ressource }
Cet opérateur est appelé lors de l’affectation d’un objet dans un autre :
X x1; X x2; // utilisation de x1 et x2 x1 = x2;
Dans ce cas, la destruction de la ressource utilisée par x1 et la duplication de la ressource de x2 est nécessaire.
Le problème survient lors de l’utilisation d’un variable temporaire. Par exemple, le code suivant :
X foo(); X x; // utilisation de x x = foo();
Dans ce cas, la destruction de la ressource de x est encore nécessaire, mais la duplication de la ressource utilisée dans la variable temporaire retournée par la fonction est inutile, ce qui implique un surcoût inutile.
Cette optimisation est généralement réalisée automatiquement par le compilateur, mais il est possible de l’utiliser explicitement. Elle consiste à éviter la copie lors du retour d’une fonction en passant la variable en entrée de la fonction. Sans l'optimisation NRVO :
A foo() { A a; // traitement complexe sur a return a; } A a = foo(); // copie du temporaire
Le code précédent peut être remplacé par le code suivant, pour éviter la copie lors du retour de la fonction.
A foo(A & a) { // traitement complexe sur a return a ; } A a; foo(a); // plus de copie du temporaire
Cependant, il manque un mécanisme permettant d'utiliser un objet temporaire retourné pour une fonction, sans avoir le surcoût d'une copie. Nous verrons dans les prochains articles comment la sémantique de déplacement répond à cette problématique.
La fonction swap permet d’échanger le contenu de deux variables. Une implémentation classique peut être la suivante :
template<class T> swap(T & a, T & b) { T temp = a; a = b; b = temp; }
Encore une fois, dans le cas où la copie est coûteuse, cette implémentation pose des problèmes. Une méthode pour éviter la copie est de fournir une fonction membre swap, qui réalise l’échange des données sans copie. C’est notamment ce que font les classes std::string et std::vector. La fonction libre std::swap réalise la copie des données dans le cas général, sauf pour certaines classes de la STL (comme les classes std::string et std::vector), pour lesquelles il existe une surcharge de std::swap pour utiliser la fonction membre swap de ces classes.
Dans ce cas aussi, il manque en C++03 un mécanisme permettant d'éviter la copie des données. La sémantique de déplacement du C++11 pourra répondre également à cette problématique.
Dans certain cas, les paramètres passés dans une fonction sont directement transmis à d’autres fonctions. Par exemple, la fonction make_shared permet de créer un objet et un pointeur intelligent partagé sur un objet. Les paramètres passés à la fonction make_shared doivent être directement transmis au constructeur de l’objet. Un exemple d’implémentation de cette fonction make_shared peut être la suivante :
template<typename T, typename Arg> shared_ptr<T> make_shared(Arg arg) { return shared_ptr<T>(new T(arg)); }
Dans le cas où le constructeur de T prend son argument avec une référence, cela veut dire que le paramètre est copié inutilement. Une autre implémentation utilisant les références permet de corriger ce problème :
template<typename T, typename Arg> shared_ptr<T> make_shared(Arg & arg) { return shared_ptr<T>(new T(arg)); }
Un nouveau problème se présente, cette fonction ne peut être utilisée avec un objet temporaire, par exemple une constante littérale ou une variable temporaire retournée par une fonction :
make_shared<X>(foo()); // erreur make_shared<X>(123); // erreur Ce problème peut être réglé facilement, en utilisant une référence constante : template<typename T, typename Arg> shared_ptr<T> make_shared(Arg const& arg) { return shared_ptr<T>(new T(arg)); }
Différents problèmes subsistent. Premièrement, il va falloir fournir plusieurs implémentations de cette fonction, en utilisant le passage par copie, par référence et par référence constante, suivant les cas. Ensuite, s’il existe des constructeurs prenant plusieurs paramètres, il sera nécessaire de fournir les différentes combinaisons de chaque version constante et non constante des paramètres.
Les values references permettent d'implémenter une technique appelée perfect forwarding, qui permet la compatibilité des types passés en fonction.
Dans ce premier article, vous avez vu quelques problématiques, qui peuvent trouver des solutions en C++03, mais dont l'implémentation peut être assez lourd. Les rvalues references et la sémantique de déplacement permettent d'utiliser des implémentations plus élégantes et légères.