Surcharge sur les références de valeur R et duplication de code

Considérer ce qui suit:

struct vec { int v[3]; vec() : v() {}; vec(int x, int y, int z) : v{x,y,z} {}; vec(const vec& that) = default; vec& operator=(const vec& that) = default; ~vec() = default; vec& operator+=(const vec& that) { v[0] += that.v[0]; v[1] += that.v[1]; v[2] += that.v[2]; return *this; } }; vec operator+(const vec& lhs, const vec& rhs) { return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]); } vec&& operator+(vec&& lhs, const vec& rhs) { return move(lhs += rhs); } vec&& operator+(const vec& lhs, vec&& rhs) { return move(rhs += lhs); } vec&& operator+(vec&& lhs, vec&& rhs) { return move(lhs += rhs); } 

Grâce aux références de valeur r, ces quatre surcharges d’opérateur + I permettent de minimiser le nombre d’objects créés, en réutilisant des temporaires. Mais je n’aime pas la duplication de code introduite. Puis-je obtenir le même résultat avec moins de répétition?

Le recyclage des temporaires est une idée intéressante et vous n’êtes pas le seul à avoir écrit des fonctions qui renvoient des références rvalue pour cette raison. Dans un ancien projet C ++ 0x, l’opérateur + (chaîne &&, chaîne const &) était également déclaré comme renvoyant une référence rvalue. Mais cela a changé pour de bonnes raisons. Je vois trois problèmes avec ce type de surcharge et le choix des types de retour. Deux d’entre eux sont indépendants du type réel et le troisième argument fait référence au type de type de vec .

  1. Des problèmes de sécurité. Considérons le code comme ceci:

     vec a = ....; vec b = ....; vec c = ....; auto&& x = a+b+c; 

    Si votre dernier opérateur retourne une référence rvalue, x sera une référence en suspens. Sinon, ça ne va pas. Ce n’est pas un exemple artificiel. Par exemple, l’astuce auto&& est utilisée en interne dans la boucle for-range pour éviter les copies inutiles. Mais comme la règle d’extension de durée de vie pour les temporaires pendant la liaison de référence ne s’applique pas dans le cas d’un appel de fonction qui renvoie simplement une référence, vous obtiendrez une référence suspendue.

     ssortingng source1(); ssortingng source2(); ssortingng source3(); .... int main() { for ( char x : source1()+source2()+source3() ) {} } 

    Si le dernier opérateur + a renvoyé une référence rvalue au temporaire créé lors de la première concaténation, ce code invoquerait un comportement non défini car la chaîne temporaire n’existerait pas suffisamment longtemps.

  2. Dans le code générique, les fonctions qui renvoient des références rvalue vous obligent à écrire

     typename std::decay::type 

    au lieu de

     decltype(a+b+c) 

    tout simplement parce que le dernier op + peut renvoyer une référence rvalue. Cela devient moche, à mon humble avis.

  3. Puisque votre type vec est à la fois “plat” et petit, ces surcharges op + ne sont guère utiles. Voir la réponse de FredOverflow.

Conclusion: les fonctions avec un type de retour de référence rvalue doivent être évitées, en particulier si ces références peuvent faire référence à des objects temporaires de courte durée. std::move et std::forward sont des exceptions spéciales à cette règle.

Comme votre type de vec est “plat” (il n’y a pas de données externes), le déplacement et la copie font exactement la même chose. Ainsi, toutes vos références rvalue et std::move s ne vous apportent absolument rien en performance.

Je voudrais supprimer toutes les surcharges supplémentaires et écrire simplement la version classique de référence à const:

 vec operator+(const vec& lhs, const vec& rhs) { return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]); } 

Si vous avez encore peu de compréhension de la sémantique des déplacements, je vous recommande d’étudier cette question .

Grâce aux références de valeur r, ces quatre surcharges d’opérateur + I permettent de minimiser le nombre d’objects créés, en réutilisant des temporaires.

À quelques exceptions près, renvoyer des références rvalue est une très mauvaise idée, car les appels de ces fonctions sont des valeurs xvalues ​​au lieu de prvalues, ce qui peut entraîner des problèmes de durée de vie des objects temporaires. Ne le fais pas.

