Namespaces
Variants

Copy elision

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Lorsque certains critères sont remplis, la création d'un objet de classe à partir d'un objet source du même type (en ignorant les qualifications cv) peut être omise, même si le constructeur sélectionné et/ou le destructeur pour l'objet ont des effets secondaires. Cette élision de création d'objet est appelée copy elision .

Table des matières

Explication

L'élision de copie est autorisée dans les circonstances suivantes (qui peuvent être combinées pour éliminer plusieurs copies) :

  • Dans une return instruction d'une fonction avec un type de retour de classe, lorsque l'opérande est le nom d'un objet non volatile obj ayant une durée de stockage automatique (autre qu'un paramètre de fonction ou un paramètre de gestionnaire ), l' initialisation par copie de l'objet résultat peut être omise en construisant obj directement dans l'objet résultat de l'appel de fonction. Cette variante de l'élision de copie est connue sous le nom d' optimisation de la valeur de retour nommée (NRVO).
  • Lorsqu'un objet de classe target est initialisé par copie avec un objet de classe temporaire obj qui n'a pas été lié à une référence, l'initialisation par copie peut être omise en construisant obj directement dans target . Cette variante de l'élision de copie est connue sous le nom d' optimisation de la valeur de retour sans nom (URVO). Depuis C++17, l'URVO est obligatoire et n'est plus considérée comme une forme d'élision de copie ; voir ci-dessous.
