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 exprès pour rester simple. Cependant, il y a beaucoup plus à faire pour déplacer la sémantique, et je pensais qu'il était temps pour une deuxième réponse de combler les lacunes. La première réponse est déjà assez ancienne et il ne semblait pas juste de la remplacer simplement par un texte complètement différent. Je pense que cela sert toujours bien de première introduction. Mais si vous voulez approfondir, lisez la suite :)
Stephan T. Lavavej a pris le temps de fournir de précieux commentaires. Merci beaucoup, Stephan!
introduction
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 mouvements 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'offre aucun avantage par rapport à la sémantique de copie. Dans ce cas, copier un objet et déplacer un objet 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 sécurisés «de déplacement uniquement»; c'est-à-dire les types pour lesquels la copie n'a pas de sens, mais le déplacement 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 d' std::auto_ptr
un modèle de bibliothèque standard C ++ 98 obsolète, qui a été remplacé par std::unique_ptr
C ++ 11. Les programmeurs C ++ intermédiaires sont probablement au moins assez familiers et std::auto_ptr
, en raison de la "sémantique de déplacement" qu'il affiche, cela semble être un bon point de départ pour discuter de la sémantique de déplacement en C ++ 11. YMMV.
Qu'est-ce qu'un déménagement?
La bibliothèque standard C ++ 98 propose un pointeur intelligent avec une sémantique de propriété unique appelée std::auto_ptr<T>
. Si vous n'êtes pas familier auto_ptr
, son but est de garantir qu'un objet alloué dynamiquement est toujours libéré, même en cas 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
Ce qui est inhabituel, auto_ptr
c'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 que l'initialisation de b
with a
ne copie pas le triangle, mais transfère à la place la propriété du triangle de a
à b
. Nous disons également " a
est déplacé vers b
" ou "le triangle est déplacé de a
vers b
". Cela peut sembler déroutant car le triangle lui-même reste toujours au même endroit dans la 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 à quelque chose comme 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
}
Mouvements dangereux et inoffensifs
Ce qui est dangereux, auto_ptr
c'est que ce qui ressemble syntaxiquement à une copie est en fait un mouvement. Essayer d'appeler une fonction membre sur un élément déplacé auto_ptr
invoquera un comportement indéfini, vous devez donc être très prudent de ne pas utiliser un élément auto_ptr
après son déplacement:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Mais ce auto_ptr
n'est pas toujours dangereux. Les fonctions d'usine sont un cas d'utilisation parfaitement adapté 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 comment les deux exemples suivent le même modèle syntaxique:
auto_ptr<Shape> variable(expression);
double area = expression->area();
Et pourtant, l'un d'eux invoque un comportement indéfini, tandis que l'autre ne le fait pas. Quelle est donc la différence entre les expressions a
et make_triangle()
? Ne sont-ils pas tous les deux du même type? En effet, ils le sont, mais ils ont des catégories de valeurs différentes .
Catégories de valeur
De toute évidence, il doit y avoir une différence profonde entre l'expression a
qui dénote une auto_ptr
variable et l'expression make_triangle()
qui dénote l'appel d'une fonction qui renvoie une auto_ptr
valeur par, créant ainsi un nouvel auto_ptr
objet temporaire à chaque appel. a
est un exemple de valeur l , alors que make_triangle()
c'est un exemple de valeur r .
Passer de lvalues telles que a
est dangereux, car nous pourrions plus tard essayer d'appeler une fonction membre via a
, en invoquant un comportement non défini. D'un autre côté, passer de rvalues telles que make_triangle()
est parfaitement sûr, car une fois que le constructeur de copie a fait son travail, nous ne pouvons plus utiliser le temporaire. Il n'y a aucune expression qui dénote ledit temporaire; si nous écrivons simplement à make_triangle()
nouveau, nous obtenons un autre 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 le côté gauche et le côté droit d'une affectation. Ce n'est plus le cas en C ++, car il y a des valeurs qui ne peuvent pas apparaître sur le côté gauche d'une affectation (comme des tableaux ou des types définis par l'utilisateur sans opérateur d'affectation), et il y a des rvalues qui peuvent (toutes les rvalues des types de classe avec un opérateur d'affectation).
Une valeur r de type classe est une expression dont l'évaluation crée un objet temporaire. Dans des circonstances normales, aucune autre expression à l'intérieur de la même portée ne dénote le même objet temporaire.
Références de valeur
Nous comprenons maintenant que passer de lvalues est potentiellement dangereux, mais passer de rvalues est inoffensif. Si C ++ avait un support de langage pour distinguer les arguments lvalue des arguments rvalue, nous pourrions soit interdire complètement le déplacement à partir de lvalues, soit au moins rendre explicite le déplacement à partir de lvalues sur le site d'appel, afin de ne plus bouger par accident.
La réponse de C ++ 11 à ce problème est les références rvalue . Une référence rvalue est un nouveau type de référence qui se lie uniquement aux rvalues, et la syntaxe est X&&
. La bonne vieille référence X&
est maintenant connue sous le nom de référence lvalue . (Notez que ce X&&
n'est pas une référence à une référence; il n'y a rien de tel en C ++.)
Si nous jetons const
dans le mélange, 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&&
. Restreindre à lire à partir des valeurs r n'est pas très utile.
Une référence rvalue X&&
est un nouveau type de référence qui se lie uniquement aux rvalues.
Conversions implicites
Les références Rvalue sont passées par plusieurs versions. Depuis la version 2.1, une référence rvalue X&&
se lie également à toutes les catégories de valeurs d'un type différent Y
, à condition qu'il y ait une conversion implicite de Y
à 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 l de type const char[12]
. Puisqu'il y a 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 temporelles (objets) est un peu floue.
Déplacer les constructeurs
Un exemple utile de fonction avec un X&&
paramètre est le constructeur de déplacement X::X(X&& source)
. Son objectif 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 d'une version simplifiée de unique_ptr
. Tout d'abord, nous encapsulons un pointeur brut et surchargeons les opérateurs ->
et *
, par conséquent, notre classe se sent 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 mouvement:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Ce constructeur de déplacement fait exactement ce que le auto_ptr
constructeur de copie a fait, mais il ne peut être fourni qu'avec des 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 à compiler, car il a
s'agit d'une valeur l, mais le paramètre unique_ptr&& source
ne peut être lié qu'à des valeurs r. C'est exactement ce que nous voulions; les mouvements dangereux ne devraient jamais être implicites. La troisième ligne se compile très bien, car il make_triangle()
s'agit d'une valeur r. Le constructeur du déménagement transférera la propriété du temporaire au c
. Encore une fois, c'est exactement ce que nous voulions.
Le constructeur de déplacement transfère la propriété d'une ressource gérée dans l'objet actuel.
Déplacer les opérateurs d'affectation
La dernière pièce manquante est l'opérateur d'affectation de déplacement. 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 comment cette implémentation de l'opérateur d'affectation de déplacement duplique la logique du destructeur et du constructeur de déplacement. Connaissez-vous l'idiome de copie et d'échange? Il peut également être appliqué pour déplacer la sémantique en tant qu'idiome de déplacement et d'échange:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Maintenant que source
c'est une variable de type unique_ptr
, elle sera initialisée par le constructeur de déplacement; c'est-à-dire que l'argument sera déplacé dans le paramètre. L'argument doit toujours être une valeur r, car le constructeur de déplacement lui-même a un paramètre de référence rvalue. Lorsque le flux de contrôle atteint l'accolade de fermeture de operator=
, source
sort du champ d'application, libérant automatiquement l'ancienne ressource.
L'opérateur d'affectation de déplacement transfère la propriété d'une ressource gérée dans l'objet actuel, libérant l'ancienne ressource. L'idiome move-and-swap simplifie l'implémentation.
Passer de lvalues
Parfois, nous voulons passer de lvalues. Autrement dit, nous voulons parfois que le compilateur traite une valeur l comme s'il s'agissait d'une valeur r, de sorte qu'il peut invoquer le constructeur de déplacement, même s'il peut ê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
il jette simplement une valeur l à une valeur r; il ne pas bouger quoi que ce soit par lui - même. Il permet simplement de bouger. Peut-être qu'il aurait dû être nommé std::cast_to_rvalue
ou std::enable_move
, mais nous sommes coincés avec le nom maintenant.
Voici comment vous passez explicitement d'une valeur l:
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 en écrivant explicitementstd::move(a)
, nous avons clairement exprimé nos intentions: "Cher constructeur, faites tout ce que vous voulez a
pour initialiser c
; je m'en fiche a
plus. N'hésitez pas à vous débrouiller a
."
std::move(some_lvalue)
convertit une lvalue en une rvalue, permettant ainsi un déplacement ultérieur.
Xvalues
Notez que même s'il std::move(a)
s'agit d'une valeur r, son évaluation ne crée pas d'objet temporaire. Cette énigme a forcé le comité à introduire une troisième catégorie de valeur. Quelque chose qui peut être lié à une référence rvalue, même s'il ne s'agit pas d'une valeur r au sens traditionnel, est appelé une valeur x (valeur eXpiring). Les valeurs traditionnelles ont été renommées en valeurs (valeurs pures).
Les valeurs pr et x sont des valeurs r. Les valeurs X et les valeurs l sont toutes deux des valeurs gl (valeurs l généralisées). Les relations sont plus faciles à saisir avec un diagramme:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Notez que seules les valeurs x sont vraiment nouvelles; le reste est simplement dû au changement de nom et au regroupement.
C ++ 98 rvalues sont appelées prvalues dans C ++ 11. Remplacez mentalement toutes les occurrences de "rvalue" dans les paragraphes précédents par "prvalue".
Sortir des fonctions
Jusqu'à présent, nous avons vu des mouvements dans des variables locales et dans des paramètres de fonction. Mais le déplacement est également possible en sens inverse. Si une fonction renvoie par valeur, un objet sur le site d'appel (probablement une variable locale ou temporaire, mais pourrait être n'importe quel type d'objet) est initialisé avec l'expression après l' return
instruction comme argument pour le 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());
De manière surprenante, les objets automatiques (variables locales qui ne sont pas déclarées comme static
) peuvent également être implicitement retiré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 mouvement accepte la valeur l result
comme argument? La portée de result
est sur le point de se terminer et elle sera détruite lors du déroulement de la pile. Personne ne pouvait se plaindre par la suite qui result
avait changé d'une manière ou d'une autre; lorsque le flux de contrôle est de retour à 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 ne devez jamais utiliser std::move
pour déplacer des objets automatiques hors des fonctions, car cela inhibe l '"optimisation de la valeur de retour nommée" (NRVO).
Ne jamais utiliser std::move
pour déplacer des objets automatiques hors des fonctions.
Notez que dans les deux fonctions d'usine, le type de retour est une valeur, pas une référence rvalue. Les références Rvalue sont toujours des références, et comme toujours, vous ne devez jamais renvoyer une référence à un objet automatique; l'appelant se retrouverait avec une référence pendant si vous incitiez le compilateur à accepter 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 renvoyez jamais d'objets automatiques par référence rvalue. Le déplacement est exclusivement effectué par le constructeur de déplacement, pas par std::move
et pas simplement en liant une rvalue à une référence rvalue.
Entrer dans les membres
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
{}
};
Fondamentalement, le compilateur se plaindra qu'il parameter
s'agit d'une valeur l. Si vous regardez son type, vous voyez une référence rvalue, mais une référence rvalue signifie simplement "une référence qui est liée à une rvalue"; cela ne signifie pas que la référence elle-même est une valeur! En effet, parameter
c'est juste une variable ordinaire avec un nom. Vous pouvez utiliser parameter
autant de fois que vous le souhaitez à l'intérieur du corps du constructeur, et il désigne toujours le même objet. S'en éloigner implicitement serait dangereux, d'où la langue l'interdit.
Une référence rvalue nommée 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
{}
};
On pourrait dire que ce parameter
n'est plus utilisé après l'initialisation de member
. Pourquoi n'y a-t-il pas de règle spéciale à insérer silencieusement std::move
comme pour les valeurs de retour? Probablement parce que ce serait trop lourd pour les implémenteurs du compilateur. Par exemple, que se passe-t-il si le corps du constructeur se trouve 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'identifiant après le return
mot - clé désigne un objet automatique.
Vous pouvez également transmettre la parameter
valeur par. Pour les types à déplacement uniquement comme unique_ptr
, il semble qu'il n'y ait pas encore d'idiome établi. Personnellement, je préfère passer par valeur, car cela provoque moins d'encombrement dans l'interface.
Fonctions spéciales des membres
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 à la demande deux fonctions membres spéciales supplémentaires: 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 membres spéciaux ne sont déclarées implicitement que si aucune des fonctions membres spéciales n'est déclarée manuellement. De plus, si vous déclarez votre propre constructeur de déplacement ou 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, et vous obtiendrez une copie sémantique correcte et déplacerez la sémantique gratuitement. 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 des mouvements, il n'est pas nécessaire d'implémenter 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 à faire entre sécurité d'exception et efficacité ici, mais je ne suis pas un expert en la matière.
Renvoi de références ( précédemment appelées références universelles )
Considérez le modèle de fonction suivant:
template<typename T>
void foo(T&&);
Vous pouvez vous attendre T&&
à ne vous lier qu'à rvalues, car à première vue, cela ressemble à une référence rvalue. Comme il s'avère cependant, T&&
se lie également aux valeurs l:
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 r de type X
, T
se déduit être X
, donc T&&
moyen 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, T
on en déduit être X&
, par conséquent T&&
signifierait quelque chose comme X& &&
. Mais puisque C ++ n'a toujours aucune notion de références aux références, le type X& &&
est réduit en X&
. Cela peut sembler déroutant et inutile au début, mais l'effondrement des références est essentiel pour une transmission parfaite (qui ne sera pas discutée ici).
T&& n'est pas une référence de valeur, mais une référence de transfert. Il se lie également aux valeurs l, auquel cas T
et T&&
sont toutes deux des références lvalue.
Si vous souhaitez contraindre un modèle de fonction à rvalues, 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&&);
Mise en œuvre du déménagement
Maintenant que vous comprenez l'effondrement 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 voir, move
accepte tout type de paramètre grâce à la référence de transfert T&&
et renvoie une référence rvalue. L' std::remove_reference<T>::type
appel de la méta-fonction est nécessaire car sinon, pour les valeurs de type X
, le type de retour serait X& &&
, ce qui s'effondrerait X&
. Étant donné qu'il t
s'agit toujours d'une valeur l (rappelez-vous qu'une référence rvalue nommée est une valeur l), mais que nous voulons nous lier t
à une référence rvalue, nous devons explicitement convertir t
le type de retour correct. L'appel d'une fonction qui renvoie une référence rvalue est lui-même une valeur x. Vous savez maintenant d'où viennent les valeurs x;)
L'appel d'une fonction qui renvoie une référence rvalue, telle que std::move
, est une valeur x.
Notez que le retour par référence rvalue est correct dans cet exemple, car t
ne dénote pas un objet automatique, mais plutôt un objet qui a été transmis par l'appelant.