Cela, qui fonctionne déjà à merveille dans le C ++ actuel, utilisera la sémantique de déplacement (si disponible) en C ++ 0x. Il gère déjà tous les cas, mais repose sur la suppression et l’alignement des copies pour éviter les copies. Il est donc possible de créer plus de copies que souhaité, en particulier pour le second paramètre. La bonne chose à ce sujet est que cela fonctionne sans aucune autre surcharge et fait la bonne chose (sémantiquement):

 vec operator+(vec a, vec const &b) { a += b; return a; // "a" is local, so this is implicitly "return std::move(a)", // if move semantics are available for the type. } 

Et c’est là que vous vous arrêteriez, 99% du temps. (Je sousestime probablement ce chiffre.) Le rest de cette réponse ne s’applique que lorsque vous savez, par exemple, grâce à l’utilisation d’un profileur, que les copies supplémentaires provenant de op + méritent d’être optimisées.


Pour éviter complètement toutes les copies / déplacements possibles, vous auriez besoin des surcharges suivantes:

 // lvalue + lvalue vec operator+(vec const &a, vec const &b) { vec x (a); x += b; return x; } // rvalue + lvalue vec&& operator+(vec &&a, vec const &b) { a += b; return std::move(a); } // lvalue + rvalue vec&& operator+(vec const &a, vec &&b) { b += a; return std::move(b); } // rvalue + rvalue, needed to disambiguate above two vec&& operator+(vec &&a, vec &&b) { a += b; return std::move(a); } 

Vous étiez sur la bonne voie avec le vôtre, sans réelle réduction possible (AFAICT), mais si vous avez souvent besoin de cet op + pour de nombreux types, une macro ou un CRTP pourrait le générer pour vous. La seule différence réelle (ma préférence pour les déclarations séparées ci-dessus est mineure) réside dans les copies que vous effectuez lorsque vous ajoutez deux valeurs lvalues ​​à l’opérateur + (const vec & lhs, vec && rhs):

 return std::move(rhs + lhs); 

Réduire les doublons grâce au CRTP

 template struct Addable { friend T operator+(T const &a, T const &b) { T x (a); x += b; return x; } friend T&& operator+(T &&a, T const &b) { a += b; return std::move(a); } friend T&& operator+(T const &a, T &&b) { b += a; return std::move(b); } friend T&& operator+(T &&a, T &&b) { a += b; return std::move(a); } }; struct vec : Addable { //... vec& operator+=(vec const &x); }; 

Désormais, il n’est plus nécessaire de définir d’op + spécifiquement pour vec. Addable est réutilisable pour tout type avec op + =.

J’ai codé la réponse de Fred Nurk en utilisant clang + libc ++ . J’ai dû supprimer l’utilisation de la syntaxe d’initialisation car clang ne l’implémentait pas encore. Je mets également une déclaration print dans le constructeur de copie afin que nous puissions compter les copies.

 #include  template struct AddPlus { friend T operator+(T a, T const &b) { a += b; return a; } friend T&& operator+(T &&a, T const &b) { a += b; return std::move(a); } friend T&& operator+(T const &a, T &&b) { b += a; return std::move(b); } friend T&& operator+(T &&a, T &&b) { a += b; return std::move(a); } }; struct vec : public AddPlus { int v[3]; vec() : v() {}; vec(int x, int y, int z) { v[0] = x; v[1] = y; v[2] = z; }; vec(const vec& that) { std::cout << "Copying\n"; v[0] = that.v[0]; v[1] = that.v[1]; v[2] = that.v[2]; } vec& operator=(const vec& that) = default; ~vec() = default; vec& operator+=(const vec& that) { v[0] += that.v[0]; v[1] += that.v[1]; v[2] += that.v[2]; return *this; } }; int main() { vec v1(1, 2, 3), v2(1, 2, 3), v3(1, 2, 3), v4(1, 2, 3); vec v5 = v1 + v2 + v3 + v4; } test.cpp:66:22: error: use of overloaded operator '+' is ambiguous (with operand types 'vec' and 'vec') vec v5 = v1 + v2 + v3 + v4; ~~~~~~~ ^ ~~ test.cpp:5:12: note: candidate function friend T operator+(T a, T const &b) { ^ test.cpp:10:14: note: candidate function friend T&& operator+(T &&a, T const &b) { ^ 1 error generated. 

J'ai corrigé cette erreur comme ceci:

 template struct AddPlus { friend T operator+(const T& a, T const &b) { T x(a); x += b; return x; } friend T&& operator+(T &&a, T const &b) { a += b; return std::move(a); } friend T&& operator+(T const &a, T &&b) { b += a; return std::move(b); } friend T&& operator+(T &&a, T &&b) { a += b; return std::move(a); } }; 

Lancer les exemples de sorties:

 Copying Copying 

Ensuite, j'ai essayé une approche C ++ 03:

 #include  struct vec { int v[3]; vec() : v() {}; vec(int x, int y, int z) { v[0] = x; v[1] = y; v[2] = z; }; vec(const vec& that) { std::cout << "Copying\n"; v[0] = that.v[0]; v[1] = that.v[1]; v[2] = that.v[2]; } vec& operator=(const vec& that) = default; ~vec() = default; vec& operator+=(const vec& that) { v[0] += that.v[0]; v[1] += that.v[1]; v[2] += that.v[2]; return *this; } }; vec operator+(const vec& lhs, const vec& rhs) { return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]); } int main() { vec v1(1, 2, 3), v2(1, 2, 3), v3(1, 2, 3), v4(1, 2, 3); vec v5 = v1 + v2 + v3 + v4; } 

L'exécution de ce programme n'a produit aucune sortie.

Ce sont les résultats que j'ai obtenus avec clang ++ . Interprétez-les comme vous le pouvez. Et votre kilométrage peut varier.