web-dev-qa-db-fra.com

Comment fonctionne l'élision de copie garantie?

Lors de la réunion des normes Oulu ISO C++ 2016, une proposition intitulée Élision de copie garantie via 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 copie garantie? Couvre-t-il certains cas où la suppression de la copie était déjà autorisée, ou des changements de code sont-ils nécessaires pour garantir la suppression de la copie?

74
jotik

L'élision de la copie a été autorisée dans un certain nombre de circonstances. Cependant, même s'il était autorisé, le code devait encore pouvoir fonctionner comme si la copie n'était pas élidée. À savoir, 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/mouvements pourraient être élidés ne provoquent pas du tout de copie . Le compilateur n'échappe pas à une copie; la norme stipule qu'aucune copie de ce type ne pourra jamais se produire.

Considérez cette fonction:

T Func() {return T();}

Selon 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 T doit 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 initialiser t avec la valeur de retour de Func. Cependant, T doit toujours avoir un constructeur de déplacement, même s'il ne sera pas appelé.

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

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

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

Lorsque vous 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 la valeur de retour de Func() est une valeur équivalente à T(), t est directement initialisée par T(), exactement comme si vous aviez fait T t = T().

Si une valeur est utilisée d'une autre manière, la valeur va matérialiser un objet temporaire, qui sera utilisé dans cette expression (ou rejeté s'il n'y a pas d'expression). Donc, si vous avez fait const T &rt = Func();, la valeur matérialiserait un temporaire (en utilisant T() comme initialiseur), dont la référence serait stockée dans rt, avec le temporaire habituel extension de durée de vie.

L'élision garantie vous permet de retourner des objets immobiles. Par exemple, lock_guard Ne peut pas être copié ou déplacé, vous ne pouvez donc pas avoir de fonction qui le renvoie par valeur. Mais avec l'élision de copie garantie, vous le pouvez.

L'élision garantie fonctionne également avec une 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. Au lieu de cela, il allouera 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 renvoient par valeur peuvent initialiser directement la mémoire allouée au tas sans même le savoir. Tant que ces fonctions en interne suivent les règles d'élision de copie garantie, bien sûr. Ils doivent renvoyer une valeur de type T.

Bien sûr, cela fonctionne aussi:

new auto(FactoryFunction());

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


Il est important de reconnaître que les garanties ci-dessus ne fonctionnent que pour les valeurs. Autrement dit, vous n'obtenez aucune garantie lors du retour d'une variable nommée :

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

Dans ce cas, t doit toujours avoir un constructeur copier/déplacer 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 copier/déplacer accessible.

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

111
Nicol Bolas