Concrètement, le compilateur réorganise les opérations contenues dans la liste d'initialisation en se basant sur l'ordre de déclaration des membres au sein de la classe. Ces assignments sont injectés au début du corps du constructeur, avant l'exécution de tout code défini explicitement par le développeur.
Examinons un exemple en C++ moderne :
struct Organisme {
private:
int valA;
int valB;
int valC;
int valD;
public:
Organisme() : valB(10), valA(20), valD(30) {
valC = 40;
}
};
int main() {
Organisme obj;
return 0;
}
Voyons maintenant le code assembleur généré pour la fonction main :
_main PROC
; allocation de la pile pour 'obj'
push ebp
mov ebp, esp
sub esp, 16 ; espace mémoire réservé pour l'instance obj
; appel au constructeur
lea ecx, DWORD PTR [obj] ; passage de 'this' implicite via le registre ecx
call ?Organisme@@QAE@XZ ; invocation du constructeur
; fin de main
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_endproc
Lorsqu'on examine les instructions relatives au constructeur de Organisme, la séquence devient plus claire :
?Organisme@@QAE@XZ PROC
; sauvegarde du contexte et récupération du pointeur this
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
; Initialisations issues de la liste d'initialisation
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], 20 ; Initialisation de valA (déclaré en premier)
mov ecx, DWORD PTR _this$[ebp]
mov DWORD PTR [ecx+4], 10 ; Initialisation de valB (déclaré en deuxième)
mov edx, DWORD PTR _this$[ebp]
mov DWORD PTR [edx+12], 30 ; Initialisation de valD (déclaré en quatrième)
; Corps du constructeur
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax+8], 40 ; Assignation de valC (corps utilisateur)
pop ebp
ret 0
_endproc
L'analyse révèle que malgré l'ordre apparent dans le code source (valB avant valA), l'initialisation réelle suit la logique de définition : valA est traité avant valB. De plus, toutes ces opérations précèdent l'instruction interne valC = 40.
Négliger cet ordre peut entraîner des comportements indéfinis. Considérons le cas suivant :
struct ErreurPotentielle {
private:
int numero;
int copieNumero;
public:
ErreurPotentielle() : copieNumero(numero) { // Problème ici
}
};
Bien que la liste d'initialisation mentionne copieNumero en premier, le compilateur initialisera d'abord numero. Si ce dernier n'a pas été initialisé auparavant (ce qui est le cas ici), sa valeur est indélébile, rendant l'affectation à copieNumero erronée.
Il existe quatre situations où l'utilisation de la liste d'initialisation est obligatoire ou fortement recommandée :
- L'initialsiation d'un membre de type référence.
- L'initialisation d'une constante membre (
const). - La délégaion vers un constructeur de classe de base possédant des paramètres.
- L'appel au constructeur d'un objet membre nécessitant des arguments.
Prenons l'exemple d'une classe contenant une instance d'une autre classe :
class Component {
private:
int id;
public:
Component(int v) : id(v) {}
Component() {} // Nécessaire sans liste d'initialisation
};
class Container {
private:
int count;
Component comp;
public:
Container() {
count = 1;
comp = Component(2); // Affectation après construction par défaut
}
};
Dans cette configuraton, le compilateur effectue plusieurs étapes inefficaces :
- Construction de
compvia son constructeur par défaut. - Création d'un tempoarire via le constructeur paramétré
Component(2). - Appel de l'opérateur d'affectation (
operator=) pour copier le temporaire verscomp.
Cependant, si nous optimisons avec la liste d'initialisation et supprimons le constructeur par défaut inutile :
class OptimizedContainer {
private:
int count;
Component comp;
public:
OptimizedContainer() : comp(2) { // Initialisation directe
count = 1;
}
};
L'analyse du code binaire montre une différence notable dans le mécanisme sous-jacent. Dans la version optimisée, lors de l'exécution du constructeur de OptimizedContainer, la première action consiste directement à appeler le constructeur paramétré de Component, en passant l'adresse mémoire de l'objet membre comme cible.
Cela évite la phase intermédiaire de construction par défaut suivie d'une affectation. L'optimisation supprime la création d'objets temporaires superflus et réduit le nombre d'appels de fonctions, améliorant ainsi significativement les performances d'exécution sans compromettre la sémantique du programme.
Le code assembleur correspondant confirme l'appel direct à Component(int) avec le déplacement approprié des registres, éliminant toute opération de copie supplémentaire entre un objet temporaire et le membre final.