Comment fonctionne l'élision de la copie garantie?

89

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

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

jotik
la source

Réponses:

129

L'élision de copie a été autorisée dans un certain nombre de circonstances. Cependant, même si cela était autorisé, le code devait encore pouvoir fonctionner comme si la copie n'était pas éludée. À savoir, il devait y avoir une copie accessible et / ou un constructeur de déplacement.

Copie garantie élision redéfinit un certain nombre de concepts de C, de sorte que certaines circonstances où les copies / mouvements pourraient être éludée ne provoquez pas en fait une copie / déplacer du tout . Le compilateur n'élide pas une copie; la norme dit qu'aucune copie de ce genre ne pourrait jamais avoir lieu.

Considérez cette fonction:

T Func() {return T();}

Sous les règles d'élision de copie non garanties, cela créera un temporaire, puis passera de ce temporaire à la valeur de retour de la fonction. Cette opération de déplacement peut être élidée, mais Tdoit toujours avoir un constructeur de déplacement accessible même s'il n'est jamais utilisé.

De même:

T t = Func();

Il s'agit de l'initialisation de la copie de t. Cela copiera initialize tavec la valeur de retour de Func. Cependant, Tdoit toujours avoir un constructeur de mouvement, même s'il ne sera pas appelé.

L'élision de 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, aucun temporaire n'est matérialisé. Lorsque vous le faites return T();, cela initialise la valeur de retour de la fonction via une prvalue. Puisque cette fonction retourne T, aucun temporaire n'est créé; l'initialisation de la prvalue initialise simplement directement la valeur de retour.

La chose à comprendre est que, puisque la valeur de retour est une valeur pr, ce n'est pas encore un objet . C'est simplement un initialiseur pour un objet, tout comme l' T()est.

Lorsque vous le faites T t = Func();, la prvalue 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 est une prvalue équivalente à T(), test directement initialisée par T(), exactement comme si vous l'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 annulé s'il n'y a pas d'expression). Donc, si vous const T &rt = Func();le faisiez, la prvalue matérialiserait un temporaire (en utilisant T()comme initialiseur), dont la référence serait stockée rt, avec le truc habituel d'extension de durée de vie temporaire.

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

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

new T(FactoryFunction());

Si FactoryFunctionretourne Tpar valeur, cette expression ne copiera pas la valeur de retour dans la mémoire allouée. Il allouera à la place de la mémoire et utilisera la mémoire allouée comme mémoire de 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 ceux-ci fonctionnent en interne , bien sûr, les règles d'élision de la copie garantie. Ils doivent renvoyer une prvalue 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 prvalues. Autrement dit, vous n'obtenez aucune garantie lors du retour d'une variable nommée :

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

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

Donc, rien ne change pour l'optimisation de la valeur de retour nommée (NRVO).

Nicol Bolas
la source
1
@BenVoigt: Mettre des types définis par l'utilisateur non copiables dans des registres n'est pas une chose viable qu'une ABI peut faire, que l'élision soit disponible ou non.
Nicol Bolas
1
Maintenant que les règles sont publiques, il peut être intéressant de mettre à jour ceci avec le concept «prvalues ​​are initializations».
Johannes Schaub - litb le
6
@ JohannesSchaub-litb: Ce n'est "ambigu" que si vous en savez trop sur les minuties du standard C ++. Pour 99% de la communauté C ++, nous savons à quoi fait référence «élision de copie garantie». Le papier proprement dit proposant la fonctionnalité est même intitulé «Élision de copie garantie». L'ajout de «par le biais de catégories de valeur simplifiées» ne fait que le rendre confus et difficile à comprendre pour les utilisateurs. C'est également un abus de langage, car ces règles ne "simplifient" pas vraiment les règles relatives aux catégories de valeurs. Que cela vous plaise ou non, le terme «élision de copie garantie» fait référence à cette fonctionnalité et à rien d'autre.
Nicol Bolas
1
Je veux tellement pouvoir prendre une valeur prvalue et la transporter. Je suppose que c'est juste un (one-shot) std::function<T()>vraiment.
Yakk - Adam Nevraumont
1
@LukasSalich: C'est une question C ++ 11. Cette réponse concerne une fonctionnalité C ++ 17.
Nicol Bolas