Move semantics

En C++, les objets sont copiés par défaut. Et c'est très bien car ça é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 l'élément dans V

Parfois copier c'est débile

V.push_back(getStringFromClient())
  • getStringFromClient() renvoie un objet string que l'on note A (il n'y a pas de variable A dans le programme)
  • On crée une copie A' que l'on donne à push_back
  • Puis A est supprimé

C'est dommage de copier A pour le supprimer juste après.

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

Depuis C++11, il y a la move semantics. En reprenant l'exemple précédent, voici comment cela fonctionne :

  • getStringFromClient() renvoie un objet string que l'on note A
  • On crée A' qui reçoit les données de A
  • On met A dans un état de sorte de coquille vide
  • On donne A' à push_back
  • A est supprimé

std::move

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

Exemples

  • Situation où on a un nom car on utilise l'objet plusieurs fois :

      {
          string s("Bloup");
    
          V.push_back(s);
          V.push_back(s); // là on aurait du faire move semantics car s est de toute façon détruit après
      }
    
  • 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, que l'on note ici move :

    {
        string s("Bloup");

        V.push_back(s);
        V.push_back(move(s)); // :)
    }

et

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

Recap

move(s) signifie :

  • 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(move(a));
          a = move(b);
          b = move(tmp);
      }
    

En fait, move(s) c'est équivalent à static_cast<string&&>(y).

Implémentation côté vector

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(getName())) or it is explicitely a moved object (e.g. V.push_back(move(s))).

Implémentation côté string

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