97 votes

Pourquoi est-ce nous copier puis passer ?

J'ai vu le code quelque part où quelqu'un a décidé de copier un objet et ensuite le déplacer à un membre d'une classe. Cela m'a laissé dans la confusion en ce que je pensais tout point de mouvement a été d'éviter la copie. Voici l'exemple:

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

Voici mes questions:

  • Pourquoi ne sommes-nous pas prendre une rvalue référence- str?
  • Ne sera pas une copie être très coûteux, surtout quelque chose comme std::string?
  • Ce serait la raison pour l'auteur de décider de faire un copier puis d'un coup?
  • Quand devrais-je le faire moi-même?

97voto

Andy Prowl Points 62121

Avant je réponds à vos questions, une chose que vous semblez être de se tromper: en prenant par valeur en C++11 ne signifie pas toujours la copie. Si une rvalue est passé, qui sera déplacé (à condition viable constructeur de déplacement existe) plutôt que d'être copié. Et std::string ne disposent d'un constructeur de déplacement.

Contrairement au C++03, en C++11, il est souvent idiomatiques de prendre de paramètres par valeur, pour les raisons que je vais expliquer ci-dessous. Voir aussi ce Q&A sur StackOverflow pour un ensemble plus grand de lignes directrices sur la manière d'accepter les paramètres.

Pourquoi ne sommes-nous pas prendre une rvalue référence- str?

Parce qu'il serait impossible de passer lvalues, comme dans:

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!

Si S seulement avaient un constructeur qui accepte les rvalues, le ci-dessus ne serait pas de la compilation.

Ne sera pas une copie être très coûteux, surtout quelque chose comme std::string?

Si vous passez une rvalue, qui sera déplacé en str, et qui finira par être déplacé en data. Aucune copie ne sera effectuée. Si vous passez une lvalue, d'autre part, que lvalue sera copié en str, et a ensuite déménagé en data.

Donc, pour résumer, deux coups pour rvalues, une copie ou un déplacement pour lvalues.

Ce serait la raison pour l'auteur de décider de faire un copier puis d'un coup?

Tout d'abord, comme je l'ai mentionné ci-dessus, le premier n'est pas toujours une copie; cela dit, la réponse est: "Parce que c'est efficace (se déplace de l' std::string des objets sont bon marché) et simple".

Sous l'hypothèse que les mouvements sont bon marché (en ignorant SSO ici), ils peuvent être pratiquement ignoré lors de l'examen de l'efficacité globale de cette conception. Si nous le faisons, nous avons une copie pour lvalues (que nous aurions si nous avons accepté une lvalue référence à l' const) et pas de copies pour les rvalues (alors que nous aurions toujours une copie si nous avons accepté une lvalue référence à l' const).

Cela signifie que la prise en valeur est aussi bon que la prise en lvalue référence à l' const lorsque lvalues sont fournis, et bien mieux quand rvalues sont fournis.

P. S.: Pour expliquer le contexte, je crois que c'est la Q&A de l'OP fait référence.

51voto

Yakk Points 31636

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 moves, 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 throws 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.

13voto

Joe Points 1495

C'est probablement intentionnelle et est similaire à la copie et le swap de l'idiome. Essentiellement depuis la copie de la chaîne avant que le constructeur, le constructeur lui-même est exception sûr qu'il ne swaps (se déplace) temporaire de la chaîne str.

11voto

Philipp Claßen Points 4863

Vous ne voulez pas de répéter vous-même par l'écriture d'un constructeur pour la déplacer et l'autre pour la copie:

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}

C'est beaucoup de code réutilisable, surtout si vous avez plusieurs arguments. Votre solution évite la duplication sur le coût de la inutile de se déplacer. (L'opération de déplacement doit être très bon marché, cependant.)

La concurrence de l'idiome est d'utiliser le transfert parfait:

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}

Le modèle de la magie va choisir de déplacer ou de copier en fonction du paramètre que vous avez passé. Essentiellement, il s'élargit pour la première version, où le constructeur ont été écrits à la main. Pour plus d'informations, voir Scott Meyer post sur des références universelles.

À partir d'une dimension de la performance, le transfert parfait de la version est supérieure à votre version car elle permet d'éviter les mouvements inutiles. Cependant, on peut dire que votre version est plus facile à lire et à écrire. Le possible impact sur la performance ne devrait pas d'importance dans la plupart des cas, de toute façon, de sorte qu'il semble être une affaire de style dans la fin.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X