68 votes

Ordre d'initialisation des variables statiques

Le C++ garantit que les variables d'une unité de compilation (fichier .cpp) sont initialisées dans l'ordre de leur déclaration. Pour un nombre d'unités de compilation, cette règle fonctionne pour chacune d'entre elles séparément (je parle des variables statiques en dehors des classes).

Mais, l'ordre d'initialisation des variables, n'est pas défini entre les différentes unités de compilation.

Où puis-je trouver des explications sur cet ordre pour gcc et MSVC (je sais que se fier à cela est une très mauvaise idée - c'est juste pour comprendre les problèmes que nous pouvons avoir avec le code hérité lors du passage à une nouvelle version majeure de GCC et à un système d'exploitation différent) ?

76voto

Loki Astari Points 116129

Comme vous le dites, l'ordre n'est pas défini dans les différentes unités de compilation.

Au sein d'une même unité de compilation, l'ordre est bien défini : Le même ordre que la définition.

C'est parce que ce problème n'est pas résolu au niveau du langage mais au niveau de l'éditeur de liens. Vous devez donc vraiment consulter la documentation de l'éditeur de liens. Bien que je doute vraiment que cela puisse vous aider de manière utile.

Pour gcc : Vérifiez ld

J'ai constaté que le simple fait de changer l'ordre des fichiers d'objets liés peut modifier l'ordre d'initialisation. Ce n'est donc pas seulement de votre éditeur de liens que vous devez vous préoccuper, mais de la façon dont l'éditeur de liens est invoqué par votre système de construction. Même essayer de résoudre le problème est pratiquement voué à l'échec.

Ce problème ne se pose généralement que lors de l'initialisation de globaux qui se réfèrent les uns aux autres pendant leur propre initialisation (ce qui n'affecte donc que les objets avec constructeurs).

Il existe des techniques pour contourner le problème.

  • Initialisation paresseuse.
  • Compteur noir
  • Placez toutes les variables globales complexes dans la même unité de compilation.

  • Note 1 : les globaux :
    Utilisé librement pour faire référence à des variables statiques de durée de stockage qui sont potentiellement initialisées avant l'entrée dans le système. main() .
  • Note 2 : Potentiellement
    Dans le cas général, nous nous attendons à ce que les variables de durée de stockage statique soient initialisées avant main, mais le compilateur est autorisé à différer l'initialisation dans certaines situations (les règles sont complexes ; voir la norme pour plus de détails).

7 votes

Ma préférence va à tous les globaux dans la même unité de compilation... :-)

12 votes

Il serait préférable de ne pas avoir besoin de globales du tout.

4 votes

Vous avez tous les deux raison, mais malheureusement cela était inconnu de générations de programmeurs qui ont écrit des tonnes de bibliothèques et de code tiers que nous avons pu utiliser...

19voto

Je pense que l'ordre des constructeurs entre les modules est principalement fonction de l'ordre dans lequel vous passez les objets à l'éditeur de liens.

Cependant, GCC vous permet utiliser init_priority pour spécifier explicitement l'ordre pour les ctors mondiaux :

class Thingy
{
public:
    Thingy(char*p) {printf(p);}
};

Thingy a("A");
Thingy b("B");
Thingy c("C");

produit 'ABC' comme vous vous y attendez, mais

Thingy a __attribute__((init_priority(300))) ("A");
Thingy b __attribute__((init_priority(200))) ("B");
Thingy c __attribute__((init_priority(400))) ("C");

sorties "BAC".

2 votes

Une nouvelle spécifier explicitement lien qui fonctionne pour l'instant.

0 votes

J'ai utilisé cet attribut mais gcc donne "warning : requested init_priority is reserved for internal use". Comme il s'agit d'un avertissement, j'ai quand même été autorisé à le faire. Existe-t-il un autre moyen de définir la priorité d'initialisation ?

0 votes

