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).