Pour comprendre pourquoi c'est un bon modèle, nous devons examiner les alternatives, à la fois en C ++ 03 et en C ++ 11.
Nous avons la méthode C ++ 03 pour prendre un std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
dans ce cas, il y aura toujours une seule copie effectuée. Si vous construisez à partir d'une chaîne C brute, un std::string
sera construit, puis copié à nouveau: deux allocations.
Il existe la méthode C ++ 03 pour prendre une référence à a std::string
, puis la permuter dans un local std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
c'est la version C ++ 03 de "move semantics", et swap
peut souvent être optimisée pour être très bon marché (un peu comme a move
). Il doit également être analysé dans son contexte:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
et vous oblige à former un non-temporaire std::string
, puis à le jeter. (Un temporaire std::string
ne peut pas se lier à une référence non-const). Une seule allocation est cependant effectuée. La version C ++ 11 prendrait un &&
et vous demanderait de l'appeler avec std::move
, ou avec un temporaire: cela nécessite que l'appelant crée explicitement une copie en dehors de l'appel et déplace cette copie dans la fonction ou le constructeur.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Utilisation:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Ensuite, nous pouvons faire la version complète de C ++ 11, qui prend en charge à la fois la copie et move
:
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
Nous pouvons ensuite examiner comment cela est utilisé:
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
Il est assez clair que cette technique de surcharge est au moins aussi efficace, sinon plus, que les deux styles C ++ 03 ci-dessus. Je vais doubler cette version à 2 surcharges la version "la plus optimale".
Maintenant, nous allons examiner la version à prendre par copie:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
dans chacun de ces scénarios:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
Si vous comparez cette version côte à côte avec la version "la plus optimale", nous en faisons exactement une de plus move
! Pas une seule fois, nous ne faisons un extra copy
.
Donc, si nous supposons que move
c'est bon marché, cette version nous offre presque les mêmes performances que la version la plus optimale, mais 2 fois moins de code.
Et si vous prenez par exemple 2 à 10 arguments, la réduction du code est exponentielle - 2x fois moins avec 1 argument, 4x avec 2, 8x avec 3, 16x avec 4, 1024x avec 10 arguments.
Maintenant, nous pouvons contourner cela via un transfert parfait et SFINAE, vous permettant d'écrire un seul constructeur ou modèle de fonction qui prend 10 arguments, fait SFINAE pour s'assurer que les arguments sont de types appropriés, puis les déplace ou les copie dans le état local selon les besoins. Bien que cela évite l'augmentation de mille fois du problème de la taille du programme, il peut encore y avoir toute une pile de fonctions générées à partir de ce modèle. (les instanciations de fonction de modèle génèrent des fonctions)
Et de nombreuses fonctions générées signifient une taille de code exécutable plus grande, ce qui peut en soi réduire les performances.
Pour le coût de quelques move
secondes, nous obtenons un code plus court et presque les mêmes performances, et souvent un code plus facile à comprendre.
Maintenant, cela ne fonctionne que parce que nous savons, lorsque la fonction (dans ce cas, un constructeur) est appelée, que nous voulons une copie locale de cet argument. L'idée est que si nous savons que nous allons faire une copie, nous devrions informer l'appelant que nous faisons une copie en la plaçant dans notre liste d'arguments. Ils peuvent alors s'optimiser autour du fait qu'ils vont nous en donner une copie (en se déplaçant dans notre argumentation, par exemple).
Un autre avantage de la technique de «prise par valeur» est que les constructeurs de déplacement sont souvent noexcept. Cela signifie que les fonctions qui prennent par valeur et sortent de leur argument peuvent souvent être noexcept, déplaçant les throw
s hors de leur corps et dans la portée d'appel (qui peut parfois l'éviter via la construction directe, ou construire les éléments et move
dans l'argument, pour contrôler où se produit le lancer).