Pourquoi est-ce que nous copions puis bougeons?

J’ai vu du code quelque part dans lequel quelqu’un a décidé de copier un object et de le déplacer ensuite vers un membre de données d’une classe. Cela m’a laissé dans la confusion en ce sens que je pensais que le but du déménagement était d’éviter de copier. Voici l’exemple:

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

Voici mes questions:

  • Pourquoi ne prenons-nous pas une référence de valeur à str ?
  • Une copie ne sera-t-elle pas chère, surtout si on std::ssortingng quelque chose comme std::ssortingng ?
  • Quelle serait la raison pour que l’auteur décide de faire une copie puis un déménagement?
  • Quand devrais-je le faire moi-même?

Avant de répondre à vos questions, une chose que vous semblez vous tromper: prendre de la valeur en C ++ 11 ne signifie pas toujours copier. Si une valeur est transmise, elle sera déplacée (à condition qu’un constructeur de déplacement viable existe) plutôt que d’être copiée. Et std::ssortingng a un constructeur de déplacement.

Contrairement à C ++ 03, en C ++ 11, il est souvent idiomatique de prendre des parameters par valeur, pour les raisons que je vais expliquer ci-dessous. Voir également cette Q & R sur StackOverflow pour un ensemble plus général de directives sur la façon d’accepter les parameters.

Pourquoi ne prenons-nous pas une référence de valeur à str ?

Parce que cela rendrait impossible le passage des lvalues, comme dans:

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

Si S n’a qu’un constructeur qui accepte les valeurs, ce qui précède ne comstack pas.

Une copie ne sera-t-elle pas chère, surtout si on std::ssortingng quelque chose comme std::ssortingng ?

Si vous passez une rvalue, celle-ci sera déplacée dans str et sera éventuellement déplacée dans les data . Aucune copie ne sera effectuée. Si vous passez une lvalue, par contre, cette lvalue sera copiée dans str , puis déplacée dans les data .

Donc, pour résumer, deux mouvements pour les valeurs, une copie et un mouvement pour les lvalues.

Quelle serait la raison pour que l’auteur décide de faire une copie puis un déménagement?

Tout d’abord, comme je l’ai mentionné ci-dessus, le premier n’est pas toujours une copie; et ceci dit, la réponse est: ” Parce que c’est efficace (les déplacements des objects std::ssortingng sont bon marché) et simples “.

En supposant que les mouvements sont bon marché (en ignorant le SSO ici), ils peuvent être pratiquement ignorés lorsque l’on considère l’efficacité globale de cette conception. Si nous le faisons, nous avons une copie pour lvalues ​​(comme si nous avions accepté une référence lvalue à const ) et aucune copie pour les rvalues ​​(alors que nous aurions toujours une copie si nous acceptions une référence lvalue à const ).

Cela signifie que prendre en valeur vaut aussi bien prendre lvalue référence à const lorsque des valeurs sont fournies, et mieux lorsque des valeurs sont fournies.

PS: Pour donner un peu de contexte, je pense que c’est le Q & A auquel le PO fait référence.

Pour comprendre pourquoi il s’agit d’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::ssortingng const& :

 struct S { std::ssortingng data; S(std::ssortingng const& str) : data(str) {} }; 

dans ce cas, une seule copie sera toujours effectuée. Si vous construisez à partir d’une chaîne C brute, une chaîne std::ssortingng sera construite, puis recopiée: deux allocations.

Il existe la méthode C ++ 03 qui consiste à prendre une référence à une std::ssortingng , puis à la remplacer par une chaîne std::ssortingng locale:

 struct S { std::ssortingng data; S(std::ssortingng& str) { std::swap(data, str); } }; 

c’est la version C ++ 03 de la “sémantique des mouvements”, et le swap peut souvent être optimisé pour être très économique (un peu comme un move ). Il devrait également être analysé dans son contexte:

 S tmp("foo"); // illegal std::ssortingng s("foo"); S tmp2(s); // legal 

et vous oblige à former un std::ssortingng non temporaire, puis le supprimer. (Une std::ssortingng temporaire ne peut pas être liée à une référence non-const). Une seule allocation est faite, cependant. La version C ++ 11 prendrait un && et vous obligerait à 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::ssortingng data; S(std::ssortingng&& str): data(std::move(str)) {} }; 

