c++structheaderinclusion

Repeated Initialization of struct in Header


I'm developing a library of objects and functions and have a header file, here named super.hpp, that contains some initialization tasks.

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

My intention with this header is to create the initializer_struct object once; when it is constructed, this struct performs a few actions that set flags and settings for the entire library. One of these actions is the creation of settings struct that loads settings from an XML file; this action should also occur only once when the init struct is constructed, so the variables (here, path) are saved from the settings file. The super.hpp header is included in all objects in the library because different objects are used in different capacities, i.e., there's no way to predict which ones will be used in an application, so I include the super.hpp header in all of them to guarantee it is called no matter which objects are used.

My problem is this: when I include super.hpp in multiple classes/objects that are all loaded by the main application, the struct init appears to be re-initialized and the variables set when the settings_struct is constructed are overwritten with the default values. To see this in action, consider these additional files:

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; }

To compile and run the example,

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

I expect the output from test.out to be the following:

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

However, when I run this, I instead get "Settings path = foo". Thus, my conclusion is that the initializer_struct, init, is being constructed more than once. The first time, the boolean isInit is false, and the settings structure load function sets path to "bar." For all subsequent initializations, isInit is true, so the load function is not called again and it seems that the variable values from the uninitialized settings (i.e., path = "foo") overwrite the previously loaded values, hence the output of init.settings.path in test.cpp.

Why is this? Why is the init object constructed every time the header is included? I would have thought the include guards would keep the header code from being called multiple times. If I make the init variable in test.hpp a non-static variable, then multiple copies are created and the output prints multiple iterations of "Doing initialization" and "Doing closing ops." Additionally, if I uncomment the settings.load() function call outside the conditional statement in the initializer_struct() constructor, then the output gives a settings path of "bar". Finally, removing the inclusion of super.hpp from classA.cpp results in a path value of "bar", further supporting my hypothesis that multiple inclusions of test.hpp result in multiple constructor calls.

I would like to avoid having settings.load()' called for every object that includessuper.hpp` - that's why I placed the command within the conditional statement. Any thoughts? How to I make sure the settings file is read only once and that the values loaded are not overwritten? Is this a completely obtuse method to set some flags and settings that my library uses? If so, do you have any suggestions to make the processes simpler and/or more elegant?

Thanks!

Edit: Updated to include two object classes to closer represent my more complex setup


Solution

  • Following the suggestions of Varshavchik, I have made a few changes. First, I have replaced the super.hpp header with a very basic class that all objects in my library can extend:

    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;
        }
    }
    

    The other files remain more or less the same, with a few changes. First, both classA and classB extend the base class:

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

    Now, when any of the objects are constructed, the base class constructor is called, which initializes the settings and other variables once. Both isInit and init are static members of the base class, so all objects that include the base header or extend the base object have access to their values, which fits my needs. These values are accessed via

    base::init.settings.path
    

    and the output is now what I expect and want it to be, with Settings path = bar instead of "foo"