Supposons que nous ayons une classe B
qui a un member
qui est initialisé par défaut à 42
. Cette classe sait imprimer la valeur de son member
(elle le fait dans le constructeur):
struct B
{
B() : member(42) { printMember(); }
void printMember() const { std::cout << "value: " << member << std::endl; }
int member;
};
Ensuite, nous ajoutons une classe A
qui reçoit une référence const à un B
et demande à B
d'imprimer sa valeur:
struct A
{
A(const B& b) { b.printMember(); }
};
Enfin, nous ajoutons une autre classe Aggregate
qui agrège une A
et une B
. La partie délicate est que l'objet a
de type A
est déclaré avant l'objet b
type B
, mais ensuite a
est initialisé en utilisant un (pas encore valide? ) référence à b
:
struct Aggregate
{
A a;
B b;
Aggregate() : a(b) { }
};
Considérez la sortie de la création d'un Aggregate
(J'ai ajouté une journalisation au constructeur et au destructeur de A
et B
) ( Essayez-le en ligne! ):
a c'tor
value: 0
b c'tor
value: 42
b d'tor
a d'tor
Ai-je raison de supposer qu'il n'est pas valide d'initialiser a
avec une référence à une instance (pas encore valide) de b
et qu'il s'agit donc d'un comportement non défini?
Je connais l'ordre d'initialisation. C'est ce qui me fait lutter. Je sais que b
n'est pas encore construit, mais je pense aussi pour savoir que la future adresse de b
peut être déterminée avant même que b
soit construit. Par conséquent, j'ai supposé qu'il pouvait y avoir une règle que je ne connais pas qui permet au compilateur d'initialiser par défaut les membres b
s avant b
's construction ou quelque chose comme ça. (Il aurait été plus évident que la première valeur imprimée aurait été quelque chose qui semble aléatoire plutôt que 0
(la valeur par défaut de int
)).
Cette réponse m'a aidé à comprendre que je dois faire la distinction entre
Oui, vous avez raison: UB , mais pour des raisons différentes que de simplement stocker une référence à un objet qui n'a pas été construit.
La construction des membres de la classe se fait par ordre d'apparition dans la classe. Bien que l'adresse de B
ne va pas changer et techniquement vous pouvez y stocker une référence , comme l'a souligné @StoryTeller, en appelant b.printMember()
dans le constructeur avec b
qui n'a pas encore été construit est définitivement UB.
L'ordre d'initialisation des membres de la classe est le suivant.
D'après la norme CPP (N4713), la partie pertinente est mise en surbrillance:
15.6.2 Initialisation des bases et des membres [class.base.init] ...
13 Dans un constructeur non délégué, l'initialisation se déroule dans l'ordre suivant:
(13.1) - Premièrement, et uniquement pour le constructeur de la classe la plus dérivée (6.6.2), les classes de base virtuelles sont initialisées dans l'ordre où elles apparaissent sur une première profondeur de gauche à droite de la graphe acyclique dirigé des classes de base, où "de gauche à droite" est l'ordre d'apparition des classes de base dans la liste des spécificateurs de base des classes dérivées.
(13.2) - Ensuite, les classes de base directes sont initialisées dans l'ordre de déclaration telles qu'elles apparaissent dans la liste des spécificateurs de base (quel que soit l'ordre des initialiseurs mem).
(13.3) - Ensuite, les membres de données non statiques sont initialisés dans l'ordre dans lequel ils ont été déclarés dans la définition de classe (à nouveau indépendamment de l'ordre des initialiseurs mem).
(13.4) - Enfin, l'instruction composée du corps constructeur est exécutée.
[Remarque: l'ordre de déclaration a pour mandat de garantir que les sous-objets de base et de membre sont détruits dans l'ordre inverse de l'initialisation. —Fin note]