Повторная инициализация структуры в заголовке

Я занимаюсь разработкой библиотеки объектов и функций, и у меня есть файл заголовка, названный здесь super.hpp, который содержит некоторые задачи инициализации.

super.hpp

#ifndef H_INIT
#define H_INIT

#include <iostream>
#include <string>

static bool isInit = false;

struct settings_struct{
    std::string path = "foo";
    void load(){ path = "bar"; }
};

struct initializer_struct{
    settings_struct settings;

    initializer_struct(){
        if(!isInit){
            std::cout << "Doing initialization\n";
            settings.load();
            isInit = true;
        }
        // settings.load();
    }//====================

    ~initializer_struct(){
        if(isInit){
            std::cout << "Doing closing ops\n";
            isInit = false;
        }
    }
};

static initializer_struct init; // static declaration: only create one!

#endif

Мое намерение с этим заголовком состоит в том, чтобы создать initializer_struct возражать один раз; когда она построена, эта структура выполняет несколько действий, которые устанавливают флаги и настройки для всей библиотеки. Одним из этих действий является создание структуры настроек, которая загружает настройки из файла XML; это действие также должно происходить только один раз при создании структуры init, поэтому переменные (здесь path) сохраняются из файла настроек. super.hpp заголовок включен во все объекты в библиотеке, потому что разные объекты используются в разных мощностях, т. е. нет способа предсказать, какие из них будут использоваться в приложении, поэтому я включаю super.hpp заголовок во всех них, чтобы гарантировать, что это называется независимо от того, какие объекты используются.

Моя проблема заключается в следующем: когда я включаю super.hpp в нескольких классах / объектах, которые все загружены основным приложением, структура init кажется, что инициализируется заново, и переменные устанавливаются, когда settings_struct построен перезаписаны значениями по умолчанию. Чтобы увидеть это в действии, рассмотрите следующие дополнительные файлы:

test.cpp

#include "classA.hpp"
#include "classB.hpp"
#include <iostream>

int main(int argc, char *argv[]){
    (void) argc;
    (void) argv;

    classA a;
    classB b;

    std::cout << "Settings path = " << init.settings.path << std::endl;
    std::cout << "Class A Number = " << a.getNumber() << std::endl;
    std::cout << "Class B Number = " << b.getInteger() << std::endl;
}

classA.hpp

#ifndef H_CLASSA
#define H_CLASSA

class classA{
private:
    double number;

public:
    classA() : number(7) {}
    double getNumber();
};

#endif

classA.cpp

#include "super.hpp"
#include "classA.hpp"

double classA::getNumber(){ return number; }

classB.hpp

#ifndef H_CLASSB
#define H_CLASSB

class classB{
private:
    int number;

public:
    classB() : number(3) {}
    int getInteger();
};

#endif

classB.cpp

#include "super.hpp"
#include "classB.hpp"

int classB::getInteger(){ return number; }

Чтобы скомпилировать и запустить пример,

g++ -std=c++11 -W -Wall -Wextra -Weffc++ -pedantic -c classA.cpp -o classA.o
g++ -std=c++11 -W -Wall -Wextra -Weffc++ -pedantic -c classB.cpp -o classB.o
g++ -std=c++11 -W -Wall -Wextra -Weffc++ -pedantic classA.o classB.o test.cpp -o test.out
./test.out

Я ожидаю, что вывод test.out будет следующим:

Doing initialization
Settings path = bar
Number = 7
Doing closing ops

Однако, когда я запускаю это, я вместо этого получаю "Параметры пути = foo". Таким образом, мой вывод заключается в том, что initializer_struct, init, строится более одного раза. Первый раз, логическое isInit ложно, и структура настроек load наборы функций path в бар." Для всех последующих инициализаций isInit верно, поэтому load функция не вызывается снова и кажется, что значения переменных из неинициализированных settings (То есть, path = "foo") перезаписать ранее загруженные значения, следовательно вывод init.settings.path в test.cpp,

Почему это? Почему init объект создается каждый раз, когда заголовок включен? Я бы подумал, что охранники включения будут препятствовать тому, чтобы код заголовка вызывался несколько раз. Если я сделаю init переменная в test.hpp нестатическая переменная, затем создается несколько копий, и выходные данные выводят несколько итераций "Выполнение инициализации" и "Выполнение операций закрытия". Кроме того, если я раскомментирую settings.load() вызов функции вне условного оператора в initializer_struct() конструктор, то вывод дает путь настройки "бар". Наконец, снятие включения super.hpp от classA.cpp приводит к значению пути "bar", что еще больше подтверждает мою гипотезу о том, что множественные включения test.hpp привести к нескольким вызовам конструктора.

Я хотел бы избежать settings.load()' called for every object that includessuper.hpp` - поэтому я поместил команду в условном выражении. Какие-нибудь мысли? Как убедиться, что файл настроек читается только один раз и что загруженные значения не перезаписываются? Это совершенно тупой метод для установки некоторых флагов и настроек, которые использует моя библиотека? Если да, есть ли у вас какие-либо предложения, чтобы сделать процессы более простыми и / или более элегантными?

Спасибо!

Редактировать: Обновлено, чтобы включить два класса объектов, чтобы ближе представить мои более сложные настройки

3 ответа

Решение

