Я занимаюсь разработкой библиотеки объектов и функций, и у меня есть файл заголовка, названный здесь 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 includes
super.hpp` — поэтому я поместил команду в условном выражении. Какие-нибудь мысли? Как убедиться, что файл настроек читается только один раз и что загруженные значения не перезаписываются? Это абсолютно тупой метод для установки некоторых флагов и настроек, которые использует моя библиотека? Если да, есть ли у вас какие-либо предложения, чтобы сделать процессы более простыми и / или более элегантными?
Спасибо!
редактировать: Обновлено, чтобы включить два класса объектов, чтобы ближе представить мою более сложную установку
В вашем заголовочном файле вы определяете эти static
глобальные объекты:
static bool isInit = false;
static initializer_struct init;
Эти static
Глобальные объекты создаются в каждом модуле перевода, который включает этот заголовочный файл. У вас будет копия этих двух объектов в каждой единице перевода.
initializer_struct(){
Этот конструктор, тем не менее, будет определен в вашем приложении только один раз. Компилятор фактически скомпилирует конструктор в каждом модуле перевода, который включает эти заголовочные файлы, и в каждом модуле перевода конструктор будет использовать static
глобальный объект из его единицы перевода.
Однако при связывании приложения дублирующие конструкторы во всех единицах перевода будут удалены, и только один экземпляр конструктора станет частью вашего окончательного исполняемого файла. Не указано, какие дубликаты будут удалены. Линкер выберет одного из них, и это счастливый победитель. Какой бы экземпляр конструктора не остался, он будет использовать только static
глобальные объекты из собственного модуля перевода.
Есть способ сделать это правильно: объявить static
глобальные объекты как static
вместо этого члены класса, а затем создать экземпляр этих static
глобальные объекты в одном из блоков перевода. Блок перевода с вашим main()
это отличный выбор. Тогда будет единственная копия всего.
Следуя предложениям Варшавчика, я внес несколько изменений. Во-первых, я заменил 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 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 дизайн шаблона
Это будет выглядеть так:
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#fqa-10.12). Короче говоря, вы можете использовать последний подход, если инициализация вашего класса НЕ зависит от инициализации другого класса, поскольку вы не знаете, какой из них будет инициализирован первым.