web-dev-qa-db-fra.com

Est-ce que le transfert par valeur et ensuite par construction est un mauvais idiome?

Puisque nous avons la sémantique de déplacement en C++, il est de nos jours habituel de le faire

void set_a(A a) { _a = std::move(a); }

Le raisonnement est que si a est une valeur, la copie sera supprimée et il n’y aura qu’un seul mouvement. 

Mais que se passe-t-il si a est une lvalue? Il semble qu’il y aura une construction de copie puis une affectation de déplacement (en supposant que A dispose d’un opérateur d’affectation de déplacement approprié). Les affectations de déplacement peuvent être coûteuses si l'objet a trop de variables de membre. 

D'autre part, si nous faisons

void set_a(const A& a) { _a = a; }

Il n’y aura qu’une copie. Pouvons-nous dire que cette méthode est préférable à l'idiome de valeur par valeur si nous voulons transmettre des valeurs?

49
jbgs

Les types coûteux à déplacer sont rares dans l'utilisation de C++ moderne. Si vous êtes préoccupé par le coût du déménagement, écrivez les deux surcharges:

void set_a(const A& a) { _a = a; }
void set_a(A&& a) { _a = std::move(a); }

ou un passeur de transfert parfait:

template <typename T>
void set_a(T&& a) { _a = std::forward<T>(a); }

qui acceptera les lvalues, les valeurs et tout autre élément implicitement convertible en decltype(_a) sans nécessiter de copies ou de déplacements supplémentaires.

Bien qu’il faille exiger un déplacement supplémentaire lors du réglage d’une valeur, l’idiome n’est pas mauvais puisque (a) la grande majorité des types fournissent des déplacements à temps constant et la performance dans une seule ligne de code.

38
Casey

Mais que se passe-t-il si a est une lvalue? Il semble qu'il y aura une construction de copie, puis une affectation de déplacement (en supposant que A dispose d'un opérateur d'affectation de déplacement approprié). Les affectations de déplacement peuvent être coûteuses si l'objet a trop de variables de membre.

Problème bien identifié. Je n'irais pas jusqu'à dire que la construction passe-par-valeur-et-alors-mouvement est un mauvais idiome mais il a certainement ses pièges potentiels.

Si votre type coûte cher à déplacer et/ou que vous le déplacez est essentiellement une copie, l'approche par valeur résiduelle est sous-optimale. Les exemples de tels types incluent les types avec un tableau de taille fixe en tant que membre: le déplacement peut être relativement coûteux et un déplacement n'est qu'une copie. Voir également

dans ce contexte.

L’approche de la valeur au passage présente l’avantage de ne conserver qu’une fonction, mais de payer pour cela en termes de performances. Cela dépend de votre application si cet avantage en termes de maintenance est supérieur à la perte de performance.

L'approche de référence par lvalue et rvalue peut rapidement entraîner des problèmes de maintenance si vous avez plusieurs arguments. Considérez ceci:

#include <vector>
using namespace std;

struct A { vector<int> v; };
struct B { vector<int> v; };

struct C {
  A a;
  B b;
  C(const A&  a, const B&  b) : a(a), b(b) { }
  C(const A&  a,       B&& b) : a(a), b(move(b)) { }
  C(      A&& a, const B&  b) : a(move(a)), b(b) { }
  C(      A&& a,       B&& b) : a(move(a)), b(move(b)) { }  
};

Si vous avez plusieurs arguments, vous aurez un problème de permutation. Dans cet exemple très simple, il n’est probablement pas si mal de maintenir ces 4 constructeurs. Cependant, déjà dans ce cas simple, j’envisagerais sérieusement d’utiliser l’approche de la valeur par valeur avec une seule fonction

C(A a, B b) : a(move(a)), b(move(b)) { }

au lieu des 4 constructeurs ci-dessus.

Si longue histoire, aucune approche n’est sans inconvénients. Prenez vos décisions en fonction des informations de profilage réelles, au lieu d’optimiser prématurément.

23
Ali

Pour le cas général où la valeur sera stockée , seul le passage par la valeur est un bon compromis-

Pour le cas où vous savez que seules les valeurs seront passées (certains codes étroitement couplés), il est déraisonnable, malin.

Pour le cas où l’on soupçonne une amélioration de la vitesse en fournissant les deux, d’abord, PENSEZ DEUX FOIS, et si cela n’aide en rien, MEASURE.

Lorsque la valeur ne sera pas stockée, je préfère le passage par référence, car cela empêche de nombreuses opérations de copie inutiles.