@Andrew - Vous devriez pas utiliser init , fini o init_priority . Au lieu de cela, utilisez l'option constructor attribut . Il y a aussi un destructor attribuer. Il se peut que vous deviez utiliser init y fini sur d'autres compilateurs et plateformes, mais pour GCC, vous ne l'utilisez pas. "Clarification de la attribut init_priority" sur la liste de diffusion du GCC.

17voto

Nicholas Smith Points 580

Puisque vous savez déjà qu'il ne faut pas se fier à cette information sauf en cas de nécessité absolue, la voici. Mon observation générale à travers diverses chaînes d'outils (MSVC, gcc/ld, clang/llvm, etc) est que l'ordre dans lequel vos fichiers objets sont passés à l'éditeur de liens est l'ordre dans lequel ils seront initialisés.

Il existe des exceptions à cette règle, et je ne prétends pas les connaître toutes, mais voici celles que j'ai rencontrées moi-même :

1) Les versions de GCC antérieures à 4.7 initialisent en fait dans l'ordre inverse de la ligne de liaison. Ce billet dans GCC est le moment où le changement s'est produit, et il a cassé beaucoup de programmes qui dépendaient de l'ordre d'initialisation (y compris le mien !).

2) Dans GCC et Clang, l'utilisation de la fonction fonction constructeur priorité peut modifier l'ordre d'initialisation. Notez que cela ne s'applique qu'aux fonctions qui sont déclarées comme étant des "constructeurs" (c'est-à-dire qu'elles doivent être exécutées comme le serait un constructeur d'objet global). J'ai essayé d'utiliser les priorités de cette façon et j'ai constaté que même avec la plus haute priorité sur une fonction de constructeur, tous les constructeurs sans priorité (par exemple, les objets globaux normaux, les fonctions de construction sans priorité) seront initialisés. primero . En d'autres termes, la priorité n'est relative qu'aux autres fonctions prioritaires, mais les véritables citoyens de première classe sont ceux qui ne sont pas prioritaires. Pour aggraver les choses, cette règle est effectivement l'inverse dans GCC avant la version 4.7 en raison du point (1) ci-dessus.

3) Sous Windows, il existe une fonction de point d'entrée de bibliothèque partagée (DLL) très soignée et utile appelée DllMain() qui, s'il est défini, sera exécuté avec le paramètre "fdwReason" égal à DLL_PROCESS_ATTACH directement après que toutes les données globales aient été initialisées et que antes de l'application consommatrice a la possibilité d'appeler n'importe quelle fonction de la DLL. C'est extrêmement utile dans certains cas, et il est absolument nécessaire de faire appel à des fonctions de la DLL. n'est pas Il n'existe pas de comportement analogue à celui-ci sur d'autres plateformes avec GCC ou Clang avec C ou C++. Ce qui s'en rapproche le plus est la création d'une fonction de constructeur avec priorité (voir le point (2) ci-dessus), ce qui n'est absolument pas la même chose et ne fonctionnera pas pour la plupart des cas d'utilisation pour lesquels DllMain() fonctionne.

4) Si vous utilisez CMake pour générer vos systèmes de construction, ce que je fais souvent, j'ai constaté que l'ordre des fichiers sources d'entrée sera l'ordre des fichiers objets résultants donnés à l'éditeur de liens. Cependant, il arrive souvent que votre application/DLL soit également liée à d'autres bibliothèques, dans ce cas, ces bibliothèques seront sur la ligne de liaison. après vos fichiers sources d'entrée. Si vous souhaitez que l'un de vos objets globaux soit l'objet tout premier pour s'initialiser, alors vous avez de la chance et vous pouvez mettre le fichier source contenant cet objet en premier dans la liste des fichiers sources. Cependant, si vous souhaitez que l'un d'entre eux soit le fichier source de l'objet la toute dernière pour s'initialiser (ce qui peut effectivement reproduire le comportement de DllMain() !) alors vous pouvez faire un appel à add_library() avec ce seul fichier source pour produire une bibliothèque statique, et ajouter la bibliothèque statique résultante comme toute dernière dépendance de lien dans votre appel target_link_libraries() pour votre application/DLL. Soyez conscient que votre objet global peut être optimisé dans ce cas et vous pouvez utiliser la fonction --whole-archive pour forcer l'éditeur de liens à ne pas supprimer les symboles inutilisés pour ce petit fichier d'archive spécifique.

