Qu'est-ce que la copie é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?
Pour un aperçu technique - passez à cette réponse .
Pour les cas courants où la copie se produit - passez à cette réponse .
La suppression de copies est une optimisation mise en œuvre par la plupart des compilateurs pour éviter les copies supplémentaires (potentiellement coûteuses) dans certaines situations. Cela rend possible le retour par valeur ou par valeur par passage (des restrictions s'appliquent).
C'est la seule forme d'optimisation qui élide (ha!) La règle as-if - l'élision de copie peut être appliquée même si copier/déplacer 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 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 compter sur un nombre spécifique de destructeurs appelés. Vous ne devriez pas avoir de logique critique dans les constructeurs/destructeurs de copy/move, car vous ne pouvez pas vous fier à leur appel.
Si un appel à un constructeur de copie ou de déplacement est supprimé, ce constructeur doit toujours exister et doit être accessible. Cela garantit que la copie ne permet pas de copier des objets qui ne sont normalement pas copiables, par exemple. parce qu’ils ont un constructeur de copie/déplacement privé ou supprimé.
C++ 17 : à partir de C++ 17, la copie de l'élision 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
}
Pour une vue et une introduction moins techniques - passez à cette réponse .
Pour les cas courants où la copie se produit - passez à cette réponse .
Copier l'élision est défini dans la norme dans:
comme
31) Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction de copie/déplacement d'un objet de classe, même si le constructeur et/ou le destructeur de 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 manières différentes de faire référence au même objet, et la destruction de cet objet se produit au plus tard des moments où les deux objets auraient été détruit sans l'optimisation.123 Cette élision d'opérations de copie/déplacement, appelée copie d'élision, est autorisée dans les cas suivants (pouvant être combinés 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'un paramètre function ou catch-clause) avec le même type cvunqualified que le type de retour de la fonction, 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 rejet, lorsque l'opérande est le nom d'un objet automatique non volatile (autre qu'un paramètre function ou catch-clause) dont la portée ne s'étend pas au-delà de la fin du bloc try le plus interne (s'il existe 1), l'opération de copie/déplacement de l'opérande vers l'objet exception (15.1) peut être omise en construisant l'objet automatique directement dans l'objet 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 cv-non qualifié, l'opération de copie/déplacement peut être omise en construisant l'objet temporaire directement dans le cible de la copie ou du déplacement omis
- lorsque la déclaration d'exception d'un gestionnaire d'exception (clause 15) déclare un objet du même type (à l'exception de cv-qualification) en tant qu'objet 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 exception si la signification du programme restera inchangée, à l'exception de l'exécution des constructeurs et des destructeurs pour l'objet déclaré par la déclaration d'exception.
123) Comme un seul objet est détruit au lieu de deux, et qu'un constructeur de copie/déplacement n'est pas exécuté, il reste un objet détruit pour chaque objet 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 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 l'objett2
. En réalité, la construction de l’objet localt
peut être considérée comme initialisant directement l’objet globalt2
, et la destruction de cet objet aura lieu à la sortie du programme. L'ajout d'un constructeur de déplacement à Thing a le même effet, mais c'est la construction de déplacement de l'objet temporaire verst2
qui est supprimée.
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 dans laquelle un objet renvoyé en valeur par une méthode a sa copie supprimé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();
L'optimisation de la valeur de retour régulière se produit lorsqu'un temporaire est renvoyé:
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
return Thing();
}
Thing t2 = f();
Les autres endroits courants où 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 valeur :
struct Thing{
Thing();
Thing(const Thing&);
};
void foo() {
Thing c;
throw c;
}
int main() {
try {
foo();
}
catch(Thing c) {
}
}
Les limitations courantes de la copie sont:
La plupart des compilateurs de qualité commerciale prennent en charge la résolution de copie et le (N) RVO (en fonction des paramètres d'optimisation).
L'élision de copie est une technique d'optimisation du compilateur qui élimine les copies/déplacements inutiles 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é:
#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 copie est effectuée 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'était produite), sinon le programme serait mal formé.
Vous ne devez autoriser une telle copie que dans des endroits où cela 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 une é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 fournit l'option -fno-elide-constructors
pour désactiver la copie. Si vous voulez éviter une copie possible, utilisez -fno-elide-constructors
.
Désormais, presque tous les compilateurs fournissent des informations sur la copie lorsque l'optimisation est activée (et si aucune autre option n'est définie pour la désactiver).
Avec chaque copie de copie, une construction et une destruction correspondante de la copie sont omises, économisant ainsi le temps CPU, et un objet n'étant pas créé, économisant ainsi de l'espace sur le cadre de la pile.