Recherche…


But de l'élision de la copie

Il y a des endroits dans le standard où un objet est copié ou déplacé pour initialiser un objet. L'élision de la copie (parfois appelée optimisation de la valeur de retour) est une optimisation par laquelle, dans certaines circonstances spécifiques, un compilateur est autorisé à éviter la copie ou le déplacement même si la norme indique que cela doit se produire.

Considérons la fonction suivante:

std::string get_string()
{
  return std::string("I am a string.");
}

Selon la formulation stricte du standard, cette fonction initialisera un std::string temporaire, puis copiera / déplacera celui-ci dans l'objet de valeur de retour, puis détruira le temporaire. La norme est très claire: c'est ainsi que le code est interprété.

Copier elision est une règle permettant à un compilateur C ++ d' ignorer la création du fichier temporaire et de sa copie / destruction ultérieure. En d'autres termes, le compilateur peut prendre l'expression d'initialisation pour le temporaire et initialiser directement la valeur de retour de la fonction. Cela évite évidemment les performances.

Cependant, il a deux effets visibles sur l'utilisateur:

  1. Le type doit avoir le constructeur copy / move qui aurait été appelé. Même si le compilateur élimine la copie / le déplacement, le type doit toujours pouvoir être copié / déplacé.

  2. Les effets secondaires des constructeurs de copier / déplacer ne sont pas garantis dans les cas où une élision peut se produire. Considérer ce qui suit:

C ++ 11
struct my_type
{
  my_type() = default;
  my_type(const my_type &) {std::cout <<"Copying\n";}
  my_type(my_type &&) {std::cout <<"Moving\n";}
};

my_type func()
{
  return my_type();
}

Que va faire l'appel func ? Eh bien, il n'imprimera jamais "Copie", car le temporaire est une valeur et my_type est un type mobile. Alors, va-t-il imprimer "Moving"?

Sans la règle d'élision de la copie, il faudrait toujours imprimer "Moving". Mais comme la règle de copie existe, le constructeur de déplacement peut ou non être appelé; il dépend de la mise en œuvre.

Et par conséquent, vous ne pouvez pas compter sur l'appel de constructeurs copier / déplacer dans des contextes où l'élision est possible.

Elision étant une optimisation, votre compilateur peut ne pas prendre en charge l'élision dans tous les cas. Et que le compilateur élimine ou non un cas particulier, le type doit toujours prendre en charge l'opération en cours. Ainsi, si une construction de copie est supprimée, le type doit toujours avoir un constructeur de copie, même s'il ne sera pas appelé.

Elision de copie garantie

C ++ 17

Normalement, l'élision est une optimisation. Bien que quasiment tous les compilateurs prennent en charge l’élision de copie dans les cas les plus simples, l’élision impose toujours un fardeau particulier aux utilisateurs. A savoir, le type qui est copié / déplacé doit toujours avoir l'opération de copie / déplacement qui a été éluée.

Par exemple:

std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
  return std::lock_guard<std::mutex>(a_mutex);
}

Cela peut être utile dans les cas où a_mutex est un mutex détenu par un système privé, mais un utilisateur externe peut vouloir y placer un verrou.

Ce n'est pas légal non plus, car std::lock_guard ne peut pas être copié ou déplacé. Même si pratiquement tous les compilateurs C ++ élimine la copie / le déplacement, la norme nécessite toujours que le type dispose de cette opération.

Jusqu'à C ++ 17.

C ++ 17 impose l'élision en redéfinissant efficacement la signification même de certaines expressions afin qu'aucune copie / déplacement n'ait lieu. Considérez le code ci-dessus.

Sous un libellé pré-C ++ 17, ce code dit de créer un temporaire, puis d'utiliser le temporaire pour copier / déplacer dans la valeur de retour, mais la copie temporaire peut être éluée. Sous C ++ 17, cela ne crée pas du tout un temporaire.

En C ++ 17, toute expression de valeur utilisée pour initialiser un objet du même type que l'expression ne génère pas de valeur temporaire. L'expression initialise directement cet objet. Si vous retournez une valeur du même type que la valeur de retour, le type n'a pas besoin d'avoir un constructeur de copier / déplacer. Et par conséquent, sous les règles C ++ 17, le code ci-dessus peut fonctionner.

Le langage C ++ 17 fonctionne dans les cas où le type de la valeur correspond au type en cours d'initialisation. Donc, étant donné get_lock ci-dessus, cela ne nécessitera pas non plus de copier / déplacer:

std::lock_guard the_lock = get_lock();

