118 votes

Comment fonctionne l'élision de copie garantie ?

Lors de la réunion des normes C++ de l'ISO à Oulu en 2016, une proposition appelée Élimination garantie de la copie grâce à des catégories de valeurs simplifiées a été votée dans C++17 par le comité de normalisation.

Comment fonctionne exactement l'élision de copie garantie ? Couvre-t-elle certains cas où l'élision de copie était déjà autorisée, ou des modifications du code sont-elles nécessaires pour garantir l'élision de copie ?

170voto

Nicol Bolas Points 133791

L'élision de copie était autorisée dans un certain nombre de circonstances. Cependant, même si elle était autorisée, le code devait pouvoir fonctionner comme si la copie n'était pas élidée. En d'autres termes, il devait y avoir un constructeur de copie et/ou de déplacement accessible.

L'élision de copie garantie redéfinit un certain nombre de concepts C++, de sorte que certaines circonstances où les copies/déplacements pourraient être élidés ne provoquent pas réellement une copie/déplacement. du tout . Le compilateur n'élide pas une copie ; la norme dit qu'une telle copie ne peut jamais se produire.

Considérons cette fonction :

T Func() {return T();}

Selon les règles d'élision de copie non garanties, cela créera un temporaire, puis se déplacera de ce temporaire vers la valeur de retour de la fonction. Cette opération de déplacement mai être élidée, mais T doit toujours avoir un constructeur de mouvement accessible, même s'il n'est jamais utilisé.

De même :

T t = Func();

Il s'agit d'une copie de l'initialisation de t . Cette copie initialisera t avec la valeur de retour de Func . Cependant, T doit toujours avoir un constructeur de mouvement, même s'il ne sera pas appelé.

Élision de la copie garantie redéfinit la signification d'une expression prvalue . Avant C++17, les prvalues sont des objets temporaires. En C++17, une expression prvalue est simplement quelque chose qui peut matérialiser un temporaire, mais ce n'est pas encore un temporaire.

Si vous utilisez une prvalue pour initialiser un objet du type de la prvalue, alors aucun temporaire n'est matérialisé. Lorsque vous faites return T(); ceci initialise la valeur de retour de la fonction via une valeur prvalue. Puisque cette fonction renvoie T , aucun temporaire n'est créé ; l'initialisation de la prvalue initialise simplement directement la valeur de retour.

Ce qu'il faut comprendre, c'est que, puisque la valeur de retour est un prvalue, il est pas un objet encore. C'est simplement un initialisateur pour un objet, tout comme T() est.

Quand vous le faites T t = Func(); la valeur de la valeur de retour initialise directement l'objet. t ; il n'y a pas d'étape "créer un temporaire et copier/déplacer". Puisque Func() La valeur de retour de l'outil est une valeur prval équivalente à T() , t est directement initialisé par T() exactement comme si vous aviez fait T t = T() .

Si une prvalue est utilisée d'une autre manière, la prvalue matérialisera un objet temporaire, qui sera utilisé dans cette expression (ou écarté s'il n'y a pas d'expression). Ainsi, si vous avez fait const T &rt = Func(); la prvalue matérialiserait une valeur temporaire (en utilisant T() comme initialisateur), dont la référence serait stockée dans le fichier rt ainsi que les habituelles prolongations temporaires de la durée de vie.

Une chose que l'élision garantie vous permet de faire est de renvoyer des objets qui sont immobiles. Par exemple, lock_guard ne peut pas être copié ou déplacé, donc vous ne pourriez pas avoir une fonction qui le renvoie par valeur. Mais avec l'élision de copie garantie, vous le pouvez.

L'élision garantie fonctionne également avec l'initialisation directe :

new T(FactoryFunction());

Si FactoryFunction renvoie à T par valeur, cette expression ne copiera pas la valeur de retour dans la mémoire allouée. Elle va plutôt allouer de la mémoire et utiliser la mémoire allouée comme la mémoire de la valeur de retour pour l'appel de fonction directement.

Ainsi, les fonctions d'usine qui retournent par valeur peuvent directement initialiser la mémoire allouée au tas sans même le savoir. Tant que ces fonctions en interne suivre les règles de l'élision de la copie garantie, bien sûr. Ils doivent retourner une valeur pr de type T .

