Pregunta

Esta publicación puede parecer demasiado larga para la breve pregunta al final. Pero también necesito describir un patrón de diseño que acabo de encontrar. Tal vez se usa comúnmente, pero nunca lo he visto (o tal vez simplemente no funciona :).

Primero, aquí hay un código que (a mi entendimiento) tiene un comportamiento indefinido debido al "fiasco de orden de inicialización estática". El problema es que la inicialización del español :: s_englishtospanish depende del inglés :: s_numbertostr, que se inicializan estáticos y en diferentes archivos, por lo que el orden de esas inicializaciones no está definido:

Archivo: English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    static vector<string>* s_numberToStr;
    string m_str;

    explicit English(int number)
    {
        m_str = (*s_numberToStr)[number];
    }
};

Archivo: English.cpp

#include "English.h"

vector<string>* English::s_numberToStr = new vector<string>( /*split*/
[]() -> vector<string>
{
    vector<string> numberToStr;
    numberToStr.push_back("zero");
    numberToStr.push_back("one");
    numberToStr.push_back("two");
    return numberToStr;
}());

Archivo: español.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

typedef map<string, string> MapType;

struct Spanish {
    static MapType* s_englishToSpanish;
    string m_str;

    explicit Spanish(const English& english)
    {
        m_str = (*s_englishToSpanish)[english.m_str];
    }
};

Archivo: español.cpp

#include "Spanish.h"

MapType* Spanish::s_englishToSpanish = new MapType( /*split*/
[]() -> MapType
{
    MapType englishToSpanish;
    englishToSpanish[ English(0).m_str ] = "cero";
    englishToSpanish[ English(1).m_str ] = "uno";
    englishToSpanish[ English(2).m_str ] = "dos";
    return englishToSpanish;
}());

Archivo: staticfiasco.h

#include <stdio.h>
#include <tchar.h>
#include <conio.h>

#include "Spanish.h"

int _tmain(int argc, _TCHAR* argv[])
{
    _cprintf( Spanish(English(1)).m_str.c_str() ); // may print "uno" or crash

    _getch();
    return 0;
}

Para resolver el problema del orden de inicialización estática, utilizamos el idioma de construcción en primer uso y hacemos que esas inicializaciones estáticas funcionen locales como así:

Archivo: English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    string m_str;

    explicit English(int number)
    {
        static vector<string>* numberToStr = new vector<string>( /*split*/
        []() -> vector<string>
        {
            vector<string> numberToStr_;
            numberToStr_.push_back("zero");
            numberToStr_.push_back("one");
            numberToStr_.push_back("two");
            return numberToStr_;
        }());

        m_str = (*numberToStr)[number];
    }
};

Archivo: español.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

struct Spanish {
    string m_str;

    explicit Spanish(const English& english)
    {
        typedef map<string, string> MapT;

        static MapT* englishToSpanish = new MapT( /*split*/
        []() -> MapT
        {
            MapT englishToSpanish_;
            englishToSpanish_[ English(0).m_str ] = "cero";
            englishToSpanish_[ English(1).m_str ] = "uno";
            englishToSpanish_[ English(2).m_str ] = "dos";
            return englishToSpanish_;
        }());

        m_str = (*englishToSpanish)[english.m_str];
    }
};

Pero ahora tenemos otro problema. Debido a los datos estáticos locales de funciones, ninguna de esas clases es segura. Para resolver esto, agregamos a ambas clases una variable de miembro estático y una función de inicialización para ella. Luego, dentro de esta función forzamos la inicialización de todos los datos estáticos locales de función, llamando una vez que cada función tiene datos estáticos locales de función. Por lo tanto, efectivamente estamos inicializando todo al comienzo del programa, pero aún controlando el orden de inicialización. Así que ahora nuestras clases deberían ser seguras de hilo:

Archivo: English.h

#pragma once

#include <vector>
#include <string>

using namespace std;

struct English {
    static bool s_areStaticsInitialized;
    string m_str;

    explicit English(int number)
    {
        static vector<string>* numberToStr = new vector<string>( /*split*/
        []() -> vector<string>
        {
            vector<string> numberToStr_;
            numberToStr_.push_back("zero");
            numberToStr_.push_back("one");
            numberToStr_.push_back("two");
            return numberToStr_;
        }());

        m_str = (*numberToStr)[number];
    }

    static bool initializeStatics()
    {
        // Call every member function that has local static data in it:
        English english(0); // Could the compiler ignore this line?
        return true;
    }
};
bool English::s_areStaticsInitialized = initializeStatics();

Archivo: español.h

#pragma once

#include <map>
#include <string>

#include "English.h"

using namespace std;

struct Spanish {
    static bool s_areStaticsInitialized;
    string m_str;

    explicit Spanish(const English& english)
    {
        typedef map<string, string> MapT;

        static MapT* englishToSpanish = new MapT( /*split*/
        []() -> MapT
        {
            MapT englishToSpanish_;
            englishToSpanish_[ English(0).m_str ] = "cero";
            englishToSpanish_[ English(1).m_str ] = "uno";
            englishToSpanish_[ English(2).m_str ] = "dos";
            return englishToSpanish_;
        }());

        m_str = (*englishToSpanish)[english.m_str];
    }

