Rvalue reference et move semantic 1 – Problématiques rencontrées dans le C++03

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 :

  1. Problématiques rencontrées dans le C++03
  2. Quelques définitions
  3. Les nouveaux concepts du C++11
  4. Les rvalue references
  5. Analyse de quelques implémentations
  6. Ce que cela change pour la STL
  7. Lectures de références

Problématiques rencontrées dans le C++03

Voyons dans un premier temps quelques exemples de code écris en C++03 et les différents problèmes qu’ils peuvent poser.

Copie et sémantique d’entité

Rappel sur les sémantiques de valeur et d’entité

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 :

Implémentation de l’opérateur d’affectation

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.

Variable temporaire

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.

(Named) Return Value Optimization (NRVO)

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.

Implémentation de swap

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.

Passage de paramètres

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.

Conclusion

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.

Advertisements

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s