L’initialisation implique-t-elle une conversion de lvalue à rvalue? Est-ce que `int x = x;` UB?

Le standard C ++ contient un exemple semi-célèbre de recherche de noms “surprenante” au 3.3.2, “Point de déclaration”:

int x = x; 

Cela initialise x avec lui-même, ce qui (étant un type primitif) n’est pas initialisé et a donc une valeur indéterminée (en supposant qu’il s’agit d’une variable automatique).

Est-ce que ce comportement est indéfini?

Selon 4.1 “Lvalue-to-rvalue conversion”, il s’agit d’un comportement indéfini pour effectuer une conversion lvalue-à-valeur sur une valeur non initialisée. Le droitier x subit-il cette conversion? Si oui, l’exemple aurait-il un comportement indéfini?

    MISE À JOUR: Suite à la discussion dans les commentaires, j’ai ajouté quelques preuves à la fin de cette réponse.


    Disclaimer : J’avoue que cette réponse est plutôt spéculative. La formulation actuelle de la norme C ++ 11, par contre, ne semble pas permettre une réponse plus formelle.


    Dans le contexte de ce Q & A , il est apparu que le standard C ++ 11 ne spécifiait pas de manière formelle les catégories de valeurs attendues par chaque construction de langage. Dans ce qui suit, je me concentrerai principalement sur les opérateurs intégrés , bien que la question concerne les initialiseurs . Finalement, je finirai par étendre les conclusions que j’ai tirées pour le cas des opérateurs au cas des initialiseurs.

    Dans le cas des opérateurs intégrés, malgré l’absence de spécification formelle, des preuves (non normatives) sont trouvées dans la norme selon lesquelles la spécification voulue est de laisser les valeurs attendues partout où une valeur est requirejse, et lorsqu’elles ne sont pas spécifiées. sinon

    Par exemple, une note au paragraphe 3.10 / 1 dit:

    La discussion de chaque opérateur intégré à l’Article 5 indique la catégorie de la valeur qu’il génère et les catégories de valeur des opérandes attendues. Par exemple, les opérateurs d’atsortingbution intégrés s’attendent à ce que l’opérande gauche soit une lvalue et que l’opérande de droite soit une valeur et donne une lvalue en résultat. Les opérateurs définis par l’utilisateur sont des fonctions et les catégories de valeurs attendues et de rendement sont déterminées par leurs parameters et types de retour.

    La section 5.17 sur les opérateurs d’affectation, par contre, ne le mentionne pas. Cependant, la possibilité d’effectuer une conversion de lvalue en valeur est mentionnée, à nouveau dans une note (paragraphe 5.17 / 1):

    Par conséquent, un appel de fonction ne doit pas intervenir entre la conversion lvalue-à-valeur et l’effet secondaire associé à un seul opérateur d’affectation composé

    Bien sûr, si aucune valeur n’était attendue, cette note serait dénuée de sens.

    Une autre preuve est trouvée dans 4/8, comme le souligne Johannes Schaub dans les commentaires sur les questions et réponses liées:

    Il existe certains contextes dans lesquels certaines conversions sont supprimées. Par exemple, la conversion lvalue-à-rvalue n’est pas effectuée sur l’opérande de l’opérateur unaire &. Des exceptions spécifiques sont données dans les descriptions de ces opérateurs et contextes.

    Cela semble impliquer que la conversion de lvalue à rvalue est effectuée sur tous les opérandes des opérateurs intégrés, sauf indication contraire. Cela signifierait, à son tour, que les valeurs attendues sont des opérandes d’opérateurs intégrés, sauf indication contraire.


    CONJECTURE:

    Même si l’initialisation n’est pas une affectation et que, par conséquent, les opérateurs ne participent pas à la discussion, je pense que cette zone de la spécification est affectée par le même problème que celui décrit ci-dessus.

    Des traces supportant cette croyance peuvent être trouvées même au paragraphe 8.5.2 / 5, à propos de l’initialisation des références (pour lesquelles la valeur de l’expression d’initialisation lvalue n’est pas nécessaire):

    Les conversions usuelles de lvalue à rvalue (4.1), de tableau à pointeur (4.2) et de fonction à pointeur (4.3) ne sont pas nécessaires et sont donc supprimées lorsque de telles liaisons directes avec lvalues ​​sont effectuées.

    Le mot “habituel” semble impliquer que lors de l’initialisation d’objects qui ne sont pas d’un type de référence, la conversion lvalue-à-rvalue est censée s’appliquer.

    Par conséquent, je pense que bien que les exigences relatives à la catégorie de valeur attendue des initialiseurs ne soient pas spécifiées (sinon complètement manquantes), il est logique de supposer que la spécification voulue est que:

    Chaque fois qu’une valeur est requirejse par une construction de langage, une valeur est attendue sauf indication contraire .

    Dans cette hypothèse, une conversion de lvalue à rvalue serait requirejse dans votre exemple, ce qui entraînerait un comportement indéfini.


    PREUVES SUPPLÉMENTAIRES:

    Juste pour fournir d’autres preuves à l’appui de cette conjecture, supposons que ce soit faux , de sorte qu’aucune conversion lvalue-à-rvalue ne soit requirejse pour l’initialisation de la copie, et considérons le code suivant (grâce à jogojapan ):

     int y; int x = y; // No UB short t; int u = t; // UB! (Do not like this non-uniformity, but could accept it) int z; z = x; // No UB (x is not uninitialized) z = y; // UB! (Assuming assignment operators expect a prvalue, see above) // This would be very counterintuitive, since x == y 

    Ce comportement non uniforme n’a pas beaucoup de sens pour moi. Ce qui est plus logique pour IMO, c’est que partout où une valeur est requirejse, une valeur est attendue.

    De plus, comme le souligne correctement Jesse Good dans sa réponse, le paragraphe clé du standard C ++ est le 8.5 / 16:

    – Sinon, la valeur initiale de l’object en cours d’initialisation est la valeur (éventuellement convertie) de l’expression d’initialisation . Si nécessaire , des conversions standard (clause 4) seront utilisées pour convertir l’expression de l’initialiseur en la version non qualifiée cv du type de destination. aucune conversion définie par l’utilisateur n’est prise en compte. Si la conversion ne peut être effectuée, l’initialisation est mal formée. [Note: Une expression de type “cv1 T” peut initialiser un object de type “cv2 T” indépendamment des qualificateurs cv1 et cv2.

    Cependant, alors que Jesse se concentre principalement sur le ” si nécessaire “, je voudrais également souligner le mot ” type “. Le paragraphe ci-dessus mentionne que les conversions standard seront utilisées ” si nécessaire ” pour convertir le type de destination, mais ne dit rien sur les conversions de catégories :

    1. Les conversions de catégories seront-elles effectuées si nécessaire?
    2. Sont-ils nécessaires?

    En ce qui concerne la deuxième question, comme indiqué dans la partie originale de la réponse, le standard C ++ 11 ne spécifie actuellement pas si des conversions de catégories sont nécessaires ou non, car nulle part il est mentionné si l’initialisation de la copie . Ainsi, une réponse claire est impossible à donner. Cependant, je crois avoir fourni suffisamment de preuves pour supposer que cela correspond à la spécification voulue , de sorte que la réponse serait “oui”.

    Quant à la première question, il me semble raisonnable que la réponse soit “oui” également. Si c’était “Non”, des programmes manifestement corrects seraient mal formés:

     int y = 0; int x = y; // y is lvalue, prvalue expected (assuming the conjecture is correct) 

    Pour résumer (A1 = ” Réponse à la question 1 “, A2 = ” Réponse à la question 2 “):

      | A2 = Yes | A2 = No | ---------|------------|---------| A1 = Yes | UB | No UB | A1 = No | ill-formed | No UB | --------------------------------- 

    Si A2 est “Non”, A1 n’a pas d’importance: il n’y a pas d’UB, mais les situations bizarres du premier exemple (par exemple z = y donnant UB, mais pas z = x même si x == y ) apparaissent. Si A2 est “Oui”, A1 devient crucial. pourtant, suffisamment de preuves ont été données pour prouver que ce serait “oui”.

    Par conséquent, ma thèse est que A1 = “Oui” et A2 = “Oui”, et que nous devrions avoir un comportement indéfini .


    UNE PREUVE SUPPLÉMENTAIRE:

    Ce rapport de défaut (gracieuseté de Jesse Good ) propose un changement qui vise à donner un comportement indéfini dans ce cas:

    […] En outre, le paragraphe 1 de [conv.lval] 4.1 indique que l’application de la conversion lvalue-à-rvalue à un «object [qui] n’est pas initialisé» entraîne un comportement indéfini; ceci devrait être reformulé en termes d’object avec une valeur indéterminée .

    En particulier, le libellé proposé pour le paragraphe 4.1 dit:

    Lorsqu’une conversion lvalue à rvalue se produit dans un opérande non évalué ou une sous-expression de celle-ci (clause 5 [expr]), la valeur contenue dans l’object référencé n’est pas accessible. Dans tous les autres cas, le résultat de la conversion est déterminé selon les règles suivantes:

    – Si T est (éventuellement qualifié cv) std :: nullptr_t, le résultat est une constante de pointeur nul (4.10 [conv.ptr]).

    – Sinon, si la glvalue T a un type de classe, la conversion initialise par recopie un temporaire de type T à partir de la glvalue et le résultat de la conversion est une valeur pour le temporaire.

    – Sinon, si l’object auquel la valeur glvalue se réfère contient une valeur de pointeur invalide (3.7.4.2 [basic.stc.dynamic.deallocation], 3.7.4.3 [basic.stc.dynamic.safety]), le comportement est défini par la mise en œuvre. .

    – Sinon, si T est un type de caractère non signé (éventuellement qualifié cv) (3.9.1 [basic.fundamental]) et l’object auquel la glvalue se réfère contient une valeur indéterminée (5.3.4 [expr.new], 8.5 [dcl.init], 12.6.2 [class.base.init]), et cet object n’a pas de durée de stockage automatique ou la glvalue était l’opérande d’un unaire & opérateur ou était lié à une référence, le résultat est un valeur non spécifiée. [Note en bas de page: la valeur peut être différente chaque fois que la conversion de lvalue en valeur est appliquée à l’object. Un object char non signé avec une valeur indéterminée allouée à un registre peut piéger. —End note de bas de page]

    Sinon, si l’object auquel se réfère la glvalue contient une valeur indéterminée, le comportement est indéfini.

    – Sinon, si la glvalue a le type (éventuellement qualifié cv) std :: nullptr_t, le résultat de la valeur est une constante de pointeur nul (4.10 [conv.ptr]). Sinon, la valeur contenue dans l’object indiqué par la glvalue est le résultat de la valeur.

    Une séquence de conversion implicite d’une expression e en type T est définie comme étant équivalente à la déclaration suivante, en utilisant t comme résultat de la conversion (catégorie de valeur modulo, qui sera définie en fonction de T ), 4p3 et 4p6.

     T t = e; 

    L’effet de toute conversion implicite est le même que l’exécution de la déclaration et de l’initialisation correspondantes, puis l’utilisation de la variable temporaire comme résultat de la conversion.

    Au paragraphe 4, la conversion d’une expression en type génère toujours des expressions avec une propriété spécifique. Par exemple, la conversion de 0 en int* donne une valeur de pointeur nulle et non une seule valeur de pointeur arbitraire. La catégorie de valeur est également une propriété spécifique d’une expression et son résultat est défini comme suit

    Le résultat est une valeur si T est un type de référence lvalue ou une référence de valeur à un type de fonction (8.3.2), une valeur x si T est une référence de valeur à un type d’object et une valeur par ailleurs.

    On sait donc que dans int t = e; , le résultat de la séquence de conversion est une valeur, car int est un type non-référence. Donc, si nous fournissons une valeur, nous avons évidemment besoin d’une conversion. 3.10p2 précise que pour ne laisser aucun doute

    Lorsqu’une valeur apparaît dans un contexte où une valeur est attendue, la valeur de glvalue est convertie en une valeur; voir 4.1, 4.2 et 4.3.

    Ce comportement n’est pas indéfini. Vous ne connaissez pas ses valeurs spécifiques, car il n’y a pas d’initialisation. Si la variable est de type global et intégré, le compilateur le mettra à la valeur correcte. Si la variable est locale, le compilateur ne l’initialise pas. Donc, toutes les variables sont initialisées pour vous-même, ne vous fiez pas au compilateur.

    Le comportement n’est pas indéfini. La variable n’est pas initialisée et rest avec les valeurs non initialisées de la valeur aléatoire. Un exemple de combinaison d’essai de clan:

     int test7b(int y) { int x = x; // expected-note{{variable 'x' is declared here}} if (y) x = 1; // Warn with "may be uninitialized" here (not "is sometimes uninitialized"), // since the self-initialization is intended to suppress a -Wuninitialized // warning. return x; // expected-warning{{variable 'x' may be uninitialized when used here}} } 

    Ce que vous pouvez trouver dans les tests clang / test / Sema / uninit-variables.c pour ce cas explicitement.