Bien sûr, cela fonctionne aussi :

new auto(FactoryFunction());

Au cas où vous n'aimeriez pas écrire les noms de caractères.


Il est important de reconnaître que les garanties ci-dessus ne fonctionnent que pour les valeurs de prix. En d'autres termes, vous n'obtenez aucune garantie lorsque vous retournez un fichier nommé variable :

T Func()
{
   T t = ...;
   ...
   return t;
}

Dans ce cas, t doit toujours avoir un constructeur de copie/déplacement accessible. Oui, le compilateur peut choisir d'optimiser la copie/déplacement. Mais le compilateur doit toujours vérifier l'existence d'un constructeur de copie/déplacement accessible.

Rien ne change donc pour l'optimisation des valeurs de retour nommées (NRVO).

0 votes

N'y avait-il vraiment aucune ABI en service qui renvoyait des UDT de la taille d'un mot dans un registre ? Ce genre de règle semble tuer les performances des itérateurs et des types enveloppants tels qu'on les trouve dans les bibliothèques de correction dimensionnelle ( std::chrono a certains de ces types). Ou peut-être que le retour dans le registre est toujours acceptable si le type est trivialement copiable, de sorte que la détermination de l'élision est impossible ?

1 votes

@BenVoigt : Mettre des types définis par l'utilisateur non trivialement copiables dans des registres n'est pas une chose viable qu'une ABI peut faire, que l'élision soit disponible ou non.

1 votes

Maintenant que les règles sont publiques, il peut être intéressant de mettre à jour ce document avec le concept de "prvalues are initializations".

2voto

Vineet Gupta Points 36

Je pense que les détails de l'élision de la copie ont été bien partagés ici. Cependant, j'ai trouvé cet article : https://jonasdevlieghere.com/guaranteed-copy-elision qui fait référence à l'élision de copie garantie en C++17 dans le cas de l'optimisation de la valeur de retour.

Il fait également référence à la façon dont l'option gcc : -fno-elide-constructors, on peut désactiver l'élision de copie et voir qu'au lieu que le constructeur soit directement appelé à la destination, nous voyons 2 constructeurs de copie (ou constructeurs de déplacement en c++11) et leurs destructeurs correspondants être appelés. L'exemple suivant montre les deux cas :

#include <iostream>
using namespace std;
class Foo {
public:
    Foo() {cout << "Foo constructed" << endl; }
    Foo(const Foo& foo) {cout << "Foo copy constructed" << endl;}
    Foo(const Foo&& foo) {cout << "Foo move constructed" << endl;}
    ~Foo() {cout << "Foo destructed" << endl;}
};

Foo fReturnValueOptimization() {
    cout << "Running: fReturnValueOptimization" << endl;
    return Foo();
}

Foo fNamedReturnValueOptimization() {
    cout << "Running: fNamedReturnValueOptimization" << endl;
    Foo foo;
    return foo;
}

int main() {
    Foo foo1 = fReturnValueOptimization();
    Foo foo2 = fNamedReturnValueOptimization();
}
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 testFooCopyElision.cxx # Copy elision enabled by default
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo destructed
Foo destructed
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 -fno-elide-constructors testFooCopyElision.cxx # Copy elision disabled
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Foo destructed
Foo destructed

Je vois que l'optimisation des valeurs de retour, c'est-à-dire l'élision des objets temporaires dans les déclarations de retour, est généralement garantie indépendamment de la version 17 de c++.

Cependant, l'optimisation de la valeur de retour nommée des variables locales retournées se produit le plus souvent mais n'est pas garantie. Dans une fonction avec différentes déclarations de retour, je vois que si chacune des déclarations de retour renvoie des variables de portée locale, ou des variables de même portée, cela se produira. Sinon, si dans différentes déclarations de retour, des variables de différentes portées sont retournées, il serait difficile pour le compilateur d'effectuer l'élision de copie.

Ce serait bien s'il y avait un moyen de garantir l'élision de la copie ou d'obtenir une sorte d'avertissement lorsque l'élision de la copie ne peut pas être effectuée, ce qui inciterait les développeurs à s'assurer que l'élision de la copie est effectuée et à remanier le code si elle ne peut pas être effectuée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X