2010-09-19 18 views
9

Creo que mi problema se describe mejor en código:¿El orden de inicialización global de C++ ignora las dependencias?

#include <stdio.h> 

struct Foo; 

extern Foo globalFoo; 

struct Foo { 
    Foo() { 
     printf("Foo::Foo()\n"); 
    } 

    void add() { 
     printf("Foo::add()\n"); 
    } 

    static int addToGlobal() { 
     printf("Foo::addToGlobal() START\n"); 

     globalFoo.add(); 

     printf("Foo::addToGlobal() END\n"); 

     return 0; 
    } 
}; 

Foo globalFoo; 

int dummy = Foo::addToGlobal(); 

int main() { 
    printf("main()\n"); 

    return 0; 
} 

Las impresiones anteriores (con gcc 4.4.3):

Foo::Foo() 
Foo::addToGlobal() START 
Foo::add() 
Foo::addToGlobal() END 
main() 

Esto es lo que espero, y parece lógico.

Sin embargo, cuando intercambiar las siguientes líneas:

Foo globalFoo; 
int dummy = Foo::addToGlobal(); 

en esto:

int dummy = Foo::addToGlobal(); 
Foo globalFoo; 

emite el programa siguiente:

Foo::addToGlobal() START 
Foo::add() 
Foo::addToGlobal() END 
Foo::Foo() 
main() 

Parece métodos de instancia de Foo están siendo llamados usando una instancia que aún no ha sido construido! Algo tan simple como mover la declaración de una variable en el ámbito global está afectando el comportamiento del programa, y ​​eso me lleva a pensar (1) que el orden de inicialización de los globales no está definido y (2) el orden de inicialización de los globales ignora todas las dependencias. ¿Es esto correcto? ¿Es posible asegurarse de que se llama al constructor de Foo antes de inicializar dummy?

El problema que estoy tratando de resolver es llenar un depósito de elementos (una instancia estática de Foo) estáticamente. En mi intento actual, estoy usando una macro que (entre otras cosas) crea una variable global (en un espacio de nombres anónimo para evitar el conflicto de nombres) cuya inicialización desencadena la inicialización estática. Tal vez estoy abordando mi problema desde el ángulo equivocado? ¿Hay una mejor alternativa (s)? Gracias.

Respuesta

9

En el orden de inicialización, lea la respuesta here.

Sobre cómo resolver el problema de inicialización, puede presionar el global para que sea una variable local estática en una función. Hay garantías estándar que la variable local estática se iniciará en la primera llamada a la función:

class Foo { 
public: 
    static Foo& singleton() { 
     static Foo instance; 
     return instance; 
    } 
}; 

A continuación, sus otras variables globales tendrían acceso a la variable como:

Foo::singleton().add(); 

Tenga en cuenta que esto no es generalmente considerado como un buen diseño, y también que incluso si esto resuelve los problemas de inicialización, no resuelve el orden de finalización, por lo que debe tener cuidado de no acceder al singleton después de que se haya destruido.

+0

Encontré esta solución después de publicar mi pregunta. Lo intenté, y parece funcionar. Sin embargo, puede haber un problema: ¿habrá múltiples instancias debido a 'static' dentro de' singleton'? Es decir, ¿podría 'Foo :: singleton()' devolver cosas diferentes si se declara y define en un archivo de cabecera y se incluye en varias unidades de traducción? – strager

+0

Otro posible problema con este patrón es que AFAIK no es seguro para acceder a Singleton al mismo tiempo. – FuleSnabel

+1

@strager: cuando la variable local se declara 'static ', habrá una sola instancia de la misma, incluso si la función está en línea (es decir, compilada por separado en diferentes unidades de traducción), el enlazador debe asegurarse de que solo una definición única de la variable existe –

29

(1) el orden de inicialización de variables globales no se define

Las variables globales en una unidad (archivo de origen) Traducción solo se inicializan en el orden en el que están definidas.

El orden de inicialización de las variables globales en diferentes unidades de traducción no está especificado.

(2) el orden de inicialización de variables globales ignora todas las dependencias

derecho.

¿Es posible asegurarse de que se llama al constructor de Foo antes de inicializar el maniquí?

Sí, si se define globalFooantesdummy y ellos están en la misma unidad de traducción.

Una opción sería tener un puntero estático a la instancia global; dicho puntero se inicializará a nulo antes de que tenga lugar cualquier inicialización dinámica; addToGlobal puede probar si el puntero es nulo; si es así, entonces es la primera vez que se usa global y addToGlobal puede crear el Foo global.

+0

Por "¿Es posible asegurarse de que se llama al constructor de Foo antes de inicializar el maniquí?" Quise decir "aparte de cambiar el orden de las definiciones". Su respuesta aclara un poco las cosas, pero en realidad no 'resuelve' mi problema (todavía). – strager

+0

Me parece que sería preferible tener un miembro estático de Foo, que es el repositorio. Tratar a Foo como un repositorio (es decir, globalFoo) y para otros fines no es "correcto". Si, por otro lado, Foo 'tiene un' repositorio global, entonces init debería funcionar bien y el diseño parece más limpio. –

+0

Preferiría tener el singleton implementado como una función estática: 'Foo & global_foo() {static Foo foo; volver foo; } 'ya que hay una garantía ligeramente mejor en el orden de inicialización: se inicializará durante la primera llamada al método. Eso todavía tendrá problemas durante la finalización ... –

1

Está en lo cierto, la inicialización de globales entre unidades de traducción no está definida. Es posible evitar esto usando el singleton pattern. Sin embargo, ten en cuenta que este patrón de diseño a menudo se usa indebidamente. También ten en cuenta que el orden o la destrucción de globales tampoco está definido, en caso de que tengas dependencias en los destructores.

+1

El orden de destrucción en realidad está bien definido: los objetos con duración de almacenamiento estática se destruyen en el orden inverso al momento en que se completó su inicialización o construcción (hay algunos matices, pero esa es la regla general). –

0

C++ carece de algo como Ada's pragma elaborate, por lo que no puede hacer suposiciones sobre las iniciaciones del pedido. Lo siento. Apesta, pero ese es el diseño.

0

¿Qué tal si la variable global estática es un puntero inicializado a nullptr. Luego, antes de que otro objeto global intente usar el objeto, verifique su creación y cree si es necesario. Esto funcionó para mí para crear un registro global de creadores de clases donde se pudieran agregar nuevas clases sin cambiar el archivo que manejaba el registro. es decir

class Factory { 
    static map<string, Creator*>* theTable; 
    static void register1(const string& string, Creator* creator); 
... 
}; 
... 
map<string, Creator*>* Factory::theTable= nullptr; 
void Factory::register1(const string& theName, Creator* creator) { 
    if (!theTable) theTable=new map<string, Creator*>; 
    (*theTable)[theName]=creator; 
} 

Este compila y se trabajó con VC++ en Visual Studio 2015.

que había intentado usar antes de este

class Factory { 
    public: 
     static map<string, Creator*> theTable; 
     static map<string, Creator*>& getTable(); 
     static void register1(const string& string, Creator* creator); 
} 
map<string, Creator*> Factory::theTable; 
map<string, Creator*>& Factory::getTable() { 
    return theTable; 
} 
void Factory::register1(const string& theString, Creator* creator) { 
    getTable()[theString]=creator; // fails if executed before theTable is created 

} 

pero todavía había tirado excepciones cuando thetable no fue creado antes de intentar para insertar una entrada en el mapa, lo que puede suceder si el registro de la clase se maneja en una unidad de compilación separada de la lógica de fábrica.

Cuestiones relacionadas