Durée de vie des objects temporaires lors de l’initialisation de la liste

J’ai toujours supposé que les objects temporaires vivent jusqu’à la fin d’une expression complète. Voici cependant une curieuse différence entre les initialisations d’un std::vector et d’un tableau.

Veuillez considérer le code suivant:

 #include  #include  struct ID{ static int cnt; // the number of living object of class ID at the moment of creation: int id; ID():id(++cnt){} ~ID(){ cnt--; } }; int ID::cnt=0; int main(){ int arr[]{ID().id, ID().id}; std::vector vec{ID().id, ID().id}; std::cout<<" Array: "<<arr[0]<<", "<<arr[1]<<"\n"; std::cout<<" Vector: "<<vec[0]<<", "<<vec[1]<<"\n"; } 

La sortie de ce programme est un peu (au moins pour moi) inattendue:

  Array: 1, 1 Vector: 1, 2 

Cela signifie que les objects temporaires sont vivants pendant toute l’initialisation du std::vector mais ils sont créés et détruits l’un après l’autre dans le cas d’un tableau. Je m’attendrais à ce que les temporaires fonctionnent jusqu’à ce que la pleine expression int arr[]{ID().id, ID().id}; est terminé.

La norme mentionne une exception concernant la durée de vie des objects temporaires et l’initialisation des tableaux (12.2). Cependant, je ne comprends pas son sens et je ne sais pas pourquoi il est appliqué dans ce cas particulier:

Il existe deux contextes dans lesquels les fonctions temporaires sont détruites à un point différent de la fin de la expression complète. Le premier contexte est celui où un constructeur par défaut est appelé pour initialiser un élément d’un tableau. Si le constructeur a un ou plusieurs arguments par défaut, la destruction de tous les temporaires créés dans un argument par défaut est séquencée avant la construction de l’élément de tableau suivant, le cas échéant.


Vue d’ensemble des résultats avec différents compilateurs (le résultat MSVS est une caractéristique de NathanOliver):

  Array Vector clang 3.8 1, 2 1, 2 g++ 6.1 1, 1 1, 2 icpc 16 1, 1 1, 2 MSVS 2015 1, 1 1, 2 

Comme ecatmur l’a fait remarquer, pour l’initialisation de l’agrégat, chaque élément de la liste-accolés est une expression complète, donc le code suivant

  struct S{ int a; int b; } s{ID().id, ID().id}; std::cout<<" Struct: "<<sa<<", "<<sb<<"\n"; 

devrait imprimer Struct 1, 1 à la console. C’est exactement ce que fait le programme compilé par g ++. Cependant, clang semble avoir un bug – le programme résultant imprime Struct 1, 2 .


Un bogue a été signalé: https://llvm.org/bugs/show_bug.cgi?id=29080

Il s’agit de la question principale 1343 “Séquencement de l’initialisation hors classe” , qui a été acceptée en tant que rapport de défaut en novembre 2016 sur papier P0570R0 . La résolution proposée fait partie de C ++ 17 mais ne fait donc pas partie de C ++ 14, donc (à moins que le comité ne décide de publier un corrigendum à C ++ 14), il s’agit d’un sharepoint différence entre C ++ 17 et C + +14.

C ++ 14

La sortie correcte selon les règles du standard C ++ 14 est 1, 1 pour le tableau et 1, 2 pour le vecteur; En effet, la construction d’un vecteur (y compris à partir d’une liste d’initiales ) nécessite un appel à un constructeur, contrairement à la construction d’un tableau.

Le langage qui régit cela se trouve dans [intro.execution] :

10 – Une expression complète est une expression qui n’est pas une sous-expression d’une autre expression. […] Si une construction de langage est définie pour produire un appel implicite d’une fonction, l’utilisation de la construction de langage est considérée comme une expression aux fins de cette définition. […]

Cela convient parfaitement à une vue d’ensemble, mais certaines questions restnt sans réponse:

  • Précisément quelle construction de langage compte comme la construction produisant un appel implicite d’une fonction;
  • Ce qui compte réellement comme un appel implicite d’une fonction; un appel à un constructeur défini par l’utilisateur est vraisemblablement un appel d’une fonction, mais qu’en est-il d’un constructeur par défaut ou défini par défaut?

Un tableau est un agrégat, il est donc initialisé à partir d’une liste- initiée selon [dcl.init.aggr] ; cela signifie que chaque élément est initialisé directement à partir de l’élément correspondant de la liste, il n’y a donc pas d’appel de fonction implicite (du moins ne correspondant pas à l’initialisation globale). Au niveau de la syntaxe, dans un initialiseur ( [dcl.init] / 1) utilisant une liste- braced -init-list comme initiasortingce de brace ou d’égale-égalité , les expressions complètes sont les expressions contenues entre accolades et séparées par des virgules. A la fin de chaque expression complète, les destructeurs de temporels doivent s’exécuter car aucun des trois contextes mentionnés dans [class.temporary] n’est le cas ici.

Le cas de l’initialisation d’un vecteur est différent, puisque vous utilisez le constructeur initializer_list , un appel implicite d’une fonction (c’est-à-dire le constructeur initializer_list ) se produit. cela signifie qu’il y a une expression complète implicite qui entoure toute l’initialisation, de sorte que les provisoires ne sont détruits que lorsque l’initialisation du vecteur est terminée.

Confusément, [dcl.init.list] dit que votre code est à peu près équivalent à:

 const int __a[2] = {int{ID().id}, int{ID().id}}; // #1 std::vector vec(std::initializer_list(__a, __a + 2)); 

Cependant, ceci doit être lu en contexte – par exemple, le tableau sauvegardant la liste initializer_list a une durée de vie limitée par l’initialisation du vecteur.

C’était beaucoup plus clair en C ++ 03, qui avait dans [intro.execution] :

13 – [ Note: certains contextes en C ++ provoquent l’évaluation d’une expression complète résultant d’un construit syntaxique autre qu’une expression (5.18). Par exemple, en 8.5, une syntaxe pour l’ initialiseur est ( expression-list ) mais la construction résultante est un appel de fonction sur une fonction constructeur avec une liste d’ expressions en tant que liste d’arguments; un tel appel de fonction est une expression complète. Par exemple, au 8.5, une autre syntaxe pour initializer est = initializer-clause mais, à nouveau, la construction résultante pourrait être un appel de fonction sur une fonction constructeur avec une expression d’affectation comme argument; à nouveau, l’appel de fonction est une expression complète. ]

Ce paragraphe est intégralement tiré de C ++ 11; c’était la résolution CWG 392 . La confusion qui en résultait n’était probablement pas intentionnelle.

C ++ 17

Après P0570R0, [intro.execution] indique qu’une expression complète est: […]

  • un init-declarator ([dcl.decl]) […] incluant les expressions constitutives de l’initialiseur, ou […]
  • une expression qui n’est pas une sous-expression d’une autre expression et qui ne fait pas partie d’une expression complète.

Donc, en C ++ 17, l’expression complète est arr[]{ID().id, ID().id} et vec{ID().id, ID().id} respectivement, et le résultat correct est 1, 2 dans chaque cas, puisque la destruction du premier ID temporaire est rescope à la fin de l’expression complète.