    static bool initializeStatics()
    {
        // Call every member function that has local static data in it:
        Spanish spanish( English(0) ); // Could the compiler ignore this line?
        return true;
    }
};

bool Spanish::s_areStaticsInitialized = initializeStatics();

Y aquí está la pregunta: ¿es posible que algún compilador pueda optimizar esas llamadas a las funciones (constructores en este caso) que tienen datos estáticos locales? Entonces, la pregunta es qué equivale exactamente a "tener efectos secundarios", lo que, a mi punto, significa que el compilador no puede optimizarlo. ¿Tener datos estáticos locales de función es suficiente para hacer que el compilador piense que la llamada de función no se puede ignorar?

¿Fue útil?

Solución

Sección 1.9 "Ejecución del programa" [Intro.Execución] del estándar C ++ 11 dice que

1 Las descripciones semánticas en este estándar internacional definen una máquina abstracta no determinista parametrizada. ... Se requieren implementaciones conformes para emular (solo) el comportamiento observable de la máquina abstracta como se explica a continuación.
...

5 Una implementación conforme que ejecuta un programa bien formado producirá el mismo comportamiento observable que una de las posibles ejecuciones de la instancia correspondiente de la máquina abstracta con el mismo programa y la misma entrada.
...

8 Los menos requisitos en una implementación conforme son:
- El acceso a los objetos volátiles se evalúa estrictamente de acuerdo con las reglas de la máquina abstracta.
- En la terminación del programa, todos los datos escritos en archivos serán idénticos a uno de los posibles resultados que la ejecución del programa de acuerdo con la semántica abstracta habría producido.
- La dinámica de entrada y salida de los dispositivos interactivos tendrá lugar de tal manera que la salida de salida en realidad se entrega antes de que un programa espere la entrada. Lo que constituye un dispositivo interactivo está definido por la implementación.
Estos colectivamente se denominan comportamiento observable Del programa.
...

12 Acceso a un objeto designado por un glValue volátil (3.10), modificando un objeto, llamando a una función de E/S de la biblioteca o llamando a una función que hace cualquiera de esas operaciones son todas efectos secundarios, que son cambios en el estado del entorno de ejecución.

Además, en 3.7.2 "Duración automática de almacenamiento" [Basic.stc.auto] se dice que

3 Si una variable con duración de almacenamiento automático tiene inicialización o un destructor con efectos secundarios, no se destruirá antes del final de su bloqueo, ni se eliminará como una optimización incluso si parece no estar utilizada, excepto que un objeto de clase o su copia/movimiento puede eliminarse como se especifica en 12.8.

12.8-31 describe la elisión de copia que creo que es irrelevante aquí.

Entonces, la pregunta es si la inicialización de sus variables locales tiene efectos secundarios que impiden que se optimice. Dado que puede realizar la inicialización de una variable estática con una dirección de un objeto dinámico, creo que produce suficientes efectos secundarios (por ejemplo, modifica un objeto). También puede agregar una operación con un objeto volátil, introduciendo así un comportamiento observable que no se puede eliminar.

Otros consejos

Ok, en pocas palabras:

  1. No puedo ver por qué los miembros estáticos de la clase deben ser públicos: son detalles de implementación.

  2. No los haga privados, sino que los haga miembros de la unidad de compilación (donde el código que implementa sus clases será).

  3. Usar boost::call_once Para realizar la inicialización estática.

La inicialización en el primer uso es relativamente fácil de hacer cumplir el orden de, es la destrucción la que es mucho más difícil de realizar en orden. Sin embargo, tenga en cuenta que la función utilizada en Call_once no debe lanzar una excepción. Por lo tanto, si puede fallar, debe dejar algún tipo de estado fallido y verificarlo después de la llamada.

(Asumiré que en su ejemplo real, su carga no es algo en lo que codifique, pero lo más probable es que cargue algún tipo de tabla dinámica, por lo que no puede simplemente crear una matriz en memoria).

¿Por qué no esconde inglés :: s_numbertostr detrás de una función estática pública y omite la sintaxis del constructor por completo? Usar DCLP para garantizar la seguridad de hilo.

Recomiendo evitar las variables estáticas de clase cuya inicialización implica efectos secundarios no triviales. Como patrón de diseño general, tienden a causar más problemas de los que resuelven. Cualesquiera que sean los problemas de rendimiento que le preocupan aquí, necesita justificación porque dudoso de que sean medibles en circunstancias del mundo real.

Tal vez deba hacer un trabajo adicional para controlar el orden de inicio. me gusta,

class staticObjects
{
    private:
    vector<string>* English::s_numberToStr;
    MapType* s_englishToSpanish;
};

static staticObjects objects = new staticObjects();

y luego defina algunas interfaces para recuperarlo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top