在 Header 中重复初始化结构

Repeated Initialization of struct in Header

我正在开发一个包含 object 和函数的库,并且有一个 header 文件,这里命名为 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

我用这个 header 的目的是创建 initializer_struct object 一次;构造时,此结构会执行一些操作,为整个库设置标志和设置。其中一项操作是创建设置结构,从 XML 文件加载设置;此操作也应仅在构造 init 结构时发生一次,因此变量(此处为 path)从设置文件中保存。 super.hpp header 包含在库中的所有 object 中,因为不同的 object 用于不同的容量,即无法预测将使用哪些在一个应用程序中,所以我在所有应用程序中都包含 super.hpp header 以保证无论使用哪个 objects 都会调用它。

我的问题是:当我在主应用程序加载的多个 classes/objects 中包含 super.hpp 时,结构 init 似乎是 re-initialized 并且构造 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

然而,当我 运行 这个时,我反而得到 "Settings path = foo"。因此,我的结论是 initializer_structinit 被构造了不止一次。第一次,布尔值isInit为假,设置结构体load函数将path设置为"bar."对于所有后续初始化,isInit为真,所以load 函数没有被再次调用,似乎来自未初始化的 settings(即 path = "foo")的变量值覆盖了先前加载的值,因此 init.settings.path 的输出在 test.cpp.

这是为什么?为什么每次包含header时都构造initobject?我原以为包含守卫会阻止 header 代码被多次调用。如果我将 test.hpp 中的 init 变量设为 non-static 变量,则会创建多个副本并且输出会打印 "Doing initialization" 和 "Doing closing ops." 的多个迭代 此外,如果我取消注释 initializer_struct() 构造函数中条件语句外的 settings.load() 函数调用,然后输出给出 "bar" 的设置路径。最后,从 classA.cpp 中删除 super.hpp 的包含导致路径值为 "bar",进一步支持我的假设,即 test.hpp 的多个包含导致多个构造函数调用。

我想避免 settings.load()' called for every object that includessuper.hpp` - 这就是我将命令放在条件语句中的原因。有什么想法吗?如何确保设置文件只读一次并且加载的值不被覆盖?这是一种完全迟钝的方法来设置我的图书馆使用的一些标志和设置吗?如果是这样,您有什么建议可以让流程更简单 and/or 更优雅吗?

谢谢!

编辑:更新为包含两个 object 类 以更接近地代表我更复杂的设置

在头文件中定义了这些 static 全局对象:

static bool isInit = false;

static initializer_struct init;

这些 static 全局对象在包含此头文件的每个翻译单元中得到实例化。您将在每个翻译单元中拥有这两个对象的副本。

initializer_struct(){

不过,此构造函数只会在您的应用程序中定义一次。编译器实际上会在包含这些头文件的每个翻译单元中编译构造函数,并且在每个翻译单元中,构造函数将使用其翻译单元中的 static 全局对象。

但是,当您 link 您的应用程序时,所有翻译单元中的重复构造函数将被消除,并且只有一个构造函数实例将成为您最终可执行文件的一部分。未指定将消除哪些重复实例。 linker 会从中选出一位,这就是幸运儿。无论构造函数的实例仍然存在,它只会使用来自其自己的翻译单元的 static 全局对象。

有一种方法可以正确地做到这一点:将 static 全局对象声明为 static class 成员,然后将这些 static 全局对象实例化为一个的翻译单位。 main() 的翻译单元是一个很好的选择。那么,所有的东西都会有一个副本。

根据 Varshavchik 的建议,我做了一些修改。首先,我用我库中的所有 object 都可以扩展的非常基本的 class 替换了 super.hpp header:

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

其他文件或多或少保持不变,有一些变化。首先,classAclassB 都扩展了 base class:

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

现在,当构造任何 object 时,都会调用基础 class 构造函数,它会初始化设置和其他变量一次 isInitinit 都是 base class 的静态成员,因此所有包含 base header 的 object 或扩展 base object 可以访问它们的值,这符合我的需要。这些值通过

访问
base::init.settings.path

并且输出现在是我期望和想要的,使用 Settings path = bar 而不是 "foo"

您几乎已经掌握了,只需将静态 isInit 移动到您的 class 的静态成员并将 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;

但是你最好使用单例模式。使用单例模式,您可以确保只实例化 class 的一个实例。您可以在这里获得一些信息: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#fqa-10.12)。简而言之,如果您的 class 初始化不依赖于另一个 class 初始化,您可以采用后一种方法,因为您不知道哪个会先被初始化。