Comme le résultat de get_lock est une expression de valeur utilisée pour initialiser un objet du même type, aucune copie ou déplacement ne se produira. Cette expression ne crée jamais de temporaire; il est utilisé pour initialiser directement the_lock . Il n'y a pas d'élision car il n'y a pas de copie / mouvement à élider élide.

Le terme "élision de copie garantie" est donc quelque peu trompeur, mais c'est le nom de la fonctionnalité telle qu'elle est proposée pour la normalisation C ++ 17 . Cela ne garantit pas du tout l'élision; cela élimine complètement le copier / déplacer, redéfinissant le C ++ de sorte qu'il n'y ait jamais eu de copier / déplacer.

Cette fonctionnalité ne fonctionne que dans les cas impliquant une expression de valeur. En tant que tel, cela utilise les règles d'élision habituelles:

std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
  std::lock_guard<std::mutex> my_lock(a_mutex);
  //Do stuff
  return my_lock;
}

Bien qu'il s'agisse d'un cas valable pour l'élision de la copie, les règles C ++ 17 n'éliminent pas la copie / le déplacement dans ce cas. En tant que tel, le type doit toujours avoir un constructeur copy / move à utiliser pour initialiser la valeur de retour. Et comme lock_guard ne le fait pas, c'est toujours une erreur de compilation. Les implémentations sont autorisées à refuser les copies lors de la transmission ou du renvoi d'un objet de type trivialement copiable. Cela permet de déplacer de tels objets dans des registres, ce que certains ABI peuvent exiger dans leurs conventions d'appel.

struct trivially_copyable {
    int a;  
};

void foo (trivially_copyable a) {}

foo(trivially_copyable{}); //copy elision not mandated

Valeur de retour elision

Si vous retournez une expression de valeur à partir d'une fonction et que l'expression de valeur a le même type que le type de retour de la fonction, la copie de la valeur temporaire peut être supprimée:

std::string func()
{
  return std::string("foo");
}

Presque tous les compilateurs éluderont la construction temporaire dans ce cas.

Élision des paramètres

Lorsque vous transmettez un argument à une fonction, et que l'argument est une expression de valeur du type de paramètre de la fonction, et que ce type n'est pas une référence, la construction de la valeur peut être élidée.

void func(std::string str) { ... }

func(std::string("foo"));

Cela dit pour créer une string temporaire, puis déplacez-le dans le paramètre de fonction str . L'option Copier permet à cette expression de créer directement l'objet dans str , plutôt que d'utiliser un déplacement temporaire.

Ceci est une optimisation utile pour les cas où un constructeur est déclaré explicit . Par exemple, nous aurions pu écrire ce qui précède sous la forme func("foo") , mais uniquement parce que string a un constructeur implicite qui convertit un caractère const char* en une string . Si ce constructeur était explicit , nous serions obligés d'utiliser un temporaire pour appeler le constructeur explicit . Copier elision nous évite d'avoir à faire une copie / un déplacement inutile.

Elision de valeur de retour nommée

Si vous retournez une expression lvalue d'une fonction, et cette lvalue:

  • représente une variable automatique locale à cette fonction, qui sera détruite après le return
  • la variable automatique n'est pas un paramètre de fonction
  • et le type de la variable est du même type que le type de retour de la fonction

Si tout cela est le cas, alors la copie / déplacement de la lvalue peut être éluée:

std::string func()
{
  std::string str("foo");
  //Do stuff
  return str;
}

Les cas plus complexes sont éligibles, mais plus le cas est complexe, moins le compilateur aura tendance à l'éliminer:

std::string func()
{
  std::string ret("foo");
  if(some_condition)
  {
    return "bar";
  }
  return ret;
}

Le compilateur pourrait encore échapper à ret , mais les chances qu’elles le fassent disparaissent.

Comme indiqué précédemment, l'élision n'est pas autorisée pour les paramètres de valeur.

std::string func(std::string str)
{
  str.assign("foo");
  //Do stuff
  return str; //No elision possible
}

Copie de l'initialisation

Si vous utilisez une expression de valeur pour copier l'initialisation d'une variable et que cette variable a le même type que l'expression de la valeur, la copie peut être supprimée.

std::string str = std::string("foo");

L'initialisation de la copie transforme effectivement cela en std::string str("foo"); (il y a des différences mineures).

Cela fonctionne également avec les valeurs de retour:

std::string func()
{
  return std::string("foo");
}

std::string str = func();

Sans élision de copie, cela provoquerait 2 appels au constructeur de déplacement de std::string . L'option Copier permet d'appeler le constructeur de déplacement 1 fois ou zéro, et la plupart des compilateurs opteront pour ce dernier.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow