全局c++对象初始化

Global C++ object initialization

本文关键字:初始化 对象 c++ 全局      更新时间:2023-10-16

许多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()来获取对它的引用。而且,顺便说一句,这个函数调用很容易被编译器内联。

所以这个方法看起来很好。我只是好奇,从c++标准的角度来看,这是否合法。

我看到了两个问题,一个是合法性问题,一个是可用性问题。

第一个问题是对齐:MyObject::s_pMyPlaceholder不能保证适当地对齐以容纳MyObject

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

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

我不认为你有一个正式的保证,你的解决方案适用于每一个兼容的实现,因为c++标准并不能保证静态分配的char数组是对齐的,这将是任何相同大小的对象所需要的。

从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相同的行为。

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