Следуя предложениям Варшавчика, я внес несколько изменений. Во-первых, я заменил super.hpp заголовок с очень базовым классом, который могут расширять все объекты в моей библиотеке:

base.hpp

#ifndef H_BASE
#define H_BASE

#include <iostream>
#include <string>

struct settings_struct{
    settings_struct(){
        std::cout << "Constructing settings_struct\n";
    }
    std::string path = "foo";
    void load(){ path = "bar"; }
};

struct initializer_struct{
    settings_struct settings;

    initializer_struct(){
        std::cout << "Constructing initializer_struct\n";
    }

    ~initializer_struct(){
        std::cout << "Doing closing ops\n";
    }

    void initialize(){
        std::cout << "Doing initialization\n";
        settings.load();
    }
};

class base{
public:
    static initializer_struct init;
    static bool isInit;

    base();
};

#endif

base.cpp

#include "base.hpp"

initializer_struct base::init;
bool base::isInit = false;

base::base(){
    if(!isInit){
        init.initialize();
        isInit = true;
    }
}

Другие файлы остаются более или менее такими же, с некоторыми изменениями. Во-первых, оба classA а также classB продлить base учебный класс:

class classA : public base {...}
class classB : public base {...}

Теперь, когда любой из объектов создается, вызывается конструктор базового класса, который инициализирует настройки и другие переменные один раз. И то и другое isInit а также init являются статическими членами base класс, поэтому все объекты, которые включают в себя base заголовок или расширить base Объект имеет доступ к своим ценностям, что соответствует моим потребностям. Эти значения доступны через

base::init.settings.path

и теперь результат такой, какой я ожидаю и хочу, Settings path = bar вместо "фу"

В вашем заголовочном файле вы определяете эти static глобальные объекты:

static bool isInit = false;

static initializer_struct init;

Эти static Глобальные объекты создаются в каждом модуле перевода, который включает этот заголовочный файл. У вас будет копия этих двух объектов в каждой единице перевода.

initializer_struct(){

Этот конструктор, тем не менее, будет определен в вашем приложении только один раз. Компилятор фактически скомпилирует конструктор в каждом модуле перевода, который включает эти заголовочные файлы, и в каждом модуле перевода конструктор будет использовать static глобальный объект из его единицы перевода.

Однако при связывании приложения дублирующие конструкторы во всех единицах перевода будут удалены, и только один экземпляр конструктора станет частью вашего окончательного исполняемого файла. Не указано, какие дубликаты будут удалены. Линкер выберет одного из них, и это счастливый победитель. Какой бы экземпляр конструктора не остался, он будет использовать только static глобальные объекты из собственного модуля перевода.

Есть способ сделать это правильно: объявить static глобальные объекты как static вместо этого члены класса, а затем создать экземпляр этих static глобальные объекты в одном из блоков перевода. Блок перевода с вашим main() это отличный выбор. Тогда будет единственная копия всего.

У вас почти это есть, просто переместите static isInit, чтобы он был статическим членом вашего класса, и перенесите создание экземпляра init в модуль перевода. Это будут следующие файлы:

super.hpp

#ifndef H_INIT
#define H_INIT

#include <iostream>
#include <string>

struct initializer_struct{
    static bool isInit;

    struct settings_struct{
        std::string path = "foo";
        void load(){ path = "bar"; }
    } settings;

    initializer_struct(){
        if(!isInit){
            std::cout << "Doing initialization\n";
            settings.load();
            isInit = true;
        }
        // settings.load();
    }//====================

    ~initializer_struct(){
        if(isInit){
            std::cout << "Doing closing ops\n";
            isInit = false;
        }
    }
};

extern initializer_struct init; // extern declaration, instantiate it in super.cpp

#endif

super.cpp

#include "super.hpp"

bool initializer_struct::isInit = false;
initializer_struct init;

Однако вам лучше использовать одноэлементный шаблон. Используя шаблон синглтона, вы убедитесь, что создан только один экземпляр вашего класса. Вы можете получить немного информации здесь: C++ Singleton design pattern

Это будет выглядеть так:

singleton.hpp

#pragma once

class initializer_struct{
public:
    struct settings_struct{
        std::string path = "foo";
        void load(){ path = "bar"; }
    } settings;

    static initializer_struct *GetInstance() {
        if (_instance == NULL) {
            _instance = new initializer_struct();
        }
        return _instance;
    }
    ~initializer_struct(){
    }    
private:
    initializer_struct(){
        if(!isInit){
            std::cout << "Doing initialization\n";
            settings.load();
            isInit = true;
        }
    }

    static initializer_struct *_instance;
}

singleton.cpp

#include "singleton.hpp"

initializer_struct *initializer_struct::_instance = NULL;

Вы могли бы даже выполнить инициализацию при загрузке, изменив _instance с указателя на просто объект, объявив его как объект в singleton.cpp и изменив прототип GetInstance() на:

initializer_struct &GetInstance() { return _instance; }

Однако с последним следует остерегаться статического порядка инициализации фиаско ( http://yosefk.com/c++fqa/ctors.html). Короче говоря, вы можете использовать последний подход, если инициализация вашего класса НЕ зависит от инициализации другого класса, поскольку вы не знаете, какой из них будет инициализирован первым.

Другие вопросы по тегам