Que sont l'élision de copie et l'optimisation de la valeur de retour?
Qu'est-ce que l'élision de copie? Qu'est-ce que l'optimisation de la valeur de retour (nommée)? Que font-ils impliquent?
Dans quelles situations peuvent-ils se produire? Quelles sont les limites?
- Si vous avez été référencé à cette question, vous cherchez probablement l'introduction.
- Pour un aperçu, voir la norme de référence.
- Voir des cas fréquents ici.
4 réponses
Introduction
Pour un aperçu technique - passez à cette réponse .
Pour les cas courants où l'élision de copie se produit - passez à cette réponse .
Copy elision est une optimisation implémentée par la plupart des compilateurs pour éviter des copies supplémentaires (potentiellement coûteuses) dans certaines situations. Il fait le retour par valeur ou par la valeur réalisable dans la pratique (des restrictions s'appliquent).
C'est la seule forme d'optimisation qui élide (ha!) le cas de la règle - copie élision peut être appliquée même si la copie/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();
}
En fonction du compilateur et des paramètres, les sorties suivantes sont toutes valides:
Bonjour Tout Le Monde!
Une copie a été faite.
Une copie a été faite.
Bonjour Tout Le Monde!
Une copie a été faite.
Bonjour Monde!
Cela signifie également que moins d'objets peuvent être créés, de sorte que vous ne pouvez pas compter sur un nombre spécifique de destructeurs appelés. Vous ne devriez pas avoir de logique critique dans copy / move-constructors ou destructors, car vous ne pouvez pas compter sur leur appel.
Si un appel à un constructeur copy ou move est elided, ce constructeur doit toujours exister et doit être accessible. Cela garantit que l'élision de copie n'autorise pas la copie d'objets qui ne sont normalement pas copiables, par exemple parce qu'ils avoir un constructeur de copie/déplacement privé ou supprimé.
C++17 : à partir de C++17, L'élision de copie est garantie lorsqu'un objet est renvoyé 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
}
Référence Standard
Pour une vue et une introduction moins techniques - passez à cette réponse .
Pour les cas courants où l'élision de copie se produit - passez à cette réponse .
L'élision de copie est définie dans la norme dans:
12.8 copier et déplacer des objets de classe [class.copie]
Comme
31) lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction copier/déplacer d'une classe objet, même si le copier/déplacer constructeur et/ou le destructeur 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 différentes les moyens de se référer au même objet, et la destruction de cet objet se produit au plus tard des temps lorsque les deux objets auraient été détruits sans l'optimisation.123 Cette élision de copier/déplacer opérations, appelées copie élision, est autorisé dans les circonstances suivantes (qui peuvent être combinées pour supprimer plusieurs copies):
- dans une instruction return dans une fonction avec une classe de type de retour, lorsque l'expression est le nom d'un objet automatique non volatile (autre qu'un paramètre de fonction ou de clause de capture) avec le même cvunqualified type en tant que type de retour de fonction, l'opération copier/déplacer peut être omise en construisant l'objet automatique directement dans la valeur de retour de la fonction
- dans un throw-expression, lorsque l'opérande est le nom d'un objet automatique non volatile (autre qu'un fonction ou paramètre catch-clause) dont la portée ne s'étend pas au-delà de la fin de la entourant try-block (s'il y en a un), l'opération copier/déplacer de l'opérande à l'exception l'objet (15.1) peut être omis 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) être copiés/déplacés pour un objet de classe avec le même type cv-unqualified, l'opération copier/déplacer peut être omise par construire l'objet temporaire directement dans la cible de la copie/déplacement omis
- lorsque la déclaration d'exception d'un gestionnaire d'exception (Clause 15) déclare un objet du même type (sauf pour CV-qualification) en tant qu'objet d'exception (15.1), l'opération copier/déplacer peut être omise en traitant la déclaration d'exception comme un alias pour l'exception objet si la signification du programme sera inchangé, sauf pour l'exécution de constructeurs et destructeurs pour l'objet déclaré par l'exception-déclaration.
123) parce qu'un seul objet est détruit au lieu de deux, et qu'un constructeur de copie/déplacement n'est pas exécuté, il y en a toujours un objet détruit pour chacun construit.
L'exemple donné est:
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t;
}
Thing t2 = f();
Et expliqué:
Voici les critères d'élision peut être combiné pour éliminer deux appels au constructeur de copie de la classe
Thing
: la copie de l'objet automatique localt
dans l'objet temporaire pour la valeur de retour de la fonctionf()
et la copie de cet objet temporaire dans objectt2
. Effectivement, la construction de l'objet localt
peut être considéré comme initialisant directement l'objet globalt2
, et la destruction de cet objet se produira au programme sortie. L'ajout d'un constructeur move à Thing a le même effet, mais c'est la construction de mouvement de la objet temporaire àt2
qui est elided.
Formes communes 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 retourné par la valeur d'une méthode a sa copie elided. L'exemple présenté dans la norme illustre nommé optimisation de la valeur de retour , puisque l'objet est nommé.
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t;
}
Thing t2 = f();
Normal l'optimisation 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();
Les autres lieux communs où l'élision de copie a lieu sont quand un temporaire est passé par la 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 la valeur :
struct Thing{
Thing();
Thing(const Thing&);
};
void foo() {
Thing c;
throw c;
}
int main() {
try {
foo();
}
catch(Thing c) {
}
}
Les limites communes de l'élision de copie sont:
- points de retour multiples
- initialisation conditionnelle
La plupart de qualité commerciale les compilateurs prennent en charge copy elision & (N)RVO (en fonction des paramètres d'optimisation).
Copy elision 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é:
- NRVO (optimisation de la valeur de retour nommée) : 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 paramètre de fonction), alors la copie / déplacement qui serait effectuée par un compilateur non optimisant peut être omise. Si c'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.
- 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 l'élision de copie a lieu et que le constructeur copy - / move-n'est pas appelé, il doit être présent et accessible (comme si aucune optimisation ne s'était produite), sinon le programme est mal formé.
Vous devez autoriser une telle élision de copie uniquement dans les 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 elide) 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 fournit le -fno-elide-constructors
option pour désactiver l'élision de copie.
Si vous voulez éviter une éventuelle élision de copie, utilisez -fno-elide-constructors
.
Maintenant, 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 la pile.