Menu
Coddy logo textTech

Sémantique de mouvement et Rvalues

Fait partie de la section Programmation Orientée Objet du Journey C++ de Coddy — leçon 83 sur 104.

En C++, chaque expression est soit une lvalue (possède une identité persistante, peut être adressée) soit une rvalue (temporaire, sur le point d'être détruite). Comprendre cette distinction débloque la sémantique de déplacement - une optimisation puissante qui évite les copies inutiles.

Une référence rvalue, déclarée avec &&, se lie spécifiquement aux objets temporaires. Cela vous permet de « voler » des ressources à des objets qui sont de toute façon sur le point de disparaître :

#include <iostream>
#include <utility>

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : data(new int[s]), size(s) {
        std::cout << "Constructed\n";
    }
    
    // Constructeur de déplacement - vole les ressources
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;  // Laisse la source dans un état valide
        other.size = 0;
        std::cout << "Moved\n";
    }
    
    ~Buffer() { delete[] data; }
};

int main() {
    Buffer b1(1000);
    Buffer b2(std::move(b1));  // Déclenche le constructeur de déplacement
}

La fonction std::move ne déplace rien en réalité — elle effectue simplement un cast d'une lvalue vers une référence rvalue, signalant que vous êtes prêt à céder les ressources de l'objet. Le déplacement réel s'effectue dans le constructeur de déplacement ou l'opérateur d'affectation par déplacement.

La sémantique de mouvement améliore considérablement les performances lors de l'utilisation d'objets lourds en ressources tels que des conteneurs ou des chaînes de caractères. Au lieu d'effectuer une copie profonde de mégaoctets de données, vous transférez simplement la propriété du pointeur — une opération en temps constant, quelle que soit la taille.

challenge icon

Défi

Facile

Construisons une classe DataBuffer de gestion de ressources qui démontre la sémantique de mouvement en action. Vous verrez comment le déplacement de ressources au lieu de leur copie peut considérablement améliorer l'efficacité lors du transfert de propriété d'une mémoire allouée dynamiquement.

Vous organiserez votre code sur trois fichiers :

  • DataBuffer.h : Définissez votre classe DataBuffer qui gère un tableau d'entiers alloué dynamiquement.

    Votre classe doit avoir des membres privés pour le pointeur de données (int*), la taille (size_t) et un nom (std::string) pour aider à suivre quel tampon est lequel pendant les opérations.

    Déclarez ce qui suit :

    • Un constructeur qui prend un nom std::string et une taille size_t, alloue le tableau et affiche : [name] constructed with size [size]
    • Un constructeur de mouvement qui prend une référence rvalue, vole les ressources et affiche : [name] moved from [source_name] (où le tampon de destination prend le nom de la source)
    • Un destructeur qui affiche [name] destroyed (ou empty destroyed si le tampon a été déplacé)
    • Une méthode getSize() qui retourne la taille actuelle
    • Une méthode getName() qui retourne le nom du tampon

    N'oubliez pas de marquer votre constructeur de mouvement comme noexcept et de laisser l'objet source dans un état vide valide (nullptr, taille 0, nom "empty").

  • DataBuffer.cpp : Implémentez toutes les méthodes déclarées dans votre en-tête. Lorsque le destructeur s'exécute, ne supprimez les données que si le pointeur n'est pas nul. Incluez <iostream> pour la sortie.
  • main.cpp : Lisez deux entrées :
    1. Un nom pour votre tampon (chaîne de caractères)
    2. Une taille pour votre tampon (entier)

    Créez un DataBuffer avec le nom et la taille donnés. Créez ensuite un second tampon en déplaçant le premier à l'aide de std::move(). Après le mouvement, affichez l'état des deux tampons :

    • Original: [name] size=[size]
    • New: [name] size=[size]

    Incluez <utility> pour std::move.

Par exemple, avec les entrées Alpha et 100 :

Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyed

Avec les entrées Buffer et 50 :

Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyed

Remarquez comment le constructeur de mouvement transfère la propriété de la mémoire allouée sans copier de données. Le tampon d'origine est laissé dans un état vide mais valide, et lorsque les deux tampons sont détruits à la fin du programme, seul celui qui possède encore la mémoire la supprime réellement.

Aide-mémoire

Les expressions C++ sont soit des lvalues (persistantes, adressables), soit des rvalues (temporaires). La sémantique de mouvement (move semantics) optimise les performances en transférant les ressources des objets temporaires au lieu de les copier.

Références de Rvalue

Déclarées avec &&, les références de rvalue se lient aux objets temporaires :

Buffer(Buffer&& other)  // Paramètre de référence de rvalue

Constructeur de mouvement

Vole les ressources d'un objet temporaire. Marquez-le comme noexcept et laissez la source dans un état vide valide :

Buffer(Buffer&& other) noexcept 
    : data(other.data), size(other.size) {
    other.data = nullptr;  // Laisse la source valide
    other.size = 0;
}

std::move

Convertit une lvalue en une référence de rvalue, signalant la volonté de transférer des ressources. Incluez <utility> :

Buffer b1(1000);
Buffer b2(std::move(b1));  // Déclenche le constructeur de mouvement

std::move ne déplace rien en soi - le transfert réel se produit dans le constructeur de mouvement ou l'opérateur d'affectation par mouvement.

Avantages

La sémantique de mouvement permet un transfert de ressources en temps constant, quelle que soit la taille de l'objet, évitant ainsi les copies profondes coûteuses de structures de données volumineuses.

Essayez vous-même

#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"

int main() {
    std::string name;
    int size;
    
    std::cin >> name;
    std::cin >> size;
    
    // TODO: Créer un DataBuffer avec le nom et la taille donnés
    
    // TODO: Créer un second buffer en déplaçant le premier à l'aide de std::move()
    
    // TODO: Afficher l'état des deux buffers :
    // Original: [name] size=[size]
    // New: [name] size=[size]
    
    return 0;
}
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation Orientée Objet