Programmation • IoT

Chapitre 3 – La sémantique de déplacement

Document réservé

Vous consultez actuellement la présentation publique de ce chapitre.

Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.

Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).

3 La sémantique de déplacement 3.3 Pourquoi déplacer plutôt que copier ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4 Un premier déplacement avec std::move . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5 Que fait réellement std::move ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6 Les catégories d’expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.1 Une expression peut désigner un objet . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.3 Une expression peut produire une valeur temporaire . . . . . . . . . . . . . . . . . . 3.6.6 Type et catégorie sont deux propriétés différentes . . . . . . . . . . . . . . . . . . . . 3.7 Références lvalue et références rvalue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.7.1 Les références que nous connaissons déjà . . . . . . . . . . . . . . . . . . . . . . . . 3.7.2 Une référence lvalue ne se lie pas à une rvalue . . . . . . . . . . . . . . . . . . . . . 3.7.3 Le cas important des références constantes . . . . . . . . . . . . . . . . . . . . . . . 3.7.5 Une référence rvalue et une lvalue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.7.6 Pourquoi avoir créé un nouveau type de référence ? . . . . . . . . . . . . . . . . . . . 3.7.7 Une expérience avec deux fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.7.8 Comment traiter une lvalue comme source de déplacement ? . . . . . . . . . . . . . 3.7.9 Ce que fait réellement std::move . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.7.10 std::move ne déplace rien par lui-même . . . . . . . . . . . . . . . . . . . . . . . . . 3.7.11 Un piège important : une référence rvalue nommée . . . . . . . . . . . . . . . . . . . 3.7.12 Conséquence pour le constructeur de déplacement . . . . . . . . . . . . . . . . . . . 3.7.13 Résumé des distinctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.8 Le constructeur de déplacement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.8.1 Transférer la ressource . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.9 Utiliser notre constructeur de déplacement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10 Un objet déplacé existe toujours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.11 L’affectation par déplacement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.13 Copie ou déplacement : une différence fondamentale . . . . . . . . . . . . . . . . . . . . . . 2026 – C++ Partie III La sémantique de déplacement
Accueil
La sémantique de déplacement Copier ou déplacer ? Dans le chapitre précédent, nous avons rencontré une propriété fondamentale de std::unique_ptr qui Un unique_ptr ne peut pas être copié. auto p1 = std::make_unique<int>(42); La deuxième instruction est refusée par le compilateur. La raison est simple : p1 représente la propriété exclusive de l’entier dynamique et copier p1 conduirait à créer deux propriétaires de la même ressource. Mais une autre opération peut parfaitement être envisagée, nous pouvons vouloir que p1 cesse d’être propriétaire de la ressource et que p2 en devienne le nouveau propriétaire. Nous voulons donc passer de : Il ne s’agit plus de copier la ressource mais de la déplacer. Copie et déplacement La différence entre copie et déplacement est fondamentale en C++ moderne. Lors d’une copie, l’objet d’origine conserve son contenu et un nouvel objet reçoit une copie de celui-ci. 2026 – C++ Partie III La sémantique de déplacement objet A −→ ressource A objet A −→ ressource A objet B −→ ressource B Les deux objets continuent donc d’exister avec leur propre contenu. Lors d’un déplacement, on cherche au contraire à transférer les ressources appartenant à un objet vers un objet A −→ ressource le déplacement produit : objet A −→ état valide mais non spécifié objet B −→ ressource La ressource elle-même n’a pas nécessairement besoin d’être copiée, la ressource reste la même mais elle Pourquoi déplacer plutôt que copier ? Imaginons un objet contenant un tableau de plusieurs millions de valeurs. Une copie profonde de celui-ci pourrait nécessiter • l’allocation d’une nouvelle zone de mémoire ; • la copie de plusieurs millions de valeurs ; • la gestion ultérieure de deux allocations distinctes. Si nous supposons que l’objet d’origine n’est plus nécessaire, il serait alors inutile de recopier toutes les données et nous pourrions simplement transférer la propriété de la zone de mémoire au nouvel objet. Avant le déplacement : objet A −→ grande zone de mémoire Après le déplacement : objet A −→ aucune ressource objet B −→ la même zone de mémoire La zone de mémoire elle-même n’a pas été copiée, seules les informations permettant d’en transférer la propriété ont été modifiées. Pour certains objets, cette opération peut donc être beaucoup moins coûteuse Un premier déplacement avec std::move Revenons à unique_ptr. auto p1 = std::make_unique<int>(42);
Accueil
2026 – C++ Partie III La sémantique de déplacement La copie suivante est interdite : Mais nous pouvons écrire : auto p2 = std::move(p1); La fonction std::move est déclarée dans Un programme complet peut donc être : auto p1 = std::make_unique<int>(42); auto p2 = std::move(p1); std::cout << *p2 << ’\n’; auto p2 = std::move(p1); p2 est devenu propriétaire de l’entier. Dans le cas particulier de std::unique_ptr, p1 ne possède alors plus rien et devient un pointeur intelligent Vérifier l’état de p1 Nous pouvons le constater : std::cout << « p1 ne possede plus rien\n »; std::cout << *p2 << ’\n’; permet toujours d’accéder à la valeur. avant le déplacement après le déplacement
Accueil
2026 – C++ Partie III La sémantique de déplacement • Le déplacement d’un std::unique_ptr transfère la propriété de la ressource. • Après le déplacement, le unique_ptr destination possède la ressource et le unique_ptr source Que fait réellement std::move ? Le nom std::move peut être trompeur car il pourrait laisser penser que cette fonction déplace elle-même les données. Ce n’est pas exactement ce qui se produit, std::move ne déplace pas directement une ressource. Il indique essentiellement au compilateur que l’objet qui lui est fourni peut être traité comme une valeur dont les ressources peuvent être transférées et c’est ensuite le constructeur de déplacement ou l’opérateur d’affectation par déplacement du type concerné qui effectue réellement le transfert. auto p2 = std::move(p1); peut être lu conceptuellement comme : p1 peut maintenant être utilisé comme source d’un déplacement. Le mécanisme de déplacement appartient donc au type de l’objet, et non à std::move lui-même. • Malgré son nom, std::move ne déplace pas lui-même les ressources. • Il permet à une opération de déplacement, si elle existe pour le type concerné, d’être sélectionnée. Les catégories d’expressions Pour comprendre correctement la sémantique de déplacement, il faut examiner plus précisément une notion que nous avons utilisée depuis le début sans réellement avoir besoin de la formaliser, les expressions. Considérons l’instruction suivante : Nous pouvons distinguer ici plusieurs notions. • a est le nom d’un objet ; • cet objet est de type int ; • cet objet contient la valeur 10. Mais lorsque nous utilisons ensuite :
Accueil
2026 – C++ Partie III La sémantique de déplacement dans une instruction, a constitue également une expression, par exemple : Dans cette instruction, l’expression a permet d’accéder à l’objet nommé a et d’obtenir sa valeur. Cette distinction peut sembler inutile dans des programmes simples, mais elle devient fondamentale avec le C++ moderne, car une expression possède notamment : • une catégorie de valeur. La catégorie d’une expression indique notamment si cette expression désigne un objet possédant une identité, ou si elle représente plutôt une valeur temporaire et c’est cette distinction qui permettra au compilateur de déterminer si un objet doit être copié ou peut être déplacé. Une expression peut désigner un objet L’objet a existe à une certaine adresse en mémoire que nous pouvons vérifier : std::cout << &a << ’\n’; Tant que cet objet existe, nous pouvons le retrouver grâce à son nom et à son adresse, l’expression : désigne donc un objet possédant une identité. Une expression de cette nature appartient à la catégorie appelée : Le terme lvalue est historique. Il provient de l’expression anglaise left value, car une lvalue pouvait typiquement apparaître à gauche d’une affectation : Dans cette instruction : Cette origine historique est cependant trompeuse et ne constitue plus une définition correcte. L’expression a est toujours une lvalue, pourtant est interdit puisque l’objet est constant. Il ne faut donc pas retenir :
Accueil
2026 – C++ Partie III La sémantique de déplacement Une lvalue est quelque chose qui peut être placé à gauche du signe =. Une interprétation beaucoup plus utile est : Une lvalue est une expression qui désigne un objet possédant une identité. Les expressions a et b sont toutes les deux des lvalues car elles désignent deux objets distincts, chacun possédant sa propre adresse. Une expression peut produire une valeur temporaire Considérons maintenant : effectue un calcul et produit la valeur 15 qui sert à initialiser b. Mais le résultat intermédiaire de l’expression a + 5 n’est pas un objet nommé que nous avons l’intention de retrouver ultérieuremen, il s’agit d’une valeur temporaire. produit temporairement une valeur utilisée pour initialiser b. Ce type d’expression appartient à la grande famille des rvalues. Le terme rvalue provient historiquement de right value. Une rvalue apparaissait typiquement à droite d’une Dans ces exemples, les expressions :
Accueil
2026 – C++ Partie III La sémantique de déplacement Là encore, l’origine historique du terme ne doit pas être prise comme une définition complète. Pour comprendre la sémantique de déplacement, nous pouvons retenir dans un premier temps : Une rvalue représente typiquement une valeur temporaire ou une valeur dont les ressources peuvent être récupérées. Cette deuxième possibilité deviendra particulièrement importante lorsque nous étudierons std::move. Dans la dernière instruction, est une lvalue et de même Ces expressions désignent des objets possédant une identité. En revanche : produit une valeur temporaire qui est une rvalue. Enfin, après l’initialisation : est à son tour une lvalue. Il faut donc bien distinguer l’expression utilisée pour calculer une valeur de l’objet qui reçoit ensuite cette Type et catégorie sont deux propriétés différentes Une expression possède un type et une catégorie de valeur et ces deux notions ne doivent pas être confondues.
Accueil
2026 – C++ Partie III La sémantique de déplacement est de type int et appartient à la catégorie des lvalues. le résultat est également de type int, mais l’expression appartient à une catégorie différente : c’est ici une Le type ne suffit donc pas à déterminer la catégorie d’une expression. • Les catégories lvalue et rvalue caractérisent des expressions. • Une lvalue désigne typiquement un objet possédant une identité. • Une rvalue représente typiquement une valeur temporaire ou une valeur dont les ressources peuvent être récupérées. • Une expression possède à la fois un type et une catégorie de valeur. Deux expressions de même type peuvent donc appartenir à des catégories différentes. Références lvalue et références rvalue Nous savons maintenant qu’une expression possède non seulement un type, mais également une catégorie, dans les cas simples, étudiés jusqu’ici, nous avons distingué : • les lvalues, qui désignent des objets possédant une identité ; • les rvalues, qui représentent notamment des valeurs temporaires ou des valeurs dont les ressources peuvent être récupérées. Cette distinction permet de comprendre une nouveauté fondamentale introduite avec C++11 : les références rvalue. Les références que nous connaissons déjà Nous avons déjà largement utilisé les références classiques :
Accueil
2026 – C++ Partie III La sémantique de déplacement crée une référence vers l’objet a. Cette référence est plus précisément appelée une référence lvalue. Le type de r est donc : La référence (alias) constitue un autre moyen d’accéder au même objet. Si on écrit le code std::cout << a << ’\n’; celui-ci affichera : Une référence lvalue ne se lie pas à une rvalue Essayons maintenant : Cette instruction est refusée par le compilateur. Pourquoi ? Or une référence lvalue modifiable de type : ne peut pas se lier directement à une rvalue. Nous pouvons donc écrire : Le cas important des références constantes Il existe cependant une règle que nous connaissons déjà : Cette instruction est parfaitement valide. Une référence lvalue constante :
Accueil
2026 – C++ Partie III La sémantique de déplacement peut se lier à une lvalue mais également à une rvalue Dans ce dernier cas, le C++ matérialise un objet temporaire permettant à la référence de s’y lier. Lorsque la référence locale est directement liée de cette manière, la durée de vie de ce temporaire est prolongée jusqu’à la fin de la durée de vie de la référence. Cette propriété explique notamment pourquoi les références constantes sont très utiles pour passer des objets à une fonction sans effectuer de copie : void afficher(const Objet& objet) L’objet n’est pas copié et la fonction ne peut pas le modifier par l’intermédiaire de cette référence. Les références rvalue C++11 introduit un deuxième type de référence : désigne une référence rvalue. Les deux caractères && ne signifient donc pas “référence vers une référence” ni “double référence”, ils désignent une catégorie particulière de référence. Une référence rvalue peut se lier à une rvalue, par exemple : est valide puisque l’expression :
Accueil
2026 – C++ Partie III La sémantique de déplacement Une référence rvalue et une lvalue Essayons maintenant : Cette instruction est refusée, car a est une lvalue et elle ne peut donc pas se lier directement à une référence Nous pouvons résumer les règles simples de liaison que nous venons d’étudier : Pourquoi avoir créé un nouveau type de référence ? À première vue, l’existence de T&& peut sembler inutile, après tout, const T& permet déjà de se lier à une rvalue. Mais il existe une différence fondamentale. Une fonction recevant : promet essentiellement de consulter l’objet sans le modifier et donc ne peut pas raisonnablement lui retirer ses ressources. En revanche, une fonction recevant : peut être conçue précisément pour recevoir un objet dont les ressources peuvent être transférées. C’est cette distinction qui rend possible la sémantique de déplacement. Une expérience avec deux fonctions Nous pouvons rendre cette distinction visible avec deux fonctions : void identifier(int& x) std::cout << « lvalue\n »; void identifier(int&& x) std::cout << « rvalue\n »;
Accueil
2026 – C++ Partie III La sémantique de déplacement Quand la fonction reçoit l’expression a étant une lvalue c’est void identifier(int& x) qui est sélectionnée. En revanche si la fonction recoit c’est l’autre fonction qui est choisie, celle avec la signature correspondant à une rvalue, c.-à-d. la fonction : void identifier(int&& x). Le compilateur est ainsi capable de distinguer les deux catégories d’expressions et de sélectionner des fonctions différentes. Comment traiter une lvalue comme source de déplacement ? Revenons maintenant à un objet existant : Elle ne peut donc pas se lier directement à un paramètre de type : Pourtant, le programmeur peut savoir que l’objet a n’a plus besoin de conserver ses ressources. Il faut alors pouvoir exprimer explicitement l’intention suivante : Cet objet existe toujours, mais j’autorise maintenant l’utilisation de ses ressources pour effectuer un C’est précisément le rôle de
Accueil
2026 – C++ Partie III La sémantique de déplacement Ce que fait réellement std::move Dans cette expression : est une lvalue, en revanche produit une expression appartenant à la famille des rvalues. Plus précisément, cette expression appartient à la catégorie appelée xvalue 1 . Nous pouvons donc écrire : int&& r = std::move(a); Pour utiliser std::move, il faut inclure Pour l’instant on retient std::move(a) −→ rvalue pouvant être utilisée comme source d’un déplacement std::move ne déplace rien par lui-même Le nom std::move est quelque peu trompeur. Considérons simplement : Cette instruction ne transfère aucune ressource car std::move permet essentiellement de changer la manière dont l’expression est présentée au mécanisme de sélection des fonctions. Le déplacement proprement dit doit être réalisé par une opération capable de déplacer l’objet. Tableau b(std::move(a)); permet au constructeur Tableau(Tableau&& autre) 1. Nous préciserons cette catégorie ultérieurement.
Accueil
2026 – C++ Partie III La sémantique de déplacement d’être sélectionné. C’est ensuite ce constructeur qui effectue réellement le transfert des ressources. • std::move ne déplace pas lui-même un objet. • Il produit une expression permettant au compilateur de sélectionner une opération de déplacement lorsque celle-ci existe. • Le déplacement réel est effectué par le constructeur de déplacement, l’opérateur d’affectation par déplacement ou une autre opération appropriée. Un piège important : une référence rvalue nommée Considérons maintenant Nous savons que le type déclaré de r est Il s’agit donc d’une référence rvalue. Mais considérons maintenant l’expression Cette expression est une lvalue même si cela peut sembler contradictoire. Il faut cependant se souvenir de la distinction établie dans la section précédente Le type d’une variable et la catégorie d’une expression sont deux notions différentes. La variable r possède maintenant un nom et l’expression constituée par ce nom permet de retrouver l’objet auquel elle fait référence, c’est donc une lvalue. catégorie de l’expression r Conséquence pour le constructeur de déplacement Cette règle devient particulièrement importante dans un constructeur de déplacement. Considérons : Tableau(Tableau&& autre) Le paramètre autre est déclaré avec le type Mais à l’intérieur du constructeur, l’expression
Accueil
2026 – C++ Partie III La sémantique de déplacement est une lvalue. Pourquoi ? Parce que autre possède maintenant un nom et désigne un objet identifiable. Cette distinction peut devenir importante si certains membres de autre doivent eux-mêmes être déplacés. Nous rencontrerons alors des expressions telles que : std::move(autre.membre) qui permettront à nouveau d’autoriser le déplacement de ce membre. Résumé des distinctions Il faut maintenant distinguer clairement trois notions. l’entier contenant 10 Catégorie de l’expression r Il n’y a aucune contradiction, la syntaxe && caractérise ici le type de la référence et les termes lvalue et rvalue caractérisent la catégorie d’une expression. • Une référence rvalue s’écrit : • Les deux caractères && ne représentent pas une double référence. • Ils permettent notamment de recevoir des rvalues et constituent le mécanisme fondamental sur lequel repose la sémantique de déplacement. • Il faut cependant toujours distinguer le type d’une référence de la catégorie de l’expression qui r est de type int&&, mais l’expression r est une lvalue. Le constructeur de déplacement Considérons une classe qui possède directement une zone de mémoire dynamique
Accueil
2026 – C++ Partie III La sémantique de déplacement Tableau(std::size_t n) : donnees(new double[n]), taille(n) Nous voulons maintenant pouvoir transférer la ressource appartenant à un objet vers un autre objet. Un constructeur de déplacement possède une forme semblable à : Tableau(Tableau&& autre) est une référence rvalue vers un objet de type Tableau. Transférer la ressource Nous pouvons écrire : Tableau(Tableau&& autre) : donnees(autre.donnees), taille(autre.taille) autre.donnees = nullptr; Examinons cette opération. donnees = autre.donnees; ne copie pas le tableau, elle copie uniquement l’adresse de la zone de mémoire tel que le nouvel objet possède maintenant cette adresse. Mais il faut immédiatement empêcher l’ancien objet de continuer à se comporter comme propriétaire de cette même ressource en écrivant autre.donnees = nullptr; Ainsi, lorsque l’ancien objet sera détruit, son destructeur exécutera : ce qui est autorisé et ne provoque aucune libération supplémentaire, par conséquent la propriété de la ressource a réellement été transférée. Aucune copie du tableau Le constructeur de déplacement n’a pas créé un nouveau tableau de n éléments, il a simplement transféré l’adresse du tableau existant.
Accueil
2026 – C++ Partie III La sémantique de déplacement Nous sommes passés de : autre.donnees −→ tableau dynamique autre.donnees −→ nullptr donnees −→ tableau dynamique La ressource n’a pas été dupliquée, sa propriété a changé. Utiliser notre constructeur de déplacement Nous pouvons maintenant écrire : Tableau b(std::move(a)); permet au constructeur de déplacement d’être utilisé et après cette opération, b possède le tableau dynamique initialement possédé par a. L’objet a existe toujours, mais il ne possède plus cette ressource. Un objet déplacé existe toujours Il est très important de ne pas confondre déplacer les ressources d’un objet détruire cet objet. Tableau b(std::move(a)); l’objet a existe toujours et il sera détruit normalement lorsqu’il arrivera à la fin de sa durée de vie. Dans notre propre classe Tableau, nous avons choisi de placer explicitement l’objet source dans l’état Nous connaissons donc précisément cet état. Pour les types de la bibliothèque standard, la règle générale est qu’après un déplacement l’objet source reste dans un état valide mais dont la valeur n’est généralement pas spécifiée et donc, il peut donc être détruit ou recevoir une nouvelle valeur. Il ne faut en revanche pas supposer qu’il conserve nécessairement son ancienne valeur.
Accueil
2026 – C++ Partie III La sémantique de déplacement • Un objet déplacé n’est pas détruit. • Il continue d’exister et son destructeur sera appelé normalement. • Pour les types de la bibliothèque standard, après un déplacement, l’objet source reste généralement dans un état valide mais dont la valeur n’est pas spécifiée. • Dans le cas particulier de std::unique_ptr, l’état est précisément défini : le pointeur source L’affectation par déplacement Le constructeur de déplacement intervient lors de la construction d’un nouvel objet Tableau b(std::move(a)); Mais les deux objets peuvent déjà exister Il ne s’agit plus d’une construction, mais d’une affectation. Le C++ permet donc également de définir un opérateur d’affectation par déplacement Tableau& operator=(Tableau&& autre) donnees = autre.donnees; taille = autre.taille; autre.donnees = nullptr; Avant de recevoir la nouvelle ressource, l’objet destination doit libérer celle qu’il possède déjà Il peut ensuite prendre possession de la ressource de autre et enfin, l’objet source cesse d’en être propriétaire autre.donnees = nullptr; Retour à std::unique_ptr Nous pouvons maintenant comprendre précisément l’instruction qui avait motivé ce chapitre. auto p1 = std::make_unique<int>(42); auto p2 = std::move(p1);
Accueil
2026 – C++ Partie III La sémantique de déplacement std::move(p1) permet d’utiliser la sémantique de déplacement de std::unique_ptr. La propriété de la ressource est transférée de p1 vers p2 et après le déplacement est vrai, tandis que permet toujours d’accéder à la valeur 42. Nous comprenons maintenant pourquoi la copie : est interdite alors que auto p2 = std::move(p1); Dans le premier cas, nous demanderions deux propriétaires et dans le second, nous transférons la propriété d’un objet vers un autre. Copie ou déplacement : une différence fondamentale Nous pouvons maintenant résumer les deux opérations. conserve ses ressources cède ses ressources reçoit les ressources existantes généralement dupliquée généralement transférée potentiellement important souvent beaucoup plus faible Cette différence devient particulièrement importante pour les objets possédant de grandes quantités de données ou des ressources dont la copie n’a pas de sens. La sémantique de déplacement, introduite avec C++11, permet de transférer les ressources d’un objet vers un autre sans devoir nécessairement les copier, elle repose notamment sur : • les références rvalue, écrites Type&& ; • les constructeurs de déplacement ; • les opérateurs d’affectation par déplacement ; std::move ne déplace pas lui-même une ressource mais permet à l’objet concerné d’être utilisé comme source d’une opération de déplacement. Après un déplacement, l’objet source existe toujours et doit pouvoir être détruit correctement. La sémantique de déplacement est particulièrement importante pour std::unique_ptr, car celui-ci ne peut pas être copié mais sa propriété peut en revanche être transférée :
Accueil
2026 – C++ Partie III La sémantique de déplacement auto p1 = std::make_unique<int>(42); auto p2 = std::move(p1); Après cette opération, p2 possède la ressource et p1 est vide. La sémantique de déplacement ne concerne cependant pas uniquement les pointeurs intelligents. Elle constitue un mécanisme général du C++ moderne et intervient dans de nombreux types de la bibliothèque
Accueil
P4-Chap-03 La sémantique de déplacement
Accueil
Termes à ajouter au glossaire
Contenu

Inscription

×
Cancel