Dentro de una unidad de traducción específica, está garantizado que el orden de inicialización de los objetos estáticos será el mismo que el de aparición de sus definiciones en la unidad de traducción.
No obstante, no hay garantías sobre el orden en que se inicializan los objetos estáticos entre diferentes unidades de traducción, y el lenguaje no proporciona ninguna forma de averiguarlo. Esto puede producir problemas significativos. Como ejemplo de desastre posible (que provocará el cuelgue de sistemas operativos primitivos o la necesidad de matar el proceso en otros más sofisticados), si un archivo contiene:
//: C10:Out.cpp {O} // First file #include <fstream> std::ofstream out("out.txt"); ///:~
Listado 10.25. C10/Out.cpp
y otro archivo utiliza el objeto out
en uno
de sus inicializadores
//: C10:Oof.cpp // Second file //{L} Out #include <fstream> extern std::ofstream out; class Oof { public: Oof() { out << "ouch"; } } oof; int main() {} ///:~
Listado 10.26. C10/Oof.cpp
el programa puede funcionar, o puede que no. Si el entorno de
programación construye el programa de forma que el primer
archivo sea inicializado después del segundo, no habrá
problemas. Pero si el segundo archivo se inicializa antes que el
primero, el constructor para Oof
se
sustenta en la existencia de out
, que todavía
no ha sido construido, lo que causa el caos.
Este problema sólo ocurre con inicializadores de objetos
estáticos que dependen el uno del otro. Los estáticos dentro de
cada unidad de traducción son inicializados antes de la primera
invocación a cualquier función de esa unidad, aunque puede que
después de main()
. No puede estar seguro
del orden de inicialización de objetos estáticos si están en
archivos diferentes.
Un ejemplo sutil puede encontrarse en ARM.[68] en un archivo que aparece en el rango global:
extern int y; int x = y + 1;
y en un segundo archivo también en el ámbitoglobal:
extern int x; int y = x + 1;
Para todos los objetos estáticos, el mecanismo de carga-enlazado
garantiza una inicialización estática a cero antes de la
inicialización dinámica especificada por el programador. En el
ejemplo anterior, la inicialización a cero de la zona de memoria
ocupada por el objeto fstream out
no tiene especial
relevancia, por lo que realmente no está definido hasta que se
llama al constructor. Pese a ello, en el caso de los tipos
predefinidos, la inicialización a cero sí tiene importancia, y
si los archivos son inicializados en el orden mostrado arriba, y
empieza estáticamente inicializada a cero, por lo que
x
se convierte en uno, e y
es dinámicamente inicializada a dos. Pero si los archivos fuesen
inicializados en orden opuesto, x
sería
estáticamente inicializada a cero, y
dinámicamente inicializada a uno y después, x
pasaría a valer dos.
Los programadores deben estar al tanto de esto porque puede darse el caso de crear un programa con dependencias de inicialización estáticas que funcionen en una plataforma determinada y, de golpe y misteriosamente, compilarlo en otro entorno y que deje de funcionar.
Existen tres aproximaciones para tratar con este problema:
No hacerlo. Evitar las dependencias de inicialización estática es la mejor solución.
Si debe hacerlo, coloque las definiciones de objetos estáticos críticos en un único fichero, de forma que pueda controlar, de forma portable, su inicialización colocándolos en el orden correcto.
Si está convencido que es inevitable dispersar objetos estáticos entre unidades de traducción diferentes (como en el caso de una librería, donde no puede controlar el programa que la usa), hay dos técnicas de programación para solventar el problema.
El pionero de esta técnica fue iostream
(puesto que las
definiciones para cin
,
cout
y cerr
son
static
y residen en archivos
diferentes). Realmente es inferior a la segunda técnica
pero ha pululado durante mucho tiempo por lo que puede
encontrarse con código que la utilice; así pues, es
importante que entienda como funciona.
Esta técnica requiere una clase adicional en su archivo de cabecera. Esta clase es la responsable de la inicialización dinámica de sus objetos estáticos de librería. He aquí un ejemplo simple:
//: C10:Initializer.h // Static initialization technique #ifndef INITIALIZER_H #define INITIALIZER_H #include <iostream> extern int x; // Declarations, not definitions extern int y; class Initializer { static int initCount; public: Initializer() { std::cout << "Initializer()" << std::endl; // Initialize first time only if(initCount++ == 0) { std::cout << "performing initialization" << std::endl; x = 100; y = 200; } } ~Initializer() { std::cout << "~Initializer()" << std::endl; // Clean up last time only if(--initCount == 0) { std::cout << "performing cleanup" << std::endl; // Any necessary cleanup here } } }; // The following creates one object in each // file where Initializer.h is included, but that // object is only visible within that file: static Initializer init; #endif // INITIALIZER_H ///:~
Listado 10.27. C10/Initializer.h
Las declaraciones para x
e
y
anuncian tan sólo que esos objetos
existen, pero no reservan espacio para los objetos. No
obstante, la definición para el Initializer init
reserva espacio para ese objeto en cada archivo en que se
incluya el archivo de cabecera. Pero como el nombre es
static
(en esta ocasión controlando la visibilidad,
no la forma en la que se almacena; el almacenamiento se
produce a nivel de archivo por defecto), sólo es visible en
esa unidad de traducción, por lo que el enlazador no se
quejará por múltiples errores de definición.
He aquí el archivo con las definiciones para
x
, y
e
initCount
:
//: C10:InitializerDefs.cpp {O} // Definitions for Initializer.h #include "Initializer.h" // Static initialization will force // all these values to zero: int x; int y; int Initializer::initCount; ///:~
Listado 10.28. C10/InitializerDefs.cpp
(Por supuesto, una instancia estática de
fichero de init
también se
incluye en este archivo cuando se incluye el archivo de
cabecera. Suponga que otros dos archivos se crean por
la librería del usuario:
//: C10:Initializer.cpp {O} // Static initialization #include "Initializer.h" ///:~
Listado 10.29. C10/Initializer.cpp
y
//: C10:Initializer2.cpp //{L} InitializerDefs Initializer // Static initialization #include "Initializer.h" using namespace std; int main() { cout << "inside main()" << endl; cout << "leaving main()" << endl; } ///:~
Listado 10.30. C10/Initializer2.cpp
Ahora no importa en qué unidad de traducción se inicializa
primero. La primera vez que una unidad de traducción que
contenga Initializer.h
se inicialice,
initCount
será cero por lo que la
inicialización será llevada a cabo. (Esto depende en gran
medida en el hecho que la zona de almacenamiento estático está
a cero antes de que cualquier inicialización dinámica se lleve
a cabo). Para el resto de unidades de traducción,
initCount
no será cero y se eludirá la
inicialización. La limpieza ocurre en el orden inverso, y
~Initializer()
asegura que sólo ocurrirá
una vez.
Este ejemplo utiliza tipos del lenguaje como objetos estáticos
globales. Esta técnica también funciona con clases, pero esos
objetos deben ser inicializados dinámicamente por la clase
Initializer
. Una forma de hacer esto
es creando clases sin constructores ni destructores, pero sí
con métodos de inicialización y limpieza con nombres
diferentes. Una aproximación más común, de todas formas, es
tener punteros a objetos y crearlos utilizando new
dentro de Initializer()
.
Bastante después de la aparición de la técnica uno, alguien (no sé quien) llegó con la técnica explicada en esta sección, que es mucho más simple y limpia que la anterior. El hecho de que tardase tanto en descubrirse es un tributo a la complejidad de C++.
Esta técnica se sustenta en el hecho de que los objetos estáticos dentro de funciones (sólo) se inicializan la primera vez que se llama a la función. Tenga presente que el problema que estamos intentando resolver aquí no es cuando se inicializan los objetos estáticos (que se puede controlar separadamente) sino más bien asegurarnos de que la inicialización ocurre en el orden adecuado.
Esta técnica es muy limpia y astuta. Para cualquier dependencia de inicialización, se coloca un objeto estático dentro de una función que devuelve una referencia a ese objeto. De esta forma, la única manera de acceder al objeto estático es llamando a la función, y si ese objeto necesita acceder a otros objetos estáticos de los que depende, debe llamar a sus funciones. Y la primera vez que se llama a una función, se fuerza a llevar a cabo la inicialización. Está garantizado que el orden de la inicialización será correcto debido al diseño del código, no al orden que arbitrariamente decide el enlazador.
Para mostrar un ejemplo, aquí tenemos dos clases que
dependen la una de la otra. La primera contiene un
bool
que sólo se inicializa por el constructor,
por lo que se puede decir si se ha llamado el constructor
por una instancia estática de la clase (el área de
almacenamiento estático se inicializa a cero al inicio del
programa, lo que produce un valor false
para el
bool
si el constructor no ha sido llamado).
//: C10:Dependency1.h #ifndef DEPENDENCY1_H #define DEPENDENCY1_H #include <iostream> class Dependency1 { bool init; public: Dependency1() : init(true) { std::cout << "Dependency1 construction" << std::endl; } void print() const { std::cout << "Dependency1 init: " << init << std::endl; } }; #endif // DEPENDENCY1_H ///:~
Listado 10.31. C10/Dependency1.h
El constructor también indica cuando ha sido llamado, y es posible el estado del objeto para averiguar si ha sido inicializado.
La segunda clase es inicializada por un objeto de la primera clase, que es lo que causa la dependencia:
//: C10:Dependency2.h #ifndef DEPENDENCY2_H #define DEPENDENCY2_H #include "Dependency1.h" class Dependency2 { Dependency1 d1; public: Dependency2(const Dependency1& dep1): d1(dep1){ std::cout << "Dependency2 construction "; print(); } void print() const { d1.print(); } }; #endif // DEPENDENCY2_H ///:~
Listado 10.32. C10/Dependency2.h
El constructor se anuncia a si mismo y imprime el estado del
objeto d1
por lo que puede ver si éste se
ha inicializado cuando se llama al constructor.
Para demostrar lo que puede ir mal, el siguiente archivo
primero pone las definiciones de los objetos estáticos en el
orden incorrecto, tal y como sucedería si el enlazador
inicializase el objeto Dependency2
antes del Dependency1
. Después se
invierte el orden para mostrar que funciona correctamente si
el orden resulta ser el correcto. Finalmente, se muestra la
técnica dos.
Para proporcionar una salida más legible, se ha creado la
función separator()
. El truco está en
que usted no puede llamar a la función globalmente a menos
que la función sea utilizada para llevar a cabo la
inicialización de la variable, por lo que
separator()
devuelve un valor absurdo
que es utilizado para inicializar un par de variables
globales.
//: C10:Technique2.cpp #include "Dependency2.h" using namespace std; // Returns a value so it can be called as // a global initializer: int separator() { cout << "---------------------" << endl; return 1; } // Simulate the dependency problem: extern Dependency1 dep1; Dependency2 dep2(dep1); Dependency1 dep1; int x1 = separator(); // But if it happens in this order it works OK: Dependency1 dep1b; Dependency2 dep2b(dep1b); int x2 = separator(); // Wrapping static objects in functions succeeds Dependency1& d1() { static Dependency1 dep1; return dep1; } Dependency2& d2() { static Dependency2 dep2(d1()); return dep2; } int main() { Dependency2& dep2 = d2(); } ///:~
Listado 10.33. C10/Technique2.cpp
Las funciones d1()
y
d2()
contienen instancias estáticas de
los objetos Dependency1
y
Dependency2
. Ahora, la única forma de
acceder a los objetos estáticos es llamando a las funciones y
eso fuerza la inicialización estática en la primera llamada a
la función. Esto significa que se garantiza la inicialización
correcta, cosa que verá cuando lance el programa y observe la
salida.
He aquí como debe organizar el código para usar esta
técnica. Ordinariamente, los objetos estáticos deben ser
definidos en archivos diferentes (puesto que se ha visto
forzado a ello por alguna razón; recuerde que definir objetos
estáticos en archivos diferentes es lo que causa el problema),
por lo que definirá las funciones envoltorio wrapping functions
) en archivos
diferentes. Pero éstas necesitan estar declaradas en los
archivos de cabecera:
//: C10:Dependency1StatFun.h #ifndef DEPENDENCY1STATFUN_H #define DEPENDENCY1STATFUN_H #include "Dependency1.h" extern Dependency1& d1(); #endif // DEPENDENCY1STATFUN_H ///:~
Listado 10.34. C10/Dependency1StatFun.h
En realidad, el «extern» es redundante para la declaración de la función. Éste es el segundo archivo de cabecera:
//: C10:Dependency2StatFun.h #ifndef DEPENDENCY2STATFUN_H #define DEPENDENCY2STATFUN_H #include "Dependency2.h" extern Dependency2& d2(); #endif // DEPENDENCY2STATFUN_H ///:~
Listado 10.35. C10/Dependency2StatFun.h
Ahora, en los archivos de implementación donde previamente habría situado las definiciones de los objetos estáticos, situará las definiciones de las funciones envoltorio:
//: C10:Dependency1StatFun.cpp {O} #include "Dependency1StatFun.h" Dependency1& d1() { static Dependency1 dep1; return dep1; } ///:~
Listado 10.36. C10/Dependency1StatFun.cpp
Presumiblemente, otro código puede también componer esos archivos. He aquí otro archivo:
//: C10:Dependency2StatFun.cpp {O} #include "Dependency1StatFun.h" #include "Dependency2StatFun.h" Dependency2& d2() { static Dependency2 dep2(d1()); return dep2; } ///:~
Listado 10.37. C10/Dependency2StatFun.cpp
Ahora hay dos archivos que pueden ser enlazados en cualquier orden y si contuviesen objetos estáticos ordinarios podría producirse cualquier orden de inicialización. Pero como contienen funciones envoltorio, no hay posibilidad de inicialización incorrecta:
//: C10:Technique2b.cpp //{L} Dependency1StatFun Dependency2StatFun #include "Dependency2StatFun.h" int main() { d2(); } ///:~
Listado 10.38. C10/Technique2b.cpp
Cuando ejecute este programa verá que la inicialización del
objeto estático Dependency1
siempre
se lleva a cabo antes de la inicialización del objeto
estático Dependency2
. También puede
ver que ésta es una solución bastante más simple que la de
la uno.
Puede verse tentado a escribir d1()
y
d2()
como funciones inline
dentro de sus respectivos archivos de cabecera, pero eso es
algo que, definitivamente, no debe hacer. Una función
inline
puede ser duplicada en cada archivo en el
que aparezca y esa duplicación incluye la definición de los
objetos estáticos. Puesto que las funciones inline
llevan asociado por defecto enlazado interno, esto provocará
la aparición de múltiples objetos estáticos entre las
diversas unidades de traducción, lo que ciertamente causará
problemas. Es por eso que debe asegurarse que sólo existe
una única definición para cada función contenedora, y eso
significa no hacerlas inline
.
[68] Bjarne Stroustrup and Margaret Ellis, The Annotated C++ Reference Manual, Addison-Wesley, 1990, pp. 20-21.