Move semantics

En C++, les objets sont copiés par défaut. C'est inspiré de C. Ca évite les effets de bords.

string s("Bonjour");
vector<string> V;
V.push_back(s);
... on continue à utiliser
s[1] = "c"; // s est modifé mais pas la copie de s dans V

Parfois copier c'est débile

Supposons que l'on a un tableau de trucs.

class MyVector {
    string* data;
    size_t size;
    size_t capacity;

public:
    vector(){
        size = 0;
        capacity = 2;
        data = new truc[capacity];
    }

    resize(size_t new_capacity) {
        truc* new_data = new truc[new_capacity];
        for(int i = 0; i < size; i++) {
            new_data[i] = data[i];
        }
        std::swap(data, new_data);
        delete[] new_data;
        capacity = new_capacity;
    }

    void push_back(const truc& s){
        if(size == capacity) {
            resize(capacity * 2);
        }
        data[size] = s;
        size++;
    }
}

Le problème est dans

```cpp
new_data[i] = data[i];
```

Ca fait une copie des trucs ! alors que data va disparaître juste après.

Idée

Ce qu'on fait avec la copie :

TODO image avec des fruits

Idée pour améliorer :

Idée générale de la move semantics

Depuis C++11 (hé oui !), il y a la move semantics. Un move c'est une copie superficielle + on rend l'ancien élément "vide". En reprenant l'exemple précédent, voici comment cela fonctionne :

  • on met les données dans new_data[i]
  • data[i] est maintenant dans un état de sorte de coquille vide

Le compilateur ne pouvait pas deviner que data[i] allait être perdu. Là, on l'a rendu "coquille vide" donc ce n'est pas grave.

Exemples

Voici des situations où C++ fait une copie alors qu'un move aurait été plus efficace, mais le compilateur ne peut pas le deviner.

  • Situation où on a un nom car on utilise l'objet plusieurs fois :
{
    string s("Bloup");
    V.push_back(s); // là on aurait du faire move semantics car s est de toute façon détruit après le }
}
  • Situation où on a un paramètre
void reinit(string& s) {
    history.push_back(s); // là on aimerait avoir move semantics car s n'est plus utilisé après
    s = getDefaultValue();
}

Solution

La solution est d'utiliser std::move :

    {
        string s("Bloup");
        V.push_back(std::move(s)); // :)
    }

et

    void reinit(string& s) {
        history.push_back(std::move(s)); // :)
        s = getDefaultValue();
    }

Recap

std::move(s) signifie intuitivement :

  • s n'est plus utile ici
  • tu me déplaces au lieu de me copier
  • après l'appel s est toujours un objet valide mais sa valeur est quelconque (souvent vide a priori). On peut réutiliser la variable s.
void swap(string& a, string& b) {
    string tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

Comment ça marche... que fait std::move ?

En C++, on distingue :

  • les lvalue : ce sont des noms de variables, ou des cellules mémoires adressées par un pointeur, etc. (l pour location, ou pour left)
  • les rvalue qui sont des valeurs non nommés comme 5, f(3) (r pour right)

On va s'appuyer sur cette distinction. En effet, les lvalue sont amenés à rester en mémoire, alors que les rvalue sont amenés à mourir vite.

En C++, on note :

  • string& une référence sur une lvalue
  • string&& une référence sur une rvalue (c'est une lvalue qui est une référence sur une rvalue)

L'appel std::move(s) renvoie s mais en tant que rvalue pour dire qu'elle va mourir et donc qu'il faut faire une move semantics. En fait, std::move(s) c'est équivalent à static_cast<string&&>(y). Maintenant y, tu vas mourir !

Move semantics sur une méthode quelconque

template <typename T>
class vector {
    public:

        //copy elem into the vector
        void push_back(const T& elem);

        //**move** elem into the vector
        void push_back(T&& elem);

}
  • void push_back(const T& elem) is called in old version of C++ ;) or in C++11 when there is a variable name in the call (e.g. V.push_back(s))
  • In C++11, void push_back(T&& elem) is called when there is no name (e.g. V.push_back(2), V.push_back(getName())) or it is explicitely a moved object (e.g. V.push_back(move(s))).

Move semantics sur un constructeur

class string {
    private:
        int len;
        char* data;

    public:
        // copy constructor
        string(const string& a) : len(s.len) {
            data = new char(len+1);
            memcpy(data, s.data, len+1);
        }

        //move constructor
        string(string&& s) : len(s.len), data(s.data) {
            s.data = nullptr;
            s.len = 0;
        }

}

Pour aller plus loin