全球 C++对象初始化

发布于 2024-11-27 21:27:46 字数 1982 浏览 1 评论 0原文

许多 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
    // ...

}

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

  • 从语义上讲,我们动态分配对象,因此我们可以完全控制
  • 对象的生命周期。因此,与“指针方式”方法相关的所有缺点都不适用于。我们的案例。
  • 该对象在程序中的任何地方都是可见的。顺便说一句,这个函数调用很容易被编译器内联,

因此一切看起来都很好。我只是好奇从 C++ 标准的角度来看它是否合法。

Many C++ programmers have suffered from the fierce clashes with the global C++ objects initialization/cleanup. Eventually I've found a good enough solution to this problem, I've been using (and anjoying) it for years now. My question is: does this solution fully comply to the C++ standard, or it's "platform/implementation-dependent"?

The problem

Generally-speaking there are two major problems with global objects:

  • Unpredictable order of their construction/destruction. This bites if those objects depend on each other.
  • The construction/destruction code is executed during the CRT initialization/cleanup outside the main program entry point. There's no way to wrap this code with try/catch, or perform any preliminary initialization.

One way to overcome those issues is not using global objects at all. Instead one may use static/global pointers to those objects. During the program initialization those objects are either allocated dynamically or instantiated as automatic variables within the entry point function (main), and their pointers stored in those pointers. By such you have the full control over your "global" objects lifetime.

However this method also has some drawbacks. It's related to the fact that not only the creation/destruction of those objects is different, but also their access is different. Normally a global object resides in the data section, which is allocated by the loader, and its virtual address is known at the build time. Using global pointers leads to the following drawbacks:

  • Somewhat slower object access, extra pointer dereferencing. During the runtime instead of assuming the object is at the specified address the compiler generates the code that dereferences the global pointer.
  • Weaker optimizations. The compiler may not realize that the pointer always points to the same object.
  • If the actual objects are allocated on heap:
    • Worse performance (heap allocations are "heavy")
    • Memory fragmentation
    • Chance of out-of-memory exception
  • If the actual objects are allocated on stack (auto variables in main):
    • Stack size is usually limited. In some circumstances its consumption by "fat" objects is suboptimal.

The solution

The solution I've found is to override the object's new/delete operatiors.

// 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
    // ...

}

The trick is to allocate enough space in the global memory (by declaring a global array of the adequate size), and then use fictive memory allocation for the needed object, that will "allocate" this global memory". By such we achieve the following:

  • Semantically we allocate the object dynamically. Hence we have the full control over its lifetime.
  • Actually the object resides in the global memory. Hence all the drawbacks related to the "pointer-wise" method are inapplicable to our case.
  • The object is visible everywhere in the program. One calls MyObject::Instance() to get the reference to it. And, BTW, this function call is easily inlined by the compiler.

So that everything seems ok with this method. I'm just curious if it's legal from the C++ standard perspective.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

迷乱花海 2024-12-04 21:27:46

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

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

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

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

I see two problems with this, one a legality problem and one a usability problem.

The first problem is alignment: MyObject::s_pMyPlaceholder is not guaranteed to be suitably aligned to hold a MyObject.

The second problem is that you have restricted yourself to a single object of type MyObject. Create a second and you have overwritten the first with no warning.

I would suggest using boost::optional to delay initialisation of objects.

相对绾红妆 2024-12-04 21:27:46

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

I don't think you have a formal guarantee that your solution works on every compliant implementation, because the C++ standard doesn't guarantee that statically allocated arrays of char are aligned as would be required by any object of the same size.

香草可樂 2024-12-04 21:27:46

从 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。

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

From 3.7.3.1 (Allocation functions, [basic.stc.dynamic.allocation]) (ISO/IEC 14882/2003):

2/ [...] The pointer returned shall be suitably aligned so that it can be
converted to a pointer of any complete object type and then used to
access the object or array in the storage allocated (until the storage
is explicitly deallocated by a call to a corresponding deallocation
function).

My doubt is that you cannot guarantee portably that the address of s_MyPlaceHolder[0] is aligned correctly.

I don't see anything bad (in a single threaded environment) with:

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

except that singletons and globals are usually a poor idea (they tend to marry your code with the presence of such objects, which tightens coupling between parts of your code).

Since you're interested in controlling the lifetime of the object, you can use 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;
    }
};

Usage:

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

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

}

You can even pass the InstanceKeeper object around to functions, it has the same behavior as std::auto_ptr.

Any performance concerns that you may have are cases of premature optimization.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文