web-dev-qa-db-fra.com

Le passage d'un objet C ++ dans son propre constructeur est-il légal?

Je suis surpris de découvrir accidentellement que les œuvres suivantes:

#include <iostream>            
int main(int argc, char** argv)
{
  struct Foo {
    Foo(Foo& bar) {
      std::cout << &bar << std::endl;
    }
  };
  Foo foo(foo); // I can't believe this works...
  std::cout << &foo << std::endl; // but it does...
}

Je passe l'adresse de l'objet construit à son propre constructeur. Cela ressemble à une définition circulaire au niveau de la source. Les normes vous permettent-elles vraiment de passer un objet dans une fonction avant même que l'objet ne soit construit ou ce comportement n'est-il pas défini?

Je suppose que ce n'est pas si étrange étant donné que toutes les fonctions membres de classe ont déjà un pointeur vers les données de leur instance de classe en tant que paramètre implicite. Et la disposition des membres de données est fixée au moment de la compilation.

Remarque, je ne demande PAS si c'est utile ou une bonne idée; Je suis en train de bricoler pour en savoir plus sur les cours.

109
Andrew Wagner

Ce n'est pas un comportement indéfini. Bien que foo ne soit pas initialisé, vous l'utilisez d'une manière autorisée par la norme. Une fois l'espace alloué à un objet mais avant qu'il ne soit complètement initialisé, vous êtes autorisé à l'utiliser de manière limitée. La liaison d'une référence à cette variable et la prise de son adresse sont autorisées.

Ceci est couvert par rapport de défaut 363: Initialisation de la classe à partir de soi qui dit:

Et si oui, quelle est la sémantique de l'auto-initialisation de l'UDT? Par exemple

 #include <stdio.h>

 struct A {
        A()           { printf("A::A() %p\n",            this);     }
        A(const A& a) { printf("A::A(const A&) %p %p\n", this, &a); }
        ~A()          { printf("A::~A() %p\n",           this);     }
 };

 int main()
 {
  A a=a;
 }

peut être compilé et imprime:

A::A(const A&) 0253FDD8 0253FDD8
A::~A() 0253FDD8

et la résolution était:

3.8 Le paragraphe 6 de [basic.life] indique que les références ici sont valides. Il est permis de prendre l'adresse d'un objet de classe avant qu'il ne soit complètement initialisé, et il est permis de le passer comme argument à un paramètre de référence tant que la référence peut se lier directement. À l'exception de l'échec de transtypage des pointeurs pour annuler * pour le% p dans les printfs, ces exemples sont conformes à la norme.

La citation complète de la section 3.8 [basic.life] du projet de norme C++ 14 est le suivant:

De même, avant le début de la durée de vie d'un objet, mais après que le stockage que l'objet va occuper a été alloué ou, après la fin de la durée de vie d'un objet et avant que le stockage que l'objet occupé est réutilisé ou libéré, toute valeur glue qui fait référence à l'objet d'origine peut être utilisé mais uniquement de manière limitée. Pour un objet en construction ou en destruction, voir 12.7. Sinon, une telle glvalue fait référence au stockage alloué (3.7.4.2), et l'utilisation des propriétés de la glvalue qui ne dépendent pas de sa valeur est bien définie. Le programme a un comportement indéfini si:

  • une conversion lvalue-to-rvalue (4.1) est appliquée à une telle glvalue,

  • la glvalue est utilisée pour accéder à un membre de données non statique ou appeler une fonction membre non statique de l'objet, ou

  • la glvalue est liée à une référence à une classe de base virtuelle (8.5.3), ou

  • la glvalue est utilisée comme l'opérande d'un dynamic_cast (5.2.7) ou comme l'opérande de typeid.

Nous ne faisons rien avec foo qui relève d'un comportement indéfini tel que défini par les puces ci-dessus.

Si nous essayons cela avec Clang, nous voyons un avertissement inquiétant (voir en direct):

avertissement: la variable 'foo' n'est pas initialisée lorsqu'elle est utilisée dans sa propre initialisation [-Wuninitialized]

C'est un avertissement valide puisque la production d'une valeur indéterminée à partir d'une variable automatique non initialisée est un comportement indéfini . Cependant, dans ce cas, vous liez simplement une référence et prenez l'adresse de la variable dans le constructeur, ce qui ne produit pas de valeur indéterminée et est valide. D'autre part, le suivant l'exemple d'auto-initialisation du projet de norme C++ 11 :

int x = x ;

appelle un comportement indéfini.

Problème actif 453: les références peuvent uniquement se lier à des objets "valides" semble également pertinent mais est toujours ouvert. Le libellé initial proposé est conforme au rapport de défauts 363.

65
Shafik Yaghmour

Le constructeur est appelé à un point où la mémoire est allouée à l'objet à devenir. À ce stade, aucun objet n'existe à cet endroit (ou peut-être un objet avec un destructeur trivial). En outre, le pointeur this fait référence à cette mémoire et la mémoire est correctement alignée.

Puisqu'il s'agit d'une mémoire allouée et alignée, nous pouvons nous y référer en utilisant des expressions lvalue de type Foo (c'est-à-dire Foo&). Ce que nous pouvons pas faire, c'est avoir une conversion de lvalue en rvalue. Cela n'est autorisé qu'après la saisie du corps du constructeur.

Dans ce cas, le code essaie simplement d'imprimer &bar À l'intérieur du corps du constructeur. Il serait même légal d'imprimer bar.member Ici. Puisque le corps constructeur a été entré, un objet Foo existe et ses membres peuvent être lus.

Cela nous laisse avec un petit détail, et c'est la recherche de nom. Dans Foo foo(foo), le premier foo introduit le nom dans la portée et le second foo renvoie donc au nom qui vient d'être déclaré. C'est pourquoi int x = x N'est pas valide, mais int x = sizeof(x) est valide.

15
MSalters