Que sont l'élision de copie et l'optimisation de la valeur de retour?

377

Qu'est-ce que la copie d'élision? Qu'est-ce que l'optimisation de la valeur de retour (nommée)? Qu'impliquent-ils?

Dans quelles situations peuvent-ils se produire? Quelles sont les limitations?

Luchian Grigore
la source
1
L'élision de copie est une façon de la voir; L'élision d'objet ou la fusion d'objet (ou la confusion) est une autre vue.
curiousguy
J'ai trouvé ce lien utile.
subtleseeker

Réponses:

246

introduction

Pour un aperçu technique - passez à cette réponse .

Pour les cas courants où la copie est supprimée, passez à cette réponse .

L'élision de copie est une optimisation mise en œuvre par la plupart des compilateurs pour empêcher des copies supplémentaires (potentiellement coûteuses) dans certaines situations. Il rend possible le retour en valeur ou en valeur par transfert dans la pratique (des restrictions s'appliquent).

C'est la seule forme d'optimisation qui élude (ha!) La règle du «comme si» - l' élision de copie peut être appliquée même si la copie / le déplacement de l'objet a des effets secondaires .

L'exemple suivant tiré de Wikipedia :

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}

Selon le compilateur et les paramètres, les sorties suivantes sont toutes valides :

Bonjour le monde!
Une copie a été faite.
Une copie a été faite.


Bonjour le monde!
Une copie a été faite.


Bonjour le monde!

Cela signifie également que moins d'objets peuvent être créés, vous ne pouvez donc pas non plus compter sur un nombre spécifique de destructeurs. Vous ne devriez pas avoir de logique critique à l'intérieur des constructeurs / destructeurs de copie / déplacement, car vous ne pouvez pas compter sur leur appel.

Si un appel à un constructeur de copie ou de déplacement est élidé, ce constructeur doit toujours exister et doit être accessible. Cela garantit que la suppression de copie n'autorise pas la copie d'objets qui ne sont normalement pas copiables, par exemple parce qu'ils ont un constructeur de copie / déplacement privé ou supprimé.

C ++ 17 : Depuis C ++ 17, Copy Elision est garanti quand un objet est retourné directement:

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C(); //Definitely performs copy elision
}
C g() {
    C c;
    return c; //Maybe performs copy elision
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f(); //Copy constructor isn't called
}
Luchian Grigore
la source
2
pourriez-vous expliquer quand la deuxième sortie se produit-elle et quand la troisième?
zhangxaochen
3
@zhangxaochen quand et comment le compilateur décide d'optimiser de cette façon.
Luchian Grigore
10
@zhangxaochen, 1ère sortie: la copie 1 est du retour vers un temp, et la copie 2 de temp vers obj; 2e est lorsque l'un des éléments ci-dessus est optimisé, probablement la copie reutnr est élidée; les trois sont éliminés
vainqueur
2
Hmm, mais à mon avis, cela DOIT être une fonctionnalité sur laquelle nous pouvons compter. Parce que si nous ne le pouvons pas, cela affecterait gravement la façon dont nous implémentons nos fonctions en C ++ moderne (RVO vs std :: move). En regardant certaines des vidéos de CppCon 2014, j'ai vraiment eu l'impression que tous les compilateurs modernes font toujours RVO. De plus, j'ai lu quelque part que sans aucune optimisation, les compilateurs l'appliquent. Mais, bien sûr, je n'en suis pas sûr. Voilà pourquoi je demande.
j00hi
8
@ j00hi: N'écrivez jamais move dans une déclaration de retour - si rvo n'est pas appliqué, la valeur de retour est quand même retirée par défaut.
MikeMB
96

Référence standard

Pour une vue et une introduction moins techniques - passez à cette réponse .

Pour les cas courants où la copie est supprimée, passez à cette réponse .

L'élision de copie est définie dans la norme dans:

12.8 Copie et déplacement d'objets de classe [class.copy]

comme

31) Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction copier / déplacer d'un objet de classe, même si le constructeur et / ou le destructeur copie / déplacement de l'objet ont des effets secondaires. Dans de tels cas, l'implémentation traite la source et la cible de l'opération de copie / déplacement omise comme simplement deux façons différentes de se référer au même objet, et la destruction de cet objet se produit à la dernière des périodes où les deux objets auraient été détruit sans l'optimisation. 123 Cette élision des opérations de copie / déplacement, appelée élision de copie , est autorisée dans les circonstances suivantes (qui peuvent être combinées pour éliminer plusieurs copies):

- dans une instruction return dans une fonction avec un type de retour de classe, lorsque l'expression est le nom d'un objet automatique non volatile (autre qu'une fonction ou un paramètre catch-clause) avec le même type cvunqualified que le type de retour de fonction, le l'opération de copie / déplacement peut être omise en construisant l'objet automatique directement dans la valeur de retour de la fonction

- dans une expression de jet, lorsque l'opérande est le nom d'un objet automatique non volatile (autre qu'une fonction ou un paramètre catch-clause) dont la portée ne s'étend pas au-delà de la fin du bloc d'essai englobant le plus interne (s'il existe un), l'opération de copie / déplacement de l'opérande vers l'objet d'exception (15.1) peut être omise en construisant l'objet automatique directement dans l'objet d'exception

- lorsqu'un objet de classe temporaire qui n'a pas été lié à une référence (12.2) serait copié / déplacé vers un objet de classe avec le même type non qualifié cv, l'opération de copie / déplacement peut être omise en construisant l'objet temporaire directement dans le cible de la copie / du déplacement omis

- lorsque la déclaration d'exception d'un gestionnaire d'exceptions (article 15) déclare un objet du même type (sauf pour cv-qualification) que l'objet d'exception (15.1), l'opération de copie / déplacement peut être omise en traitant la déclaration d'exception comme alias pour l'objet d'exception si la signification du programme reste inchangée, sauf pour l'exécution des constructeurs et destructeurs de l'objet déclaré par la déclaration d'exception.

123) Parce qu'un seul objet est détruit au lieu de deux, et qu'un constructeur copier / déplacer n'est pas exécuté, il y a toujours un objet détruit pour chaque construit.

L'exemple donné est:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

et a expliqué:

Ici, les critères d'élision peuvent être combinés pour éliminer deux appels au constructeur de copie de classe Thing: la copie de l'objet automatique local tdans l'objet temporaire pour la valeur de retour de la fonction f() et la copie de cet objet temporaire dans l'objet t2. En effet, la construction de l'objet local t peut être considérée comme initialisant directement l'objet global t2, et la destruction de cet objet se produira à la sortie du programme. L'ajout d'un constructeur de mouvement à Thing a le même effet, mais c'est la construction de déplacement de l'objet temporaire à t2celle qui est élidée.

Luchian Grigore
la source
1
Est-ce de la norme C ++ 17 ou d'une version antérieure?
Nils
90

Formes courantes d'élision de copie

Pour un aperçu technique - passez à cette réponse .

Pour une vue et une introduction moins techniques - passez à cette réponse .

(Nommé) L'optimisation de la valeur de retour est une forme courante d'élision de copie. Il fait référence à la situation où un objet renvoyé par la valeur d'une méthode a sa copie élidée. L'exemple présenté dans la norme illustre l' optimisation de la valeur de retour nommée , puisque l'objet est nommé.

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

Une optimisation régulière de la valeur de retour se produit lorsqu'un temporaire est renvoyé:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

D'autres endroits communs où la suppression de la copie a lieu sont lorsqu'un temporaire est passé par valeur :

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

foo(Thing());

ou lorsqu'une exception est levée et interceptée par une valeur :

struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

Les limitations courantes de l'élision de copie sont:

  • plusieurs points de retour
  • initialisation conditionnelle

