Orden de inicialización de variables estáticas
C++ garantiza que las variables en una unidad de compilación (archivo .cpp) se inicializan en el orden de declaración. Para el número de unidades de compilación, esta regla funciona para cada una por separado (me refiero a variables estáticas fuera de las clases).
Sin embargo, el orden de inicialización de las variables no está definido en diferentes unidades de compilación.
¿Dónde puedo ver algunas explicaciones sobre este orden para gcc y MSVC (sé que confiar en eso es una muy mala idea; es solo para comprender los problemas que podemos tener con el código heredado al pasar a un nuevo GCC principal y a un sistema operativo diferente)? ?
Como usted dice, el orden no está definido en las diferentes unidades de compilación.
Dentro de una misma unidad de compilación el orden está bien definido: El mismo orden que la definición.
Esto se debe a que esto no se resuelve a nivel de lenguaje sino a nivel de enlazador. Así que realmente necesitas consultar la documentación del vinculador. Aunque realmente dudo que esto ayude de alguna manera útil.
Para gcc: consulte ld
Descubrí que incluso cambiar el orden de los archivos de objetos que se vinculan puede cambiar el orden de inicialización. Por lo tanto, no solo debe preocuparse por su vinculador, sino también por cómo su sistema de compilación invoca el vinculador. Incluso intentar resolver el problema es prácticamente imposible.
Generalmente, esto es solo un problema al inicializar elementos globales que hacen referencia entre sí durante su propia inicialización (por lo que solo afecta a objetos con constructores).
Existen técnicas para solucionar el problema.
- Inicialización diferida.
- Contador negro
- Coloque todas las variables globales complejas dentro de la misma unidad de compilación.
- Nota 1: globales:
se usa libremente para referirse a variables de duración de almacenamiento estático que potencialmente se inicializan antesmain()
. - Nota 2: Potencialmente,
en el caso general esperamos que las variables de duración del almacenamiento estático se inicialicen antes que main, pero el compilador puede diferir la inicialización en algunas situaciones (las reglas son complejas, consulte el estándar para obtener más detalles).
Espero que el orden del constructor entre módulos sea principalmente una función del orden en que se pasan los objetos al vinculador.
Sin embargo, GCC le permite utilizar init_priority
para especificar explícitamente el orden de los ctores globales:
class Thingy
{
public:
Thingy(char*p) {printf(p);}
};
Thingy a("A");
Thingy b("B");
Thingy c("C");
genera 'ABC' como era de esperar, pero
Thingy a __attribute__((init_priority(300))) ("A");
Thingy b __attribute__((init_priority(200))) ("B");
Thingy c __attribute__((init_priority(400))) ("C");
genera 'BAC'.
Como ya sabes que no debes confiar en esta información a menos que sea absolutamente necesario, aquí viene. Mi observación general en varias cadenas de herramientas (MSVC, gcc/ld, clang/llvm, etc.) es que el orden en el que los archivos objeto se pasan al vinculador es el orden en el que se inicializarán.
Hay excepciones a esto, y no las reclamo todas, pero estas son las que encontré:
1) Las versiones de GCC anteriores a la 4.7 en realidad se inicializan en el orden inverso a la línea de enlace. Este ticket en GCC es cuando ocurrió el cambio y rompió muchos programas que dependían del orden de inicialización (¡incluido el mío!).
2) En GCC y Clang, el uso de la prioridad de la función constructora puede alterar el orden de inicialización. Tenga en cuenta que esto sólo se aplica a funciones que están declaradas como "constructores" (es decir, deben ejecutarse como lo haría un constructor de objetos globales). Intenté usar prioridades como esta y descubrí que incluso con la prioridad más alta en una función constructora, todos los constructores sin prioridad (por ejemplo, objetos globales normales, funciones constructoras sin prioridad) se inicializarán primero . En otras palabras, la prioridad es sólo relativa a otras funciones con prioridades, pero los verdaderos ciudadanos de primera clase son aquellos que no tienen prioridad. Para empeorar las cosas, esta regla es efectivamente la opuesta en GCC antes de 4.7 debido al punto (1) anterior.
3) En Windows, hay una función de punto de entrada de biblioteca compartida (DLL) muy interesante y útil llamada DllMain() que, si se define, se ejecutará con el parámetro "fdwReason" igual a DLL_PROCESS_ATTACH directamente después de que se hayan inicializado todos los datos globales y antes de que la aplicación consumidora tenga la oportunidad de llamar a cualquier función en la DLL. Esto es extremadamente útil en algunos casos y no existe en absoluto un comportamiento análogo a este en otras plataformas con GCC o Clang con C o C++. Lo más parecido que encontrará es crear una función constructora con prioridad (consulte el punto (2) anterior), que no es en absoluto lo mismo y no funcionará para muchos de los casos de uso para los que funciona DllMain().
4) Si está utilizando CMake para generar sus sistemas de compilación, lo que hago a menudo, descubrí que el orden de los archivos fuente de entrada será el orden de los archivos objeto resultantes proporcionados al vinculador. Sin embargo, muchas veces su aplicación/DLL también se vincula en otras bibliotecas, en cuyo caso esas bibliotecas estarán en la línea de vínculo después de sus archivos fuente de entrada. Si está buscando que uno de sus objetos globales sea el primero en inicializarse, entonces está de suerte y puede colocar el archivo fuente que contiene ese objeto como el primero en la lista de archivos fuente. Sin embargo, si desea que uno sea el último en inicializarse (¡lo que puede replicar efectivamente el comportamiento de DllMain()!), entonces puede realizar una llamada a add_library() con ese archivo fuente para producir una biblioteca estática y agregar la biblioteca estática resultante como la última dependencia de enlace en su llamada target_link_libraries() para su aplicación/DLL. Tenga cuidado porque su objeto global puede optimizarse en este caso y puede usar el indicador --whole-archive para forzar al vinculador a no eliminar los símbolos no utilizados para ese pequeño archivo específico.
Consejo de cierre
Para conocer absolutamente el orden de inicialización resultante de su aplicación vinculada/biblioteca compartida, pase --print-map al vinculador ld y grep para .init_array (o en GCC anterior a 4.7, grep para .ctors). Cada constructor global se imprimirá en el orden en que se inicializará y recuerde que el orden es opuesto en GCC anterior a 4.7 (consulte el punto (1) anterior).
El factor que me motivó a escribir esta respuesta es que necesitaba conocer esta información, no tenía otra opción que confiar en el orden de inicialización y solo encontré fragmentos escasos de esta información en otras publicaciones de SO y foros de Internet. La mayor parte se aprendió a través de mucha experimentación, ¡y espero que esto les ahorre a algunas personas el tiempo de hacerlo!
http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 : este enlace se mueve. Este es más estable pero tendrás que buscarlo.
Editar: osgx proporcionó un enlace mejor .
Una solución sólida es utilizar una función getter que devuelva una referencia a una variable estática. A continuación se muestra un ejemplo simple, una variante compleja de nuestro middleware SDG Controller .
// 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);
La inicialización sería con la única variable miembro estática bool A::a1
. Esto entonces llamaría Foo::insertIntoBar(22)
. Esto luego llamaría Foo::getBar()
en el que se produciría la inicialización de la std::vector<int>
variable estática antes de devolver una referencia al objeto inicializado.
Si se static std::vector<int> bar
colocara directamente como una variable miembro de Foo class
, existiría la posibilidad, dependiendo del orden de los nombres de los archivos fuente, de que bar
se inicializaran después insertIntoBar()
de ser llamados, lo que bloquearía el programa.
Si se llamaran varias variables miembro estáticas insertIntoBar()
durante su inicialización, el orden no dependería de los nombres de los archivos fuente, es decir, aleatorio, pero se std::vector<int>
garantizaría que se inicializarían antes de que se insertara cualquier valor en ellos.