Global C++ object initialization
Many C++ programmers have suffered from the fierce clashes with the global C++ objects initialization/cleanu开发者_如何学运维p. 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.
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.
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.
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.
精彩评论