Move semantics
Rappel : copie par défaut
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);
s[1] = "i";
- Est-ce que
Vcontient"Binjour"ou"Bonjour"?
En faisant V.push_back(s);, on met une copie de s dans V. Donc V contient "Bonjour" alors que s est "Binjour".
Parfois copier c’est stupide
Supposons que l’on a conçu une classe MyVector pour un tableau dynamique de truc.
class MyVector {
truc* data;
size_t size;
size_t capacity;
public:
vector() {
size = 0;
capacity = 2;
data = new truc[capacity];
}
void 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
new_data[i] = data[i];
On fait une copie pour chaque élément du tableau, alors que data va de toute façon disparaître juste après.
Idée
Au lieu de faire une copie :
l’idée est de réaliser un déplacement des données :
Idée générale de la move semantics
La move semantics est discutée depuis 2002, cf. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm. Depuis C++11 (hé oui !), il y a la move semantics. Un move est une copie superficielle et le fait de rendre l’ancien élément “vide” (ou alors, l’élément est carrément inaccessible comme en Rust).
Le langage C++ offre std::move pour réaliser une copie (ce qu’est std::move précisémment est un peu technique et propre au C++). En reprenant l’exemple précédent, en C++, on écrit :
new_data[i] = std::move(data[i]);
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 :
sn’est plus utile ici- tu me déplaces au lieu de me copier
- après l’appel
sest toujours un objet valide mais sa valeur est quelconque (souvent vide a priori). On peut réutiliser la variables.
void swap(string& a, string& b) {
string tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
Comment ça marche en C++… 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. (lpour location, ou pour left) - les
rvaluequi sont des valeurs non nommés comme5,f(3)(rpour 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 unelvaluestring&&une référence sur unervalue(c’est unelvaluequi est une référence sur unervalue)
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&&>(s). Maintenant s, tu vas mourir !
Du coup, ce changement de type, a priori, ne devrait rien faire de spécial. En fait, si. Car il indique au compilateur qu’il faut appeler la bonne fonction, qui s’occupe de faire un move, celle qui prend un string&& et pas celle qui prend un string&.
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)
C’était la seule fonction qui existait en C++ avant 2011. Depuis C++11, c’est cette fonction qui est appelée quand on lui donne une l-valeur. Par exemple : V.push_back(s).
void push_back(T&& elem)
Depuis C++11, cette méthode est appelée quand on donne une r-valeur. Par exemple : V.push_back(2), V.push_back(getName()).
C’est ausi cette méthode qui est appelée quand l’objet a été déplacé. Par exemple : V.push_back(std::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;
}
}
string(const string& a)
Le constructeur par copie est appelé par exemple sur l’affectation suivante :
string a = ...;
string b = a;
string(string&& s)
string a = ...;
string b = std::move(a);
Exercice
Ecrire le constructeur par move semantics de cette classe (si ça vous amuse, on peut aussi écrire les opérations + et -, *, etc.) :
#include <iostream>
#include <vector>
class BigInt
{
u_int64_t *digits;
u_int64_t nDigits;
public:
BigInt(u_int64_t k)
{
std::cout << "Creation of a bigint" << std::endl;
digits = new u_int64_t[1];
digits[0] = k;
nDigits = 1;
}
BigInt(BigInt &x)
{
std::cout << "Creation of a bigint by copy" << std::endl;
digits = new u_int64_t[1];
digits[0] = x.digits[0];
nDigits = x.nDigits;
}
~BigInt()
{
std::cout << "Destruction of a bigint" << std::endl;
delete digits;
}
void print()
{
std::cout << digits[0] << std::endl;
}
};
void simpleFunction(BigInt b) {
b.print();
}
BigInt functionWithCopyEllusion(BigInt b) {
return b;
}
BigInt createBigInt() {
BigInt b(5);
return b;
}
int main()
{
int x = 4;
int& y = x;
y = 5;
std::cout << x;
// BigInt b(5);
//b.print();
//simpleFunction(b);
//BigInt c = functionWithCopyEllusion(b);
BigInt b = createBigInt();
}
Pour aller plus loin
- https://www.thecodedmessage.com/posts/cpp-move/
- Vidéo à CPPCON 2021 sur la move semantics, dont est inspiré ce cours https://www.youtube.com/watch?v=Bt3zcJZIalk