Enfin, si la programmation pouvait être réduite à une application irréfléchie des règles, nous pourrions en laisser le soin aux robots. Donc, à mon humble avis, ce n’est pas une bonne idée de se concentrer autant sur les règles. Mieux vaut se concentrer sur les avantages et les coûts, dans différentes situations. Les coûts comprennent non seulement la vitesse, mais aussi, par exemple, la taille du code et la clarté. Les règles ne peuvent généralement pas traiter de tels conflits d'intérêts.

7

Passer par valeur, puis se déplacer est en réalité un bon idiome pour les objets que vous savez mobiles.

Comme vous l'avez mentionné, si une valeur est passée, elle supprimera la copie ou sera déplacée, puis dans le constructeur, elle sera déplacée.

Vous pouvez surcharger le constructeur de copie et le déplacer de manière explicite, mais cela devient plus compliqué si vous avez plusieurs paramètres.

Considérez l'exemple,

class Obj {
  public:

  Obj(std::vector<int> x, std::vector<int> y)
      : X(std::move(x)), Y(std::move(y)) {}

  private:

  /* Our internal data. */
  std::vector<int> X, Y;

};  // Obj

Supposons que si vous vouliez fournir des versions explicites, vous vous retrouviez avec 4 constructeurs comme ceci:

class Obj {
  public:

  Obj(std::vector<int> &&x, std::vector<int> &&y)
      : X(std::move(x)), Y(std::move(y)) {}

  Obj(std::vector<int> &&x, const std::vector<int> &y)
      : X(std::move(x)), Y(y) {}

  Obj(const std::vector<int> &x, std::vector<int> &&y)
      : X(x), Y(std::move(y)) {}

  Obj(const std::vector<int> &x, const std::vector<int> &y)
      : X(x), Y(y) {}

  private:

  /* Our internal data. */
  std::vector<int> X, Y;

};  // Obj

Comme vous pouvez le constater, à mesure que vous augmentez le nombre de paramètres, le nombre de constructeurs nécessaires augmente en permutations.

Si vous n'avez pas de type concret mais un constructeur basé sur un modèle, vous pouvez utiliser une transmission parfaite comme ceci:

class Obj {
  public:

  template <typename T, typename U>
  Obj(T &&x, U &&y)
      : X(std::forward<T>(x)), Y(std::forward<U>(y)) {}

  private:

  std::vector<int> X, Y;

};   // Obj

Références:

  1. Tu veux de la vitesse? Pass par valeur
  2. Assaisonnement C++
1
mpark

Je me réponds parce que je vais essayer de résumer certaines des réponses. Combien de coups/copies avons-nous dans chaque cas?

(A) Passer par valeur et déplacer la construction d'affectation, en passant un paramètre X. Si X est un ...

Temporaire: 1 coup (la copie est élidée)

Lvalue: 1 copie 1 coup

std :: move (lvalue): 2 coups

(B) Passer par référence et copier l'assemblage habituel (avant C++ 11). Si X est un ...

Temporaire: 1 copie

Lvalue: 1 copie

std :: move (lvalue): 1 copie

Nous pouvons supposer que les trois types de paramètres sont également probables. Ainsi, tous les 3 appels, nous avons (A) 4 coups et 1 copie, ou (B) 3 copies. C'est-à-dire que, en moyenne, (A) 1,33 déplace et 0,33 copie par appel ou (B) 1 copie par appel.

Si nous arrivons à une situation où nos classes sont principalement composées de POD, les déménagements sont aussi coûteux que des copies. Nous aurions donc 1,66 copies (ou déplacements) par appel vers le passeur dans le cas (A) et 1 copies dans le cas (B).

Nous pouvons dire que dans certaines circonstances (types basés sur les POD), la construction passe-par-valeur-puis-mouvement est une très mauvaise idée. Il est 66% plus lent et cela dépend d'une fonctionnalité C++ 11. 

Par contre, si nos classes incluent des conteneurs (qui utilisent la mémoire dynamique), (A) devrait être beaucoup plus rapide (sauf si nous passons surtout des lvalues). 

Corrigez-moi si j'ai tort, s'il-vous plait.

1
jbgs

Lisibilité dans la déclaration:

void foo1( A a ); // easy to read, but unless you see the implementation 
                  // you don't know for sure if a std::move() is used.

void foo2( const A & a ); // longer declaration, but the interface shows
                          // that no copy is required on calling foo().

Performance:

A a;
foo1( a );  // copy + move
foo2( a );  // pass by reference + copy

Responsabilités:

A a;
foo1( a );  // caller copies, foo1 moves
foo2( a );  // foo2 copies

Pour un code en ligne typique, il n'y a généralement aucune différence avec l'optimisation . Mais foo2 () peut ne copier que dans certaines conditions (par exemple, insérer dans la carte si la clé n'existe pas), alors que pour foo1 (), la copie sera toujours effectuée. .

0
Joachim