【问题标题】:Repeated Initialization of struct in HeaderHeader 中结构的重复初始化
【发布时间】:2016-08-13 23:57:56
【问题描述】:

我正在开发一个对象和函数库,并有一个头文件,这里命名为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_structinit 不止一次被构建。第一次,布尔值isInit 为假,设置结构load 函数将path 设置为“bar”。对于所有后续初始化,isInit 为真,因此不会再次调用 load 函数,并且似乎来自未初始化 settings(即path = "foo")的变量值会覆盖先前加载的值,因此输出init.settings.path 中的 test.cpp

这是为什么?为什么每次包含标头时都会构造init 对象?我原以为包含守卫会阻止标题代码被多次调用。如果我将test.hpp 中的init 变量设为非静态变量,则会创建多个副本,并且输出会打印“正在执行初始化”和“正在执行关闭操作”的多次迭代。此外,如果我在initializer_struct() 构造函数的条件语句之外取消注释settings.load() 函数调用,则输出会给出“bar”的设置路径。最后,从classA.cpp 中删除包含super.hpp 会导致路径值为“bar”,这进一步支持了我的假设,即多次包含test.hpp 会导致多次构造函数调用。

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

谢谢!

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

【问题讨论】:

    标签: c++ struct header inclusion


    【解决方案1】:

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

    static bool isInit = false;
    
    static initializer_struct init;
    

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

    initializer_struct(){
    

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

    但是,当您链接您的应用程序时,所有翻译单元中的重复构造函数将被消除,并且只有一个构造函数实例将成为您最终可执行文件的一部分。未指定将消除哪些重复实例。链接器将选择其中一个,这就是幸运的赢家。无论构造函数的实例是什么,它都只会使用来自它自己的翻译单元的static 全局对象。

    有一种正确的方法:将static 全局对象声明为static 类成员,然后在其中一个翻译单元中实例化这些static 全局对象。带有main() 的翻译单元是一个很好的选择。然后,所有内容都会有一个副本。

    【讨论】:

    • 我有点转身了;我应该将全局对象声明为哪个类的静态成员?如果我让他们成为班级成员,他们会不会不再是全球性的?
    • 不,那是非static 类成员,它们是每个类的每个实例的一部分。 static 班级成员是全球性的。有关static 类成员的更深入讨论,以及声明和定义它们的正确方法,请参阅您最喜欢的 C++ 书籍。
    • 啊,当然。但这仍然引出了一个问题,我会让他们成为哪个班级的成员?当前实现的美妙之处(嗯,几乎,因为它不太好用)是我不必在每个 main 函数中实例化特定的“初始化”对象。
    • 没有必要“在每个主要功能中”都这样做。静态类成员只需要在一个翻译单元中定义。
    • 想象一下我还有很多课程,classBclassC 等;每一个都包括test.hpp。我还有各种脚本,每个都有自己的主要功能,其中包括这些类的一些子集。在我看来,按照您的方法,我需要在每个脚本/主函数中实例化 initializer_struct 对象。如果我在其中一个类中实例化它,比如classB,那么不包含classB 的脚本根本不会构造该对象。感谢您的帮助和耐心。
    【解决方案2】:

    按照 Varshavchik 的建议,我做了一些更改。首先,我将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;
        }
    }
    

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

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

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

    访问
    base::init.settings.path
    

    现在的输出是我所期望和想要的,使用Settings path = bar 而不是“foo”

    【讨论】:

      【解决方案3】:

      您几乎拥有它,只需将静态 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#fqa-10.12)。简而言之,如果你的类初始化不依赖于另一个类的初始化,你可以使用后一种方法,因为你不知道哪个会先初始化。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-26
        • 2020-12-14
        • 1970-01-01
        • 2016-06-23
        • 1970-01-01
        相关资源
        最近更新 更多