全局C ++对象初始化

时间:2011-08-04 10:42:35

标签: c++ global

许多C ++程序员都遭受了与全局C ++对象初始化/清理的激烈冲突。最终我找到了一个足够好的解决方案来解决这个问题,我多年来一直在使用它(而且很兴奋)。我的问题是:这个解决方案完全符合C ++标准,还是“平台/实现依赖”?

问题

一般来说,全局对象存在两个主要问题:

  • 他们的建造/破坏的不可预测的顺序。如果这些对象相互依赖,则会产生这种情况。
  • 构造/销毁代码在主程序入口点的CRT初始化/清理期间执行。无法用try / catch包装此代码,或执行任何初步初始化。

克服这些问题的一种方法是使用全局对象。相反,可以对这些对象使用静态/全局指针。在程序初始化期间,这些对象要么动态分配,要么实例化为入口点函数(main)中的自动变量,并且它们的指针存储在这些指针中。通过这种方式,您可以完全控制“全局”对象的生命周期。

然而,这种方法也有一些缺点。这与以下事实有关:不仅这些对象的创建/销毁不同,而且访问也不同。通常,全局对象驻留在由加载器分配的数据部分中,并且其虚拟地址在构建时是已知的。使用全局指针会导致以下缺点:

  • 稍慢的对象访问,额外的指针解除引用。在运行时期间,编译器会生成取消引用全局指针的代码,而不是假设对象位于指定的地址。
  • 优化较弱。编译器可能没有意识到指针总是指向同一个对象。
  • 如果在堆上分配实际对象:
    • 性能更差(堆分配“很重”)
    • 内存碎片
    • 内存异常的可能性
  • 如果在堆栈上分配实际对象(main中的自动变量):
    • 堆栈大小通常有限。在某些情况下,“肥胖”物品的消耗不是最理想的。

解决方案

我找到的解决方案是覆盖对象的new / delete个操作符。

// class definition
class MyObject
{
    // some members
    // ...

    static char s_pMyPlaceholder[];

public:

    // methods
    // ...

    static MyObject& Instance()
    {
        return *(MyObject*) s_pMyPlaceholder;
    }

    void* operator new (size_t) { return s_pMyPlaceholder; }
    void operator delete (void*) {}
};

// object placeholder instantiated
char MyObject::s_pMyPlaceholder[sizeof(MyObject)];

void main()
{
    // global initialization
    std::auto_ptr<MyObject> pMyObj(new MyObject);

    // run the program
    // ...

}

诀窍是在全局内存中分配足够的空间(通过声明一个足够大小的全局数组),然后对所需的对象使用虚拟内存分配,这将“分配”这个全局内存“。通过这样的我们实现以下目标:

  • 从语义上讲,我们动态分配对象。因此,我们可以完全控制其生命周期。
  • 实际上,对象位于全局内存中。因此,与“指针式”方法相关的所有缺点都不适用于我们的情况。
  • 该对象在程序中随处可见。一个人调用MyObject::Instance()来获取它的引用。而且,BTW,这个函数调用很容易被编译器内联。

所以这个方法看起来一切都好。我只是好奇它是否符合C ++标准的观点。

3 个答案:

答案 0 :(得分:3)

我发现这有两个问题,一个是合法性问题,一个是可用性问题。

第一个问题是对齐:MyObject::s_pMyPlaceholder无法保证适当对齐以容纳MyObject

第二个问题是您将自己局限于MyObject类型的单个对象。创建第二个,你已经覆盖了第一个,没有任何警告。

我建议使用boost::optional来延迟对象的初始化。

答案 1 :(得分:2)

我认为您没有正式保证您的解决方案适用于每个兼容的实现,因为C ++标准不保证静态分配的char数组与任何相同大小的对象所需的对齐。

答案 2 :(得分:1)

从3.7.3.1(分配函数,[basic.stc.dynamic.allocation])(ISO / IEC 14882/2003):

  

2 / [...]返回的指针应适当对齐,以便它可以   转换为任何完整对象类型的指针,然后用于   访问分配的存储中的对象或数组(直到存储   通过调用相应的释放来显式释放   功能)。

我怀疑您无法保证s_MyPlaceHolder[0]的地址正确对齐。

我没有看到任何错误(在单线程环境中):

#include <cstdlib>

class MyObject
{
    static MyObject* instance;

    static void release_instance() { delete instance; }

public:
    static MyObject& get_instance()
    {
        if (!instance) 
        {
            instance = new MyObject();
            std::atexit(&release_instance);
        }

        return *instance;
    }
};

除了单例和全局变量通常是一个糟糕的想法(它们倾向于将代码与这些对象结合在一起,从而加强代码部分之间的耦合)。

由于您对控制对象的生命周期感兴趣,因此可以使用RAII:

class MyObject
{
    MyObject() { ... }
    ~MyObject() { ... }

    // Never defined
    MyObject(const MyObject&);
    void operator=(const MyObject&);


    static MyObject* instance = 0;
    static void create_instance()
    {
        if (instance) throw std::logic_error("Instance already created");
        else instance = new MyObject();
    }

    static void release_instance() { delete instance; }

public:
    struct InstanceKeeper
    {
        InstanceKeeper(InstanceKeeper& x) : must_clean(x.must_clean)
        { x.must_clean = false; }

        ~InstanceKeeper() { if (must_clean) release_instance(); }

    private:
        friend class MyObject;
        InstanceKeeper() : must_clean(true) { create_instance(); }  

        bool must_clean;
    };

    friend struct InstanceKeeper;
    static InstanceKeeper instance_keeper() { return InstanceKeeper(); }  

    static MyObject& instance()
    {
        if (!instance) throw std::logic_error("Instance not created");
        return *instance;
    }
};

用法:

int main()
{ 
    MyObject::InstanceKeeper k = MyObject::instance_keeper();

    MyObject::instance().do_something();
    ...

}

您甚至可以将InstanceKeeper对象传递给函数,它与std::auto_ptr具有相同的行为。

您可能遇到的任何性能问题都是过早优化的情况。