Utilisation:

 S tmp("foo"); // legal std::ssortingng s("foo"); S tmp2(std::move(s)); // legal 

Ensuite, nous pouvons faire la version complète de C ++ 11, qui supporte à la fois la copie et le move :

 struct S { std::ssortingng data; S(std::ssortingng const& str) : data(str) {} // lvalue const, copy S(std::ssortingng && str) : data(std::move(str)) {} // rvalue, move }; 

Nous pouvons alors examiner comment cela est utilisé:

 S tmp( "foo" ); // a temporary `std::ssortingng` is created, then moved into tmp.data std::ssortingng bar("bar"); // bar is created S tmp2( bar ); // bar is copied into tmp.data std::ssortingng 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”.

Nous allons maintenant examiner la version à la copie:

 struct S2 { std::ssortingng data; S2( std::ssortingng arg ):data(std::move(x)) {} }; 

dans chacun de ces scénarios:

 S2 tmp( "foo" ); // a temporary `std::ssortingng` is created, moved into arg, then moved into S2::data std::ssortingng bar("bar"); // bar is created S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data std::ssortingng 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 effectuons exactement un move supplémentaire! Pas une seule fois nous faisons une copy supplémentaire.

Donc, si nous supposons que le move est bon marché, cette version nous procure presque les mêmes performances que la version la plus optimale, mais 2 fois moins de code.

Et si vous prenez 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 une transmission parfaite et SFINAE, ce qui vous permet d’écrire un seul constructeur ou modèle de fonction qui prend 10 arguments, SFINAE pour vous assurer que les arguments sont de types appropriés, puis les déplace ou les copie dans le état local si nécessaire. Bien que cela évite la multiplication par mille du problème de la taille du programme, il peut toujours y avoir toute une stack de fonctions générées à partir de ce modèle. (les instanciations de la fonction template génèrent des fonctions)

Et beaucoup de fonctions générées signifient une plus grande taille de code exécutable, ce qui peut réduire les performances.

Pour un coût de quelques move , nous obtenons un code plus court et des performances quasiment identiques, et souvent plus faciles à comprendre.

Maintenant, cela ne fonctionne que parce que nous soaps, lorsque la fonction (dans ce cas, un constructeur) est appelée, que nous voudrons une copie locale de cet argument. L’idée est que si nous soaps que nous allons faire une copie, nous devrions laisser l’appelant savoir que nous faisons une copie en la mettant dans notre liste d’arguments. Ils peuvent alors optimiser le fait qu’ils vont nous en donner une copie (en passant à notre argument, par exemple).

Un autre avantage de la technique «prendre par la valeur» est que souvent les constructeurs mobiles ne sont pas concernés, ce qui signifie que les fonctions qui prennent de la valeur et sortent de leur argument peuvent souvent être différentes, déplaçant tout throw de leur corps scope (qui peut l’éviter via la construction directe parfois, ou construire les éléments et move à l’argumentation, pour contrôler où se produit le lancer).

Ceci est probablement intentionnel et est similaire à l’ expression de copie et d’échange . Fondamentalement, puisque la chaîne est copiée avant le constructeur, le constructeur lui-même est protégé contre les exceptions car il échange uniquement la chaîne temporaire str.

Vous ne voulez pas vous répéter en écrivant un constructeur pour le coup et un autre pour la copie:

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

C’est beaucoup de code passe-partout, surtout si vous avez plusieurs arguments. Votre solution évite cette duplication du coût d’un déménagement inutile. (L’opération de déplacement devrait être assez bon marché, cependant.)

L’idiome concurrent est d’utiliser la transmission parfaite:

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

Le modèle magique choisira de se déplacer ou de copier en fonction du paramètre que vous transmettez. Il étend essentiellement à la première version, où les deux constructeurs ont été écrits à la main. Pour plus d’informations, voir l’article de Scott Meyer sur les références universelles .

Du sharepoint vue des performances, la version de transfert parfaite est supérieure à votre version car elle évite les déplacements inutiles. Cependant, on peut soutenir que votre version est plus facile à lire et à écrire. L’impact possible sur les performances ne devrait pas avoir d’importance dans la plupart des situations, de toute façon, il semble donc que ce soit une question de style à la fin.