Que cache le terme « Sémantique de déplacement » ?
\n\nImaginez que vous déménagez dans un nouvel appartement. Vous avez un canapé énorme et lourd. Qu'est-ce qui est le plus rapide : le traîner vous-même sur cinq étages ou simplement donner les clés aux déménageurs en disant : « Les gars, le canapé est à vous, faites-en ce que vous voulez, je n'en ai plus besoin » ? La deuxième option, c'est exactement la sémantique de déplacement en C++.
\n\nAvant l'arrivée de la norme C++11, les programmeurs copiaient péniblement les données. Chaque fois que vous retourniez un vecteur ou une chaîne depuis une fonction, le compilateur créait une copie exacte. Si le vecteur contenait un million d'éléments, il copiait consciencieusement chaque élément. C'était lent, inefficace, et cela ressemblait à recopier un livre à la main au lieu de simplement donner l'original.
\n\nLa sémantique de déplacement (move semantics) est un mécanisme qui permet de déplacer des ressources (mémoire, descripteurs de fichiers, sockets) d'un objet à un autre sans copie coûteuse. Après le déplacement, l'objet source reste dans un état « vide » mais valide, et il peut être détruit ou recevoir une nouvelle valeur en toute sécurité.
\n\nDans cet article, nous allons explorer les rvalue-références, la fonction std::move, les constructeurs de déplacement et les opérateurs d'affectation — en termes simples et avec des exemples de code.
\n\nPourquoi le code ralentit-il sans sémantique de déplacement ?
\n\nRegardons un exemple classique qui a tourmenté tous les programmeurs C++ avant 2011 :
\n\n#include <iostream>\n#include <vector>\n#include <string>\n\nstd::vector<std::string> creerGrosVecteur() {\n std::vector<std::string> vec(1000000, "Bonjour");\n return vec; // Horreur ! Copie d'un million de chaînes !\n}\n\nint main() {\n std::vector<std::string> donnees = creerGrosVecteur();\n std::cout << "Taille : " << donnees.size() << std::endl;\n return 0;\n}\n\nQue se passe-t-il ici ? La fonction creerGrosVecteur crée un vecteur, le remplit de données et le retourne. Le compilateur (sans optimisations) fait ceci :
- \n
- Crée le vecteur à l'intérieur de la fonction. \n
- Copie la TOTALITÉ du vecteur dans la valeur de retour. \n
- Détruit le vecteur original. \n
- Dans
main, copie cette valeur dans la variabledonnees. \n
Résultat : deux copies complètes d'un million de chaînes. Ce n'est pas seulement lent — c'est une catastrophe pour les performances.
\n\nRvalue et Lvalue : les deux mondes du C++
\n\nPour comprendre la sémantique de déplacement, il faut maîtriser deux concepts fondamentaux : lvalue et rvalue.
\n\n- \n
- Lvalue (left value) — un objet qui a une adresse mémoire et un nom. Vous pouvez prendre son adresse (
&obj). Exemple : la variableint x = 5;—xest une lvalue. \n - Rvalue (right value) — une valeur temporaire qui n'a pas de nom et ne peut pas se trouver à gauche d'un opérateur d'affectation. Exemple :
5,x + 3, le résultat d'une fonctiongetTemp(). Vous ne pouvez pas écrire&(x + 3). \n
Avant, les rvalue-références (notées &&) n'existaient pas. Le compilateur voyait un objet temporaire et disait : « C'est une rvalue, je ne peux que la copier ou la détruire ». La sémantique de déplacement a donné au compilateur une troisième option : voler les ressources de la rvalue, puisqu'elle va de toute façon bientôt disparaître.
Comment fonctionnent std::move et les rvalue-références ?
\n\nLa fonction std::move n'est qu'un cast de type. Elle ne déplace pas physiquement les données, mais transforme une lvalue en rvalue, donnant au compilateur la permission de « dépouiller » l'objet.
\n\nPrenons une classe simple qui gère de la mémoire dynamique :
\n\n#include <iostream>\n#include <cstring>\n\nclass MaChaine {\nprivate:\n cha