La plupart des compilateurs de qualité commerciale prennent en charge la suppression de copie et (N) RVO (en fonction des paramètres d'optimisation).

Luchian Grigore
la source
4
Je serais intéressé de voir les puces "Limitations communes" expliquées juste un peu ... qu'est-ce qui rend ces facteurs limitants?
phonetagger
@phonetagger J'ai lié contre l'article msdn, j'espère que cela efface certaines choses.
Luchian Grigore
54

L'élision de copie est une technique d'optimisation du compilateur qui élimine la copie / déplacement inutile d'objets.

Dans les circonstances suivantes, un compilateur est autorisé à omettre les opérations de copie / déplacement et donc à ne pas appeler le constructeur associé:

  1. NRVO (Named Return Value Optimization) : si une fonction renvoie un type de classe par valeur et que l'expression de l'instruction return est le nom d'un objet non volatile avec une durée de stockage automatique (qui n'est pas un paramètre de fonction), alors la copie / déplacement qui serait effectuée par un compilateur non optimisant peut être omis. Si tel est le cas, la valeur renvoyée est construite directement dans le stockage vers lequel la valeur de retour de la fonction serait autrement déplacée ou copiée.
  2. RVO (Return Value Optimization) : si la fonction renvoie un objet temporaire sans nom qui serait déplacé ou copié dans la destination par un compilateur naïf, la copie ou le déplacement peut être omis selon 1.
#include <iostream>  
using namespace std;

class ABC  
{  
public:   
    const char *a;  
    ABC()  
     { cout<<"Constructor"<<endl; }  
    ABC(const char *ptr)  
     { cout<<"Constructor"<<endl; }  
    ABC(ABC  &obj)  
     { cout<<"copy constructor"<<endl;}  
    ABC(ABC&& obj)  
    { cout<<"Move constructor"<<endl; }  
    ~ABC()  
    { cout<<"Destructor"<<endl; }  
};

ABC fun123()  
{ ABC obj; return obj; }  

ABC xyz123()  
{  return ABC(); }  

int main()  
{  
    ABC abc;  
    ABC obj1(fun123());//NRVO  
    ABC obj2(xyz123());//NRVO  
    ABC xyz = "Stack Overflow";//RVO  
    return 0;  
}

**Output without -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor    
Constructor  
Constructor  
Constructor  
Destructor  
Destructor  
Destructor  
Destructor  

**Output with -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Destructor  
Destructor  
Destructor  
Destructor  

Même lorsque la suppression de copie a lieu et que le constructeur de copie / déplacement n'est pas appelé, il doit être présent et accessible (comme si aucune optimisation ne s'est produite), sinon le programme est mal formé.

Vous ne devez autoriser cette élision de copie que dans des endroits où elle n'affectera pas le comportement observable de votre logiciel. L'élision de copie est la seule forme d'optimisation autorisée à avoir (c'est-à-dire élide) des effets secondaires observables. Exemple:

#include <iostream>     
int n = 0;    
class ABC     
{  public:  
 ABC(int) {}    
 ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect    
};                     // it modifies an object with static storage duration    

int main()   
{  
  ABC c1(21); // direct-initialization, calls C::C(42)  
  ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) )  

  std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise
  return 0;  
}

Output without -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp  
root@ajay-PC:/home/ayadav# ./a.out   
0

Output with -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors  
root@ajay-PC:/home/ayadav# ./a.out   
1

GCC offre la -fno-elide-constructorspossibilité de désactiver l'élision de copie. Si vous souhaitez éviter une élision de copie possible, utilisez -fno-elide-constructors.

Désormais, presque tous les compilateurs fournissent une élision de copie lorsque l'optimisation est activée (et si aucune autre option n'est définie pour la désactiver).

Conclusion

Avec chaque élision de copie, une construction et une destruction correspondante de la copie sont omises, économisant ainsi du temps CPU, et un objet n'est pas créé, économisant ainsi de l'espace sur le cadre de pile.

Ajay yadav
la source
6
la déclaration ABC obj2(xyz123());est-elle NRVO ou RVO? est-ce qu'il n'obtient pas la variable / objet temporaire comme ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq
3
Pour avoir une illustration plus concrète de RVO, vous pouvez vous référer à l'assembly que le compilateur génère (changez l'indicateur du compilateur -fno-elide-constructors pour voir le diff). godbolt.org/g/Y2KcdH
Gab 是 好人