Je viens juste de finir d'écouter la radio Software Engineering entretien podcast avec Scott Meyers concernant C++ 0x . La plupart des nouvelles fonctionnalités me paraissaient logiques, et je suis maintenant très enthousiasmé par C++ 0x, à l’exception d’une seule. Je ne comprends toujours pas la sémantique du mouvement ... Qu'est-ce que c'est exactement?
Je trouve plus facile de comprendre la sémantique de déplacement avec un exemple de code. Commençons par une classe de chaîne très simple qui ne contient qu'un pointeur sur un bloc de mémoire alloué par tas:
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}
Puisque nous avons choisi de gérer nous-mêmes la mémoire, nous devons suivre la règle de trois . Je vais différer l'écriture de l'opérateur d'affectation et ne mettre en œuvre que le destructeur et le constructeur de copie pour l'instant:
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}
Le constructeur de copie définit ce que signifie copier des objets chaîne. Le paramètre const string& that
se lie à toutes les expressions de type chaîne, ce qui vous permet de réaliser des copies dans les exemples suivants:
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
Vient maintenant la compréhension clé de la sémantique du déménagement. Notez que c'est seulement dans la première ligne où nous copions x
que cette copie complète est vraiment nécessaire, car nous pourrions vouloir inspecter x
plus tard et nous serions très surpris si x
avait changé d'une manière ou d'une autre. Avez-vous remarqué que je viens de dire x
trois fois (quatre fois si vous incluez cette phrase) et que vous vouliez dire exactement le même objet à chaque fois? Nous appelons des expressions telles que x
"lvalues".
Les arguments des lignes 2 et 3 ne sont pas des valeurs, mais des valeurs, car les objets de chaîne sous-jacents n’ont pas de nom. Le client n’a donc aucun moyen de les inspecter ultérieurement. Les valeurs rvalues désignent des objets temporaires détruits au point-virgule suivant (pour être plus précis: à la fin de l'expression complète qui contient la valeur rvalue lexicalement). Ceci est important car lors de l'initialisation de b
et c
, nous pouvions faire ce que nous voulions avec la chaîne source et le client ne pouvait pas faire la différence !
C++ 0x introduit un nouveau mécanisme appelé "référence rvalue" qui permet, entre autres, de détecter les arguments rvalue via une surcharge de fonctions. Tout ce que nous avons à faire est d’écrire un constructeur avec un paramètre de référence rvalue. Dans ce constructeur, nous pouvons faire tout ce que nous voulons avec le source, tant que nous le laissons dans un peu état valide:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}
Qu'avons-nous fait ici? Au lieu de copier en profondeur les données de segment de mémoire, nous venons de copier le pointeur, puis de définir le pointeur d'origine sur null (pour empêcher "delete []" du destructeur de l'objet source de libérer nos "données juste volées"). En effet, nous avons "volé" les données qui appartenaient à l'origine à la chaîne source. Là encore, l’idée clé est que le client ne peut en aucun cas détecter que la source a été modifiée. Puisque nous ne faisons pas vraiment de copie ici, nous appelons ce constructeur un "constructeur de déplacement". Son travail consiste à déplacer des ressources d'un objet à un autre au lieu de les copier.
Félicitations, vous comprenez maintenant les bases de la sémantique des déplacements! Continuons en implémentant l'opérateur d'affectation. Si vous ne connaissez pas copiez et échangez l'idiome , apprenez-le et revenez parce que c'est un idiome C++ génial lié à la sécurité des exceptions.
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};
Hein, c'est ça? "Où est la référence de valeur?" vous pourriez demander. "On n'en a pas besoin ici!" est ma réponse :)
Notez que nous passons le paramètre that
par valeur , donc that
doit être initialisé comme tout autre objet chaîne. Comment exactement that
va-t-il être initialisé? Dans les temps anciens de C++ 98 , la réponse aurait été "par le constructeur de copie". Dans C++ 0x, le compilateur choisit entre le constructeur de copie et le constructeur de déplacement selon que l'argument de l'opérateur d'affectation est une valeur lvalue ou une valeur rvalue.
Donc, si vous dites a = b
, le constructeur de copie initialisera that
(car l'expression b
est une valeur lvalue. ), et l’opérateur d’affectation échange le contenu avec une copie complète fraîchement créée. Telle est la définition même de la copie et de l’échange de langage: faire une copie, permuter le contenu avec la copie, puis supprimer la copie en laissant la portée. Rien de nouveau ici.
Mais si vous dites a = x + y
, le constructeur de mouvement initialisera that
(car l'expression x + y
est une valeur rvalue ), il n'y a donc pas de copie en profondeur, mais seulement un déménagement efficace. that
est toujours un objet indépendant de l'argument, mais sa construction était triviale, car les données de segment n'ont pas à être copiées, elles ont simplement été déplacées. Il n'était pas nécessaire de le copier car x + y
est une valeur rvalue et, encore une fois, il est correct de passer des objets chaîne désignés par des valeurs rvalues.
Pour résumer, le constructeur de copie crée une copie complète, car la source doit rester intacte. Le constructeur de mouvements, par contre, peut simplement copier le pointeur, puis définir le pointeur de la source sur null. Vous pouvez "annuler" l'objet source de cette manière, car le client n'a aucun moyen de l'inspecter à nouveau.
J'espère que cet exemple a compris l'essentiel. Il y a encore beaucoup de choses à valoriser les références et à déplacer la sémantique, ce que j'ai volontairement laissé de côté pour rester simple. Si vous voulez plus de détails s'il vous plaît voir ma réponse supplémentaire .
Ma première réponse a été une introduction extrêmement simplifiée pour déplacer la sémantique, et de nombreux détails ont été laissés de côté pour rester simples. Cependant, il y a beaucoup plus de choses à faire évoluer dans la sémantique, et j’ai pensé qu’il était temps pour une deuxième réponse de combler les lacunes. La première réponse est déjà ancienne et il n’a pas semblé juste de la remplacer par un texte complètement différent. Je pense que cela sert toujours bien comme une première introduction. Mais si vous voulez creuser plus profondément, lisez la suite :)
Stephan T. Lavavej a pris le temps de fournir de précieux commentaires. Merci beaucoup, Stephan!
La sémantique de déplacement permet à un objet, sous certaines conditions, de s'approprier les ressources externes d'un autre objet. Ceci est important de deux manières:
Transformer des copies coûteuses en déménagements bon marché. Voir ma première réponse pour un exemple. Notez que si un objet ne gère pas au moins une ressource externe (directement ou indirectement via ses objets membres), la sémantique de déplacement n'offrira aucun avantage par rapport à la sémantique de copie. Dans ce cas, copier un objet et le déplacer signifie exactement la même chose:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
Implémentation de types "uniquement mobiles" sécurisés; c'est-à-dire des types pour lesquels copier n'a pas de sens, mais déplacer le fait. Les exemples incluent les verrous, les descripteurs de fichiers et les pointeurs intelligents avec une sémantique de propriété unique. Remarque: cette réponse traite de std::auto_ptr
, un modèle de bibliothèque standard C++ 98 déprécié, qui a été remplacé par std::unique_ptr
dans C++ 11. Les programmeurs C++ intermédiaires sont probablement au moins quelque peu familiarisés avec std::auto_ptr
, et en raison de la "sémantique du déplacement" affichée, il semble être un bon point de départ pour discuter de la sémantique du déplacement en C++ 11. YMMV.
La bibliothèque standard C++ 98 offre un pointeur intelligent avec une sémantique de propriété unique appelée std::auto_ptr<T>
. Si vous ne connaissez pas bien auto_ptr
, son but est de garantir qu'un objet alloué de manière dynamique est toujours libéré, même en présence d'exceptions:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
La particularité de auto_ptr
est son comportement de "copie":
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Notez comment l'initialisation de b
avec a
fait not copie le triangle, mais transfère la propriété du triangle de a
à b
. Nous disons aussi "a
est déplacé dans b
" ou "le triangle est déplacé de a
à b
". Cela peut paraître déroutant, car le triangle lui-même reste toujours au même endroit en mémoire.
Déplacer un objet signifie transférer la propriété d'une ressource qu'il gère à un autre objet.
Le constructeur de copie de auto_ptr
ressemble probablement à ceci (quelque peu simplifié):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
La dangerosité de auto_ptr
est que ce qui ressemble syntaxiquement à une copie est en réalité un déplacement. Essayer d'appeler une fonction membre sur un auto_ptr
transféré invoquera un comportement indéfini. Vous devez donc faire très attention à ne pas utiliser un auto_ptr
après l'avoir déplacé de:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Mais auto_ptr
n'est pas toujours dangereux. Les fonctions d'usine constituent un cas d'utilisation parfait pour auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Notez que les deux exemples suivent le même modèle syntaxique:
auto_ptr<Shape> variable(expression);
double area = expression->area();
Et pourtant, l'un d'entre eux invoque un comportement indéfini, l'autre non. Alors, quelle est la différence entre les expressions a
et make_triangle()
? Ne sont-ils pas du même type? En effet, ils le sont, mais ils ont différentes catégories de valeur .
De toute évidence, il doit exister une différence profonde entre l'expression a
qui désigne une variable auto_ptr
et l'expression make_triangle()
qui désigne l'appel d'une fonction renvoyant un auto_ptr
par valeur, créant ainsi un nouvel objet temporaire auto_ptr
à chaque appel. a
est un exemple de lvalue , alors que make_triangle()
est un exemple de rvalue .
Passer de valeurs telles que a
est dangereux, car nous pourrions ultérieurement appeler une fonction membre via a
, en invoquant un comportement indéfini. Par contre, passer de valeurs telles que make_triangle()
est parfaitement sécurisé, car une fois que le constructeur de copie a terminé son travail, nous ne pouvons plus utiliser le temporaire. Il n'y a pas d'expression qui dénote ladite temporaire; si nous écrivons simplement à nouveau make_triangle()
, nous obtenons un différent temporaire. En fait, le temporaire déplacé est déjà parti sur la ligne suivante:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Notez que les lettres l
et r
ont une origine historique dans les parties gauche et droite d'une affectation. Cela n’est plus vrai en C++, car il existe des valeurs qui ne peuvent pas apparaître à gauche d’une affectation (telles que des tableaux ou des types définis par l’utilisateur sans opérateur d’attribution), et des valeurs qui peuvent (toutes les valeurs de types de classe avec un opérateur d'affectation).
Une valeur de type classe est une expression dont l'évaluation crée un objet temporaire. Dans des circonstances normales, aucune autre expression dans la même portée ne désigne le même objet temporaire.
Nous comprenons maintenant qu’il est potentiellement dangereux de passer d’une valeur à une autre, mais qu’elle est inoffensive. Si C++ avait le support du langage pour distinguer les arguments lvalue des arguments rvalue, nous pourrions soit interdire complètement le déplacement depuis lvalues, soit au moins le faire passer de lvalues explicit sur le site d'appels, de sorte déplacer plus par accident.
La réponse de C++ 11 à ce problème est rvalue references . Une référence rvalue est un nouveau type de référence qui se lie uniquement à rvalues. La syntaxe est X&&
. La bonne vieille référence X&
s'appelle maintenant lvalue reference . (Notez que X&&
est not une référence à une référence; cela n'existe pas en C++.)
Si nous ajoutons const
, nous avons déjà quatre types de références différents. À quels types d'expressions de type X
peuvent-ils se lier?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
En pratique, vous pouvez oublier const X&&
. Être limité à lire des valeurs n'est pas très utile.
Une référence rvalue
X&&
est un nouveau type de référence qui se lie uniquement à rvalues.
Les références Rvalue sont passées par plusieurs versions. Depuis la version 2.1, une référence rvalue X&&
lie également toutes les catégories de valeur d'un type différent Y
, à condition qu'il existe une conversion implicite de Y
en X
. Dans ce cas, un temporaire de type X
est créé et la référence rvalue est liée à ce temporaire:
void some_function(std::string&& r);
some_function("hello world");
Dans l'exemple ci-dessus, "hello world"
est une valeur de type const char[12]
. Puisqu'il existe une conversion implicite de const char[12]
à const char*
en std::string
, un temporaire de type std::string
est créé, et r
est lié à ce temporaire. C'est l'un des cas où la distinction entre rvalues (expressions) et temporaries (objets) est un peu floue.
Un exemple utile d'une fonction avec un paramètre X&&
est le constructeur de déplacement X::X(X&& source)
. Son but est de transférer la propriété de la ressource gérée de la source à l'objet actuel.
En C++ 11, std::auto_ptr<T>
a été remplacé par std::unique_ptr<T>
, qui tire parti des références rvalue. Je développerai et discuterai une version simplifiée de unique_ptr
. Tout d'abord, nous encapsulons un pointeur brut et surchargons les opérateurs ->
et *
, de sorte que notre classe se présente comme un pointeur:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
Le constructeur prend possession de l'objet et le destructeur le supprime:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Vient maintenant la partie intéressante, le constructeur de déménagement:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Ce constructeur de mouvements fait exactement ce que faisait le constructeur de copie auto_ptr
, mais il ne peut être fourni qu'avec des valeurs rvalues:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
La deuxième ligne ne parvient pas à se compiler, car a
est une lvalue, mais le paramètre unique_ptr&& source
ne peut être lié qu'à des valeurs rvalues. C'est exactement ce que nous voulions. les mouvements dangereux ne doivent jamais être implicites. La troisième ligne se compile parfaitement, car make_triangle()
est une valeur rvalue. Le constructeur de déménagement transférera la propriété du temporaire vers c
. Encore une fois, c'est exactement ce que nous voulions.
Le constructeur de déménagement transfère la propriété d'une ressource gérée à l'objet actuel.
La dernière pièce manquante est l'opérateur d'affectation de déménagement. Son travail consiste à libérer l'ancienne ressource et à acquérir la nouvelle ressource à partir de son argument:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Notez que cette implémentation de l'opérateur d'affectation de déplacement duplique la logique du destructeur et du constructeur de déplacement. Êtes-vous familier avec l'idiome copie-et-swap? Il peut également être appliqué pour déplacer la sémantique en tant qu'idiome de transfert et d'échange:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Maintenant que source
est une variable de type unique_ptr
, elle sera initialisée par le constructeur du déplacement; c'est-à-dire que l'argument sera déplacé dans le paramètre. L'argument doit toujours être une valeur rvalue, car le constructeur du déplacement possède lui-même un paramètre de référence rvalue. Lorsque le flux de contrôle atteint l'accolade fermante de operator=
, source
sort du cadre et libère automatiquement l'ancienne ressource.
L'opérateur d'affectation de déplacement transfère la propriété d'une ressource gérée à l'objet actuel et libère l'ancienne ressource. L'idiome "déplacer-échanger" simplifie la mise en œuvre.
Parfois, on veut passer des lvalues. C'est-à-dire que nous voulons parfois que le compilateur traite une valeur de lvalue comme s'il s'agissait d'une valeur de rvalue, afin de pouvoir appeler le constructeur de mouvement, même si cela pourrait être potentiellement dangereux. À cette fin, C++ 11 propose un modèle de fonction de bibliothèque standard appelé std::move
à l'intérieur de l'en-tête <utility>
. Ce nom est un peu malheureux, car std::move
jette simplement une lvalue à une rvalue; cela fait not ne déplace rien par lui-même. C'est simplement active en mouvement. Peut-être aurait-il dû s'appeler std::cast_to_rvalue
ou std::enable_move
, mais nous en sommes maintenant au nom.
Voici comment vous vous déplacez explicitement d'une lvalue:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Notez qu'après la troisième ligne, a
ne possède plus de triangle. Ce n'est pas grave, car explicitement écrit std::move(a)
, nous avons clairement indiqué nos intentions: "Cher constructeur, faites ce que vous voulez avec a
pour initialiser c
; je ne me soucie plus de a
. Sentez-vous libre de suivre votre chemin avec a
. "
std::move(some_lvalue)
attribue une valeur à une valeur, permettant ainsi un déplacement ultérieur.
Notez que même si std::move(a)
est une valeur rvalue, son évaluation not crée un objet temporaire. Cette énigme a obligé le comité à introduire une troisième catégorie de valeur. Quelque chose qui peut être lié à une référence rvalue, même si ce n'est pas une rvalue au sens traditionnel, est appelé un xvalue (valeur expirante). Les rvalues traditionnelles ont été renommées prvalues (rvalues pures).
Prvalues et xvalues sont des rvalues. Xvalues et lvalues sont tous deux glvalues (lvalues généralisées). Les relations sont plus faciles à comprendre avec un diagramme:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Notez que seules les valeurs x sont vraiment nouvelles; le reste est simplement dû au renommage et au regroupement.
Les rvalues C++ 98 sont appelées prvalues dans C++ 11. Remplacez mentalement toutes les occurrences de "rvalue" dans les paragraphes précédents par "prvalue".
Jusqu'ici, nous avons observé une évolution dans les variables locales et dans les paramètres de fonction. Mais le déplacement est également possible dans le sens opposé. Si une fonction renvoie sa valeur, un objet du site d’appel (probablement une variable locale ou temporaire, mais tout type d’objet) est initialisé avec l’expression suivant l’instruction return
en tant qu’argument du constructeur de déplacement:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Peut-être de manière surprenante, les objets automatiques (les variables locales non déclarées comme static
) peuvent également être implicitement déplacés des fonctions:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Comment se fait-il que le constructeur de déménagement accepte la lvalue result
comme argument? La portée de result
est sur le point de se terminer et sera détruite lors du déroulement de la pile. Personne ne pourrait se plaindre par la suite que result
ait changé d'une façon ou d'une autre; lorsque le flux de contrôle est de retour chez l'appelant, result
n'existe plus! Pour cette raison, C++ 11 a une règle spéciale qui permet de retourner des objets automatiques à partir de fonctions sans avoir à écrire std::move
. En fait, vous devriez never == utiliser std::move
pour déplacer des objets automatiques en dehors des fonctions, car cela inhibe "l'optimisation de la valeur de retour nommée" (NRVO).
N'utilisez jamais
std::move
pour déplacer des objets automatiques en dehors des fonctions.
Notez que dans les deux fonctions d'usine, le type de retour est une valeur et non une référence rvalue. Les références Rvalue sont toujours des références et, comme toujours, vous ne devriez jamais renvoyer de référence à un objet automatique. l'appelant se retrouverait avec une référence suspendue si vous trompiez le compilateur en acceptant votre code, comme ceci:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Ne jamais renvoyer d'objets automatiques par référence à la valeur. Le déplacement est exclusivement effectué par le constructeur du déplacement, pas par
std::move
, ni par la simple liaison d'une valeur rvalue à une référence rvalue.
Tôt ou tard, vous allez écrire du code comme ceci:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
En gros, le compilateur se plaint que parameter
est une valeur. Si vous regardez son type, vous voyez une référence rvalue, mais une référence rvalue signifie simplement "une référence liée à une rvalue"; cela signifie not signifie que la référence elle-même est une valeur réelle! En effet, parameter
n'est qu'une variable ordinaire portant un nom. Vous pouvez utiliser parameter
aussi souvent que vous le souhaitez dans le corps du constructeur, et il désigne toujours le même objet. S'en éloigner implicitement serait dangereux, c'est pourquoi le langage l'interdit.
Une référence nommée rvalue est une lvalue, comme toute autre variable.
La solution consiste à activer manuellement le déplacement:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Vous pourriez soutenir que parameter
n'est plus utilisé après l'initialisation de member
. Pourquoi n'y a-t-il pas de règle spéciale pour insérer silencieusement std::move
comme avec les valeurs de retour? Probablement parce que cela imposerait une charge trop lourde aux développeurs du compilateur. Par exemple, que se passe-t-il si le constructeur est dans une autre unité de traduction? En revanche, la règle de valeur de retour doit simplement vérifier les tables de symboles pour déterminer si l'identificateur placé après le mot clé return
désigne un objet automatique.
Vous pouvez également passer parameter
par valeur. Pour les types de déplacement tels que unique_ptr
, il semble qu'il n'y ait pas encore d'idiome établi. Personnellement, je préfère passer par valeur, car cela engendre moins d’encombrement dans l’interface.
C++ 98 déclare implicitement trois fonctions membres spéciales à la demande, c'est-à-dire lorsqu'elles sont nécessaires quelque part: le constructeur de copie, l'opérateur d'affectation de copie et le destructeur.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
Les références Rvalue sont passées par plusieurs versions. Depuis la version 3.0, C++ 11 déclare deux fonctions membres spéciales supplémentaires à la demande: le constructeur de déplacement et l'opérateur d'affectation de déplacement. Notez que ni VC10 ni VC11 ne sont encore conformes à la version 3.0, vous devrez donc les implémenter vous-même.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Ces deux nouvelles fonctions de membre spéciales ne sont déclarées implicitement que si aucune des fonctions de membre spéciales n'est déclarée manuellement. De même, si vous déclarez votre propre constructeur de déplacement ou votre opérateur d’affectation de déplacement, ni le constructeur de copie ni l’opérateur d’affectation de copie ne seront déclarés implicitement.
Que signifient ces règles dans la pratique?
Si vous écrivez une classe sans ressources non gérées, il n'est pas nécessaire de déclarer vous-même l'une des cinq fonctions membres spéciales, vous obtiendrez une sémantique de copie correcte et vous déplacerez gratuitement la sémantique. Sinon, vous devrez implémenter vous-même les fonctions spéciales des membres. Bien sûr, si votre classe ne bénéficie pas de la sémantique du déplacement, il n’est pas nécessaire de mettre en œuvre les opérations de déplacement spéciales.
Notez que l'opérateur d'affectation de copie et l'opérateur d'affectation de déplacement peuvent être fusionnés en un seul opérateur d'affectation unifié, en prenant son argument par valeur:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
De cette façon, le nombre de fonctions membres spéciales à implémenter passe de cinq à quatre. Il y a un compromis entre sécurité des exceptions et efficacité, mais je ne suis pas un expert en la matière.
Considérez le modèle de fonction suivant:
template<typename T>
void foo(T&&);
Vous pouvez vous attendre à ce que T&&
ne se lie qu'à des valeurs rvalues, car à première vue, elles ressemblent à une référence rvalue. Il s'avère que T&&
se lie également à lvalues:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Si l'argument est une valeur de type X
, on en déduit que T
est X
, d'où T&&
signifie X&&
. C'est ce à quoi tout le monde s'attendrait. Mais si l'argument est une lvalue de type X
, en raison d'une règle spéciale, on déduit que T
est X&
, donc T&&
voudrait dire quelque chose comme X& &&
. Mais comme C++ n’a toujours pas de notion de référence à référence, le type X& &&
est réduit dans X&
. Cela peut sembler déroutant et inutile au début, mais la réduction des références est essentielle pour transmission parfaite (qui ne sera pas abordé ici).
T && n'est pas une référence de valeur, mais une référence de transmission. Il se lie également à lvalues, auquel cas
T
etT&&
sont tous deux des références à lvalue.
Si vous voulez contraindre un modèle de fonction à des valeurs, vous pouvez combiner SFINAE avec des traits de type:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Maintenant que vous comprenez le regroupement des références, voici comment std::move
est implémenté:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Comme vous pouvez le constater, move
accepte n'importe quel type de paramètre grâce à la référence de transfert T&&
et renvoie une référence rvalue. L'appel de méta-fonction std::remove_reference<T>::type
est nécessaire car sinon, pour les valeurs de type X
, le type de retour serait X& &&
, ce qui se transformerait en X&
. Puisque t
est toujours une lvalue (rappelez-vous qu’une référence rvalue nommée est une lvalue), mais nous voulons lier t
à une référence rvalue, nous devons explicitement transtyper t
vers la bonne type de retour. L'appel d'une fonction qui retourne une référence rvalue est lui-même une xvalue. Maintenant vous savez d'où viennent les valeurs x;)
L'appel d'une fonction qui retourne une référence rvalue, telle que
std::move
, est une xvalue.
Notez que le retour par rvalue est correct dans cet exemple, car t
ne désigne pas un objet automatique, mais un objet qui a été transmis par l'appelant.
La sémantique de déplacement est basée sur rvalue references .
Une valeur est un objet temporaire, qui va être détruit à la fin de l'expression. Dans C++ actuel, les valeurs rvalues ne sont liées qu'à const
références. C++ 1x autorisera les références non -const
rvalue, orthographiées T&&
, qui sont des références à des objets rvalue.
Puisqu'une valeur va mourir à la fin d'une expression, vous pouvez voler ses données . Au lieu de le copier dans un autre objet, vous y déplacez ses données. .
class X {
public:
X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
: data_()
{
// since 'x' is an rvalue object, we can steal its data
this->swap(std::move(rhs));
// this will leave rhs with the empty data
}
void swap(X&& rhs);
// ...
};
// ...
X f();
X x = f(); // f() returns result as rvalue, so this calls move-ctor
Dans le code ci-dessus, avec les anciens compilateurs, le résultat de f()
est copié dans x
avec Le constructeur de copie de X
. Si votre compilateur prend en charge la sémantique de déplacement et que X
a un constructeur de déplacement, il est alors appelé. Puisque son argument rhs
est une rvalue , nous savons que ce n'est plus nécessaire et nous pouvons voler sa valeur.
La valeur est donc déplacé du temporaire sans nom renvoyé de f()
à x
(tandis que les données de x
, initialisées à un X
vide, sont déplacées dans le temporaire, qui sera détruit après l'affectation).
Supposons que vous ayez une fonction qui retourne un objet substantiel:
Matrix multiply(const Matrix &a, const Matrix &b);
Lorsque vous écrivez un code comme celui-ci:
Matrix r = multiply(a, b);
alors un compilateur C++ ordinaire créera un objet temporaire pour le résultat de multiply()
, appellera le constructeur de copie pour initialiser r
, puis détruira la valeur de retour temporaire. La sémantique de déplacement dans C++ 0x autorise le "constructeur de déplacement" à être initialisé pour initialiser r
en copiant son contenu, puis supprime la valeur temporaire sans la détruire.
Ceci est particulièrement important si (comme peut-être l'exemple Matrix
ci-dessus), l'objet en cours de copie alloue de la mémoire supplémentaire sur le tas pour stocker sa représentation interne. Un constructeur de copie doit soit créer une copie complète de la représentation interne, soit utiliser le comptage de références et la sémantique de copie sur écriture de manière interalinique. Un constructeur de mouvements laisse la mémoire de tas vide et ne fait que copier le pointeur à l'intérieur de l'objet Matrix
.
Si vous êtes vraiment intéressé par une bonne explication détaillée de la sémantique des déplacements, je vous recommande vivement de lire le document d'origine, "Proposition pour ajouter une prise en charge de la sémantique des déplacements au langage C++."
Il est très accessible et facile à lire, ce qui en fait un excellent exemple des avantages qu’ils offrent. Il existe d’autres documents plus récents et à jour sur la sémantique des déplacements disponibles sur le site Web du WG21 , mais celui-ci est probablement le plus simple, car il aborde les choses d’un point de vue global et ne reçoit pas beaucoup d'informations. beaucoup dans les détails de la langue graveleuse.
Déplacer la sémantique concerne transférer des ressources plutôt que de les copier lorsque personne n'a plus besoin de la valeur source.
En C++ 03, les objets sont souvent copiés pour être détruits ou affectés avant qu'un code ne réutilise la valeur. Par exemple, lorsque vous renvoyez la valeur d'une fonction (à moins que RVO ne soit activé), la valeur que vous renvoyez est copiée dans le cadre de pile de l'appelant, puis elle sort de la portée et est détruite. C’est juste un exemple parmi d’autres: voir pass-par-valeur quand l’objet source est temporaire, des algorithmes comme sort
qui réorganisent simplement les éléments, une réallocation dans vector
lorsque sa capacity()
est dépassée , etc.
Lorsque ces paires copie/destruction sont coûteuses, c'est généralement parce que l'objet possède des ressources très lourdes. Par exemple, vector<string>
peut posséder un bloc de mémoire alloué de manière dynamique contenant un tableau d'objets string
, chacun avec sa propre mémoire dynamique. Copier un tel objet est coûteux: vous devez allouer une nouvelle mémoire pour chaque bloc alloué de manière dynamique dans la source et copier toutes les valeurs. Ensuite vous avez besoin de libérer toute la mémoire que vous venez de copier. Cependant, déplacer un grand vector<string>
signifie simplement copier quelques pointeurs (qui font référence au bloc de mémoire dynamique) vers la destination et les mettre à zéro dans la source.
En termes simples (pratiques):
Copier un objet signifie copier ses membres "statiques" et appeler l'opérateur new
pour ses objets dynamiques. Droite?
class A
{
int i, *p;
public:
A(const A& a) : i(a.i), p(new int(*a.p)) {}
~A() { delete p; }
};
Cependant, déplacer un objet (je le répète, d’un point de vue pratique) n’implique que de copier les pointeurs d’objets dynamiques, et non de en créer de nouveaux.
Mais n'est-ce pas dangereux? Bien sûr, vous pouvez détruire un objet dynamique deux fois (erreur de segmentation). Donc, pour éviter cela, vous devriez "invalider" les pointeurs sources pour éviter de les détruire deux fois:
class A
{
int i, *p;
public:
// Movement of an object inside a copy constructor.
A(const A& a) : i(a.i), p(a.p)
{
a.p = nullptr; // pointer invalidated.
}
~A() { delete p; }
// Deleting NULL, 0 or nullptr (address 0x0) is safe.
};
Ok, mais si je déplace un objet, l'objet source devient inutile, non? Bien sûr, mais dans certaines situations, c'est très utile. Le plus évident est lorsque j'appelle une fonction avec un objet anonyme (temporal, objet rvalue, ..., vous pouvez l'appeler avec des noms différents):
void heavyFunction(HeavyType());
Dans cette situation, un objet anonyme est créé, copié ensuite dans le paramètre de fonction et ensuite supprimé. Dans ce cas, il est préférable de déplacer l'objet, car vous n'avez pas besoin de l'objet anonyme et vous pouvez gagner du temps et de la mémoire.
Cela conduit au concept de référence "rvalue". Ils existent dans C++ 11 uniquement pour détecter si l'objet reçu est anonyme ou non. Je pense que vous savez déjà qu'une "valeur" est une entité assignable (la partie gauche de l'opérateur =
], vous avez donc besoin d'une référence nommée à un objet pour pouvoir agir en tant que valeur. Une valeur est exactement le contraire, un objet sans référence nommée. De ce fait, objet anonyme et rvalue sont synonymes. Alors:
class A
{
int i, *p;
public:
// Copy
A(const A& a) : i(a.i), p(new int(*a.p)) {}
// Movement (&& means "rvalue reference to")
A(A&& a) : i(a.i), p(a.p)
{
a.p = nullptr;
}
~A() { delete p; }
};
Dans ce cas, lorsqu'un objet de type A
doit être "copié", le compilateur crée une référence lvalue ou une référence rvalue selon que l'objet transmis est nommé ou non. Sinon, votre constructeur de mouvements est appelé et vous savez que l'objet est temporel. Vous pouvez déplacer ses objets dynamiques au lieu de les copier, en économisant de l'espace et de la mémoire.
Il est important de se rappeler que les objets "statiques" sont toujours copiés. Il n'y a aucun moyen de "déplacer" un objet statique (objet dans la pile et non sur le tas). Ainsi, la distinction "déplacer"/"copier" lorsqu'un objet n'a pas de membre dynamique (directement ou indirectement) est sans importance.
Si votre objet est complexe et que le destructeur a d'autres effets secondaires, comme appeler une fonction de bibliothèque, appeler d'autres fonctions globales ou quoi que ce soit, il est peut-être préférable de signaler un mouvement avec un drapeau:
class Heavy
{
bool b_moved;
// staff
public:
A(const A& a) { /* definition */ }
A(A&& a) : // initialization list
{
a.b_moved = true;
}
~A() { if (!b_moved) /* destruct object */ }
};
Votre code est donc plus court (vous n'avez pas besoin de faire d'assignation nullptr
pour chaque membre dynamique) et plus général.
Autre question typique: quelle est la différence entre A&&
et const A&&
? Bien sûr, dans le premier cas, vous pouvez modifier l'objet et dans le second non, mais, sens pratique? Dans le second cas, vous ne pouvez pas le modifier, vous n'avez donc aucun moyen d'invalider l'objet (sauf avec un indicateur mutable ou quelque chose du genre), et il n'y a pas de différence pratique pour un constructeur de copie.
Et qu'est-ce que transmission parfaite? Il est important de savoir qu'une "référence de valeur" est une référence à un objet nommé dans la "portée de l'appelant". Mais dans la portée réelle, une référence rvalue est un nom attribué à un objet, elle agit donc comme un objet nommé. Si vous transmettez une référence rvalue à une autre fonction, vous transmettez un objet nommé. L'objet n'est donc pas reçu comme un objet temporel.
void some_function(A&& a)
{
other_function(a);
}
L'objet a
serait copié dans le paramètre actuel de other_function
. Si vous voulez que l'objet a
continue d'être traité comme un objet temporaire, vous devez utiliser la fonction std::move
:
other_function(std::move(a));
Avec cette ligne, std::move
convertira a
en une valeur rvalue et other_function
recevra l'objet sous la forme d'un objet sans nom. Bien sûr, si other_function
n'a pas de surcharge spécifique pour travailler avec des objets sans nom, cette distinction n'est pas importante.
Cette transmission est-elle parfaite? Non, mais nous sommes très proches. Le transfert parfait n’est utile que pour travailler avec des modèles, dans le but de dire: si j’ai besoin de passer un objet à une autre fonction, j’ai besoin que si je reçois un objet nommé, celui-ci soit passé comme objet nommé, Je veux le transmettre comme un objet non nommé:
template<typename T>
void some_function(T&& a)
{
other_function(std::forward<T>(a));
}
C'est la signature d'une fonction prototype utilisant une transmission parfaite, implémentée en C++ 11 à l'aide de std::forward
. Cette fonction exploite certaines règles d'instanciation de template:
`A& && == A&`
`A&& && == A&&`
Donc, si T
est une référence de lvalue à A
(T = A &), a
également (A & && = > A &). Si T
est une référence rvalue à A
, a
également (A && && => A &&). Dans les deux cas, a
est un objet nommé dans la portée réelle, mais T
contient les informations de son "type de référence" du point de vue de la portée de l'appelant. Cette information (T
) est transmise comme paramètre de modèle à forward
et 'a' est déplacé ou non en fonction du type de T
.
C'est comme la sémantique de la copie, mais au lieu d'avoir à dupliquer toutes les données, vous devez voler les données de l'objet "déplacé".
Vous savez ce que signifie une sémantique de copie, n'est-ce pas? cela signifie que vous avez des types qui sont copiables, pour les types définis par l'utilisateur, vous définissez ceci: achetez explicitement en écrivant un constructeur de copie et un opérateur d'affectation ou le compilateur les génère de manière implicite. Cela fera une copie.
La sémantique de déplacement est fondamentalement un type défini par l'utilisateur avec un constructeur qui prend une référence de valeur r (nouveau type de référence utilisant && (oui deux esperluètes)) qui est non const, on l'appelle un constructeur de déplacement, idem pour l'opérateur d'assignation. Alors, que fait un constructeur de déplacements? Au lieu de copier la mémoire de son argument source, il "déplace" la mémoire de la source à la destination.
Quand voudriez-vous faire ça? well std :: vector est un exemple, disons que vous avez créé un std :: vector temporaire et que vous le renvoyez à partir d'une fonction, par exemple:
std::vector<foo> get_foos();
Lorsque la fonction retourne, vous aurez une surcharge du constructeur de copie. Si (et cela se fera en C++ 0x), std :: vector a un constructeur de déplacement au lieu de le copier. Il vous suffit de définir ses pointeurs et de 'déplacer' alloué dynamiquement. mémoire à la nouvelle instance. C'est un peu comme la sémantique du transfert de propriété avec std :: auto_ptr.
J'écris ceci pour m'assurer de bien comprendre.
Les sémantiques de déplacement ont été créées pour éviter la copie inutile d'objets volumineux. Bjarne Stroustrup dans son livre "Le langage de programmation C++" utilise deux exemples dans lesquels une copie inutile se produit par défaut: une, l'échange de deux objets volumineux et deux, le retour d'un objet volumineux à partir d'une méthode.
L'échange de deux objets volumineux implique généralement la copie du premier objet dans un objet temporaire, la copie du deuxième objet dans le premier objet et la copie de l'objet temporaire dans le deuxième objet. Ceci est très rapide pour un type intégré, mais pour les gros objets, ces trois copies peuvent prendre beaucoup de temps. Une "affectation de déplacement" permet au programmeur de remplacer le comportement de copie par défaut et d'échanger des références aux objets, ce qui signifie qu'il n'y a pas de copie du tout et que l'opération d'échange est beaucoup plus rapide. L'affectation de déplacement peut être appelée en appelant la méthode std :: move ().
Renvoyer un objet d'une méthode par défaut implique de faire une copie de l'objet local et de ses données associées dans un emplacement accessible à l'appelant (car l'objet local n'est pas accessible à l'appelant et disparaît à la fin de la méthode). Lorsqu'un type intégré est renvoyé, cette opération est très rapide, mais si un objet volumineux est renvoyé, l'opération peut durer longtemps. Le constructeur de mouvements permet au programmeur de remplacer ce comportement par défaut et de "réutiliser" les données de segment associées à l'objet local en pointant l'objet renvoyé à l'appelant vers les données de segment associées à l'objet local. Ainsi, aucune copie n'est requise.
Dans les langages qui ne permettent pas la création d'objets locaux (c'est-à-dire d'objets sur la pile), ce type de problèmes ne se produit pas car tous les objets sont alloués sur le tas et sont toujours accessibles par référence.
Pour illustrer le besoin de sémantique de déplacement , considérons cet exemple sans sémantique de déplacement:
Voici une fonction qui prend un objet de type T
et retourne un objet du même type T
:
T f(T o) { return o; }
//^^^ new object constructed
La fonction ci-dessus utilise appel par valeur , ce qui signifie que lorsque cette fonction est appelée, un objet doit être construit pour pouvoir être utilisé par la fonction. .
Étant donné que la fonction renvoie également par valeur , un autre nouvel objet est créé pour la valeur renvoyée:
T b = f(a);
//^ new object constructed
Deux nouveaux objets ont été construits, dont l'un est un objet temporaire utilisé uniquement pendant la durée de la fonction.
Lorsque le nouvel objet est créé à partir de la valeur de retour, le constructeur de copie est appelé pour copier le contenu de l'objet temporaire dans le nouvel objet b. Une fois la fonction terminée, l'objet temporaire utilisé dans la fonction sort de son cadre et est détruit.
Voyons maintenant ce que fait un constructeur de copie .
Il doit d'abord initialiser l'objet, puis copier toutes les données pertinentes de l'ancien objet vers le nouvel.
En fonction de la classe, c’est peut-être un conteneur avec beaucoup de données, alors cela pourrait représenter beaucoup temps et utilisation de la mémoire
// Copy constructor
T::T(T &old) {
copy_data(m_a, old.m_a);
copy_data(m_b, old.m_b);
copy_data(m_c, old.m_c);
}
Avec déplacer la sémantique , il est maintenant possible de rendre la plupart de ce travail moins désagréable en déplaçant simplement les données que de copier.
// Move constructor
T::T(T &&old) noexcept {
m_a = std::move(old.m_a);
m_b = std::move(old.m_b);
m_c = std::move(old.m_c);
}
Le déplacement des données implique la ré-association des données avec le nouvel objet. Et aucune copie n'a lieu .
Ceci est accompli avec une référence rvalue
.
Une référence rvalue
fonctionne assez bien comme une référence lvalue
avec une différence importante:
une rvalue référence peut être déplacée et une lvalue ne peut pas.
De cppreference.com :
Pour permettre une garantie d'exception forte, les constructeurs de mouvements définis par l'utilisateur ne doivent pas lancer d'exceptions. En fait, les conteneurs standard reposent généralement sur std :: move_if_noexcept pour choisir entre le déplacement et la copie lorsque des éléments de conteneur doivent être déplacés. Si les constructeurs copier et déplacer sont tous deux fournis, la résolution de surcharge sélectionne le constructeur de déplacement si l'argument est une valeur rvalue (une valeur telle que prédéfinie, telle qu'un temporaire sans nom ou une valeur x, telle que le résultat de std :: move), et sélectionne le constructeur de copie si l'argument est une lvalue (objet nommé ou une fonction/opérateur renvoyant la référence lvalue). Si seul le constructeur de copie est fourni, toutes les catégories d'arguments le sélectionnent (tant qu'il faut une référence à const, car les valeurs peuvent se lier à des références const), ce qui rend la copie du repli pour le déplacement, lorsque le déplacement est indisponible. Dans de nombreuses situations, les constructeurs de mouvements sont optimisés même s'ils produiraient des effets secondaires observables (voir Élision du texte). Un constructeur est appelé "constructeur de déplacement" lorsqu'il prend une référence rvalue en tant que paramètre. Il n'est pas obligé de déplacer quoi que ce soit, il n'est pas nécessaire que la classe ait une ressource à déplacer et un "constructeur de déplacement" peut ne pas être en mesure de déplacer une ressource comme dans le cas autorisé (mais peut-être pas raisonnable) où le paramètre est un const rvalue reference (const T &&).