Pour comprendre pourquoi c'est un bon modèle, nous devrions examiner les solutions de rechange, à la fois en C++03 et en C++11.
Nous avons le C++03 méthode de 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'un raw chaîne C, std::string
sera construit, puis copier à nouveau: deux allocations.
Il y a le C++03 méthode de prise de référence à un std::string
, puis de les échanger dans un local std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
qui est le C++03 version de "sémantique de déplacement", et swap
peut souvent être optimisé pour être très bon marché de faire (un peu comme un move
). Il doit également être analysé dans le 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. (Temporaire std::string
ne peut pas se lier à un non-const de référence). Une seule allocation est faite, toutefois. Le C++11 version prendrait &&
et vous obliger à appeler avec std::move
, ou avec une temporaire: cela nécessite que l'appelant explicitement crée une copie de l'extérieur de l'appel, et de déplacer 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 le plein de C++11 version, qui prend en charge la copie et de l' 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 alors examiner comment il 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 évident que ces 2 la surcharge de la technique est au moins aussi efficace, sinon plus, que les deux ci-dessus C++03 styles. Je vais dub ce 2-surcharge version "optimale" de la version.
Maintenant, nous allons examiner le prendre-par-une copie de la version:
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 ce côté-à-côte avec les plus "optimale" version, nous faisons exactement un move
! Pas une fois, faisons-nous un extra - copy
.
Donc, si nous supposons que move
n'est pas cher, cette version nous obtient de près les mêmes performances que la plus optimale version, mais 2 fois moins de code.
Et si vous prenez le dire de 2 à 10 arguments, la réduction de la code est exponentielle -- 2x fois moins avec 1 argument, 4x avec 2, 8x avec 3, 16x avec 4, 1024x avec les 10 arguments.
Maintenant, nous pouvons contourner ce problème via le transfert parfait et SFINAE, vous permettant d'écrire un constructeur unique ou un modèle de fonction qui prend 10 arguments, ne SFINAE pour s'assurer que les arguments sont de types appropriés, puis se déplace ou copie dans le local de l'état requis. Tout cela empêche le mille fois augmentation de la taille du programme de problème, il peut encore y avoir tout un tas de fonctions générées à partir de ce modèle. (fonction de modèle instanciations générer des fonctions)
Et beaucoup de généré des fonctions plus grand du code exécutable de la taille, ce qui peut réduire les performances.
Pour le coût de quelques move
s, nous avons une réduction de la taille du code et à peu près la même performance, et souvent plus facile de comprendre le code.
Maintenant, cela ne fonctionne que parce que nous savons que, lorsque la fonction (dans ce cas, un constructeur) est appelée, que nous allons vouloir une copie locale de cet argument. L'idée est que si nous savons que nous allons faire une copie, nous devrions laisser l'appelant savons que nous sommes de faire une copie en le mettant dans notre liste d'arguments. Ils peuvent ainsi optimiser autour du fait qu'ils vont nous donner une copie (par déplacement dans notre argument, par exemple).
Un autre avantage de le "prendre par la valeur" technique est que, souvent, des constructeurs de déplacement sont noexcept. Cela signifie que les fonctions qui prennent en valeur et de sortir de leur argument peut souvent être noexcept, le déplacement de tous throw
s de leur corps, et dans le contexte appelant (qui peut l'éviter par la construction parfois, ou de construire des éléments et de l' move
dans l'argument, de contrôler où jeter arrive). Méthodes de prise de nothrow est souvent la peine.