(jusqu'à C++17)
  • Dans une throw expression , lorsque l'opérande est le nom d'un objet non volatile obj avec une durée de stockage automatique (autre qu'un paramètre de fonction ou un paramètre de gestionnaire) qui appartient à une portée qui ne contient pas le bloc try englobant le plus interne (s'il existe), l'initialisation par copie de l'objet exception peut être omise en construisant obj directement dans l'objet exception.
  • Dans un gestionnaire , l'initialisation par copie de l'argument du gestionnaire peut être omise en traitant le paramètre du gestionnaire comme un alias de l'objet exception si la signification du programme reste inchangée, à l'exception de l'exécution des constructeurs et destructeurs pour l'argument du gestionnaire.
(depuis C++11)
  • Dans les coroutines , une copie d'un paramètre de coroutine peut être omise. Dans ce cas, les références à cette copie sont remplacées par des références au paramètre correspondant si la signification du programme reste inchangée, à l'exception de l'exécution d'un constructeur et destructeur pour l'objet copie du paramètre.
(depuis C++20)

Lorsque l'élimination de copie se produit, l'implémentation traite la source et la cible de l'initialisation omise comme simplement deux manières différentes de se référer au même objet.

La destruction se produit au moment le plus tardif entre les deux moments où les deux objets auraient été détruits sans l'optimisation.

(until C++11)

Si le premier paramètre du constructeur sélectionné est une référence à une valeur de déplacement (rvalue reference) du type de l'objet, la destruction de cet objet se produit lorsque la cible aurait été détruite. Sinon, la destruction se produit au moment le plus tardif entre les deux moments où les deux objets auraient été détruits sans l'optimisation.

(since C++11)


Sémantique des prvalues ("suppression de copie garantie")

Depuis C++17, une prvalue n'est pas matérialisée avant d'être nécessaire, puis elle est construite directement dans le stockage de sa destination finale. Cela signifie parfois que même lorsque la syntaxe du langage suggère visuellement une copie/déplacement (par ex. l'initialisation par copie ), aucune copie/déplacement n'est effectué — ce qui signifie que le type n'a pas besoin de posséder un constructeur de copie/déplacement accessible. Les exemples incluent :

T f()
{
    return U(); // constructs a temporary of type U,
                // then initializes the returned T from the temporary
}
T g()
{
    return T(); // constructs the returned T directly; no move
}
Le destructeur du type retourné doit être accessible au point de l'instruction return et non supprimé, même si aucun objet T n'est détruit.
  • Dans l'initialisation d'un objet, lorsque l'expression d'initialisation est une prvalue du même type de classe (en ignorant la qualification cv ) que le type de la variable :
T x = T(T(f())); // x is initialized by the result of f() directly; no move
Ceci ne peut s'appliquer que lorsque l'objet initialisé est connu pour ne pas être un sous-objet potentiellement chevauchant :
struct C { /* ... */ };
C f();
struct D;
D g();
struct D : C
{
    D() : C(f()) {}    // no elision when initializing a base class subobject
    D(int) : D(g()) {} // no elision because the D object being initialized might
                       // be a base-class subobject of some other class
};

Remarque : Cette règle ne spécifie pas une optimisation, et la Norme ne la décrit pas formellement comme une "suppression de copie" (car rien n'est supprimé). Au lieu de cela, la spécification du langage cœur de C++17 concernant les prvalues et les temporaires est fondamentalement différente de celle des révisions antérieures de C++ : il n'y a plus de temporaire à partir duquel copier/déplacer. Une autre façon de décrire la mécanique de C++17 est le "passage de valeur non matérialisée" ou la "matérialisation temporaire différée" : les prvalues sont retournées et utilisées sans jamais matérialiser de temporaire.

(depuis C++17)

Notes

La copie élidée est la seule forme d'optimisation autorisée (jusqu'à C++14) l'une des deux formes d'optimisation autorisées, conjointement avec l'élision d'allocation et l'extension , (depuis C++14) qui peut modifier les effets secondaires observables. Comme certains compilateurs n'effectuent pas l'élision de copie dans toutes les situations où elle est autorisée (par exemple, en mode débogage), les programmes qui dépendent des effets secondaires des constructeurs de copie/déplacement et des destructeurs ne sont pas portables.

Dans une instruction return ou une expression throw , si le compilateur ne peut pas réaliser l'élision de copie mais que les conditions pour l'élision de copie sont remplies, ou le seraient sauf que la source est un paramètre de fonction, le compilateur tentera d'utiliser le constructeur de déplacement même si l'opérande source est désignée par un lvalue (jusqu'à C++23) l'opérande source sera traitée comme un rvalue (depuis C++23) ; voir l'instruction return pour plus de détails.

Dans les expressions constantes et l' initialisation constante , l'élision de copie n'est jamais effectuée.

struct A
{
    void* p;
    constexpr A() : p(this) {}
    A(const A&); // Disable trivial copyability
};
constexpr A a;  // OK: a.p points to a
constexpr A f()
{
    A x;
    return x;
}
constexpr A b = f(); // error: b.p would be dangling and point to the x inside f
constexpr A c = A(); // (until C++17) error: c.p would be dangling and point to a temporary
                     // (since C++17) OK: c.p points to c; no temporary is involved
(depuis C++11)
Macro de test de fonctionnalité Valeur Std Fonctionnalité
__cpp_guaranteed_copy_elision 201606L (C++17) Élimination de copie garantie via les catégories de valeur simplifiées

Exemple

#include <iostream>
struct Noisy
{
    Noisy() { std::cout << "constructed at " << this << '\n'; }
    Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
    Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
    ~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
Noisy f()
{
    Noisy v = Noisy(); // (until C++17) copy elision initializing v from a temporary;
                       //               the move constructor may be called
                       // (since C++17) "guaranteed copy elision"
    return v; // copy elision ("NRVO") from v to the result object;
              // the move constructor may be called
}
void g(Noisy arg)
{
    std::cout << "&arg = " << &arg << '\n';
}
int main()
{
    Noisy v = f(); // (until C++17) copy elision initializing v from the result of f()
                   // (since C++17) "guaranteed copy elision"
    std::cout << "&v = " << &v << '\n';
    g(f()); // (until C++17) copy elision initializing arg from the result of f()
            // (since C++17) "guaranteed copy elision"
}

Sortie possible :

constructed at 0x7fffd635fd4e
&v = 0x7fffd635fd4e
constructed at 0x7fffd635fd4f
&arg = 0x7fffd635fd4f
destructed at 0x7fffd635fd4f
destructed at 0x7fffd635fd4e

Rapports de défauts

Les rapports de défauts modifiant le comportement suivants ont été appliqués rétroactivement aux normes C++ précédemment publiées.

DR Appliqué à Comportement publié Comportement corrigé
CWG 1967 C++11 lors de l'élision de copie utilisant un constructeur de déplacement,
la durée de vie de l'objet déplacé était encore prise en compte
non prise en compte
CWG 2426 C++17 le destructeur n'était pas requis lors du retour d'une prvalue le destructeur est potentiellement invoqué
CWG 2930 C++98 seules les opérations de copie(/déplacement) pouvaient être élidées, mais un
constructeur non-copie(/déplacement) peut être sélectionné par copie-initialization
élide toute construction d'objet
des copy-initializations associées

Voir aussi