Conseil de clôture

Pour connaître absolument l'ordre d'initialisation résultant de votre application/bibliothèque partagée liée, passez --print-map à l'éditeur de liens ld et recherchez .init_array (ou dans GCC avant 4.7, recherchez .ctors). Chaque constructeur global sera imprimé dans l'ordre dans lequel il sera initialisé, et rappelez-vous que l'ordre est opposé dans GCC avant 4.7 (voir le point (1) ci-dessus).

Le facteur de motivation pour écrire cette réponse est que j'avais besoin de connaître cette information, que je n'avais pas d'autre choix que de me fier à l'ordre d'initialisation et que je n'ai trouvé que des bribes de cette information dans d'autres messages de l'OS et dans des forums Internet. La plupart de ces informations ont été apprises par l'expérimentation, et j'espère que cela évitera à certaines personnes de devoir le faire !

4voto

Ray Tayek Points 4635

http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 - ce lien se déplace. ce un est plus stable mais vous devrez le chercher.

edit : osgx a fourni une meilleure enlace .

0 votes

Il y a une copie dans les archives web : http://web.archive.org/web/20080512011623/http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 ; "[10.12] What's the "static initialization order fiasco" ?" section de C++ FAQ Lite par Marshall Cline. Une section similaire se trouve dans isocpp.org/wiki/faq/ctors

1voto

Moritz Points 507

Une solution robuste consiste à utiliser une fonction getter qui renvoie une référence à une variable statique. Un exemple simple est montré ci-dessous, une variante complexe dans notre projet Middleware du contrôleur SDG .

// Foo.h
class Foo {
 public:
  Foo() {}

  static bool insertIntoBar(int number);

 private:
  static std::vector<int>& getBar();
};

// Foo.cpp
std::vector<int>& Foo::getBar() {
  static std::vector<int> bar;
  return bar;
}

bool Foo::insertIntoBar(int number) {
  getBar().push_back(number);
  return true;
}

// A.h
class A {
 public:
  A() {}

 private:
  static bool a1;
};

// A.cpp
bool A::a1 = Foo::insertIntoBar(22);

L'initialisation se ferait avec la seule variable membre statique bool A::a1 . Cela appellerait alors Foo::insertIntoBar(22) . Cela appellerait alors Foo::getBar() dans lequel l'initialisation de l'élément statique std::vector<int> se produirait avant de retourner une référence à l'objet initialisé.

Si le static std::vector<int> bar ont été placées directement comme variable membre de la Foo class il serait possible, en fonction de l'ordre de nommage des fichiers source, que bar serait initialisé après insertIntoBar() ont été appelés, faisant ainsi planter le programme.

Si plusieurs variables membres statiques appellent insertIntoBar() lors de leur initialisation, l'ordre ne dépendrait pas des noms des fichiers sources, c'est-à-dire qu'il serait aléatoire, mais la fonction std::vector<int> serait garantie d'être initialisée avant que des valeurs y soient insérées.

0 votes

Il s'agit du modèle de fonction "singleton", qui constitue une bonne solution pour le démarrage, avec seulement une petite surcharge d'exécution. Le problème auquel il faut faire attention avec les singletons est celui de l'arrêt : Si un objet singleton crée un autre singleton enfant dans ses méthodes d'exécution (après sa propre construction), ce singleton enfant sera détruit avant le singleton parent. Si le destructeur du parent fait appel à l'enfant, il échouera terriblement car l'enfant est déjà détruit !

0 votes

Tout à fait d'accord. Il est principalement destiné aux appareils embarqués disposant de ressources limitées et pour lesquels l'arrêt en cas de coupure de courant ou d'appel à reset() est possible.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X