I l@ve RuBoard |
![]() ![]() |
4.7 A Hat TrickThe last layer of our architecture consists of SmallObject, the base class that conveniently wraps the functionality provided by SmallObjAllocator. SmallObject overloads operator new and operator delete. This way, whenever you create an object derived from SmallObject, the overloads enter into action and route the request to the fixed allocator object. The definition of SmallObject is quite simple, if a bit intriguing. class SmallObject { public: static void* operator new(std::size_t size); static void operator delete(void* p, std::size_t size); virtual ~SmallObject() {} }; SmallObject looks perfectly kosher, except for a little detail. Many C++ books, such as Sutter (2000), explain that if you want to overload the default operator delete in a class, operator delete must take a pointer to void as its only argument. C++ offers a kind of a loophole that is of great interest to us. (Recall that we designed SmallObjAllocator to take the size of the block to be freed as an argument.) In standard C++ you can overload the default operator delete in two ways—either as void operator delete(void* p); or as void operator delete(void* p, std::size_t size); This issue is thoroughly explained in Sutter (2000), page 144. If you use the first form, you choose to ignore the size of the memory block to deallocate. But we badly need that block size so that we can pass it to SmallObjAlloc. Therefore, SmallObject uses the second form of overloading operator delete. How does the compiler provide the object size automagically? It seems as if the compiler is adding all by itself a per-object memory overhead that we have tried to avoid all through this chapter. No, there is no overhead at all. Consider the following code: class Base { int a_[100]; public: virtual ~Base() {} }; class Derived : public Base { int b_[200]; public: virtual ~Derived() {} }; ... Base* p = new Derived; delete p; Base and Derived have different sizes. To avoid the overhead of storing the size of the actual object to which p points, the compiler does a hat trick: It generates code that figures out the size on the fly. Four possible techniques of achieving that are listed here. (Wearing a compiler writer hat from time to time is fun—you suddenly can do little miracles inaccessible to programmers.)
(Compiler writer hat off.) As you see, the compiler makes quite an effort in passing the appropriate size to your operator delete. Why, then, ignore it and perform a costly search each time you deallocate an object? It all dovetails so nicely. SmallObjAllocator needs the size of the block to de allocate. The compiler provides it, and SmallObject forwards it to FixedAllocator. Most of the solutions listed assume you defined a virtual destructor for Base, which explains again why it is so important to make all of your polymorphic classes' destructors virtual. If you fail to do this, deleteing a pointer to a base class that actually points to an object of a derived class engenders undefined behavior. The allocator discussed herein will assert in debug mode and crash your program in NDEBUG mode. Anybody would agree that this behavior fits comfortably into the realm of "undefined." To protect you from having to remember all this (and from wasting nights debugging if you don't), SmallObject defines a virtual destructor. Any class that you derive from SmallObject will inherit its virtual destructor. This brings us to the implementation of SmallObject. We need a unique SmallObjAllocator object for the whole application. That SmallObjAllocator must be properly constructed and properly destroyed, which is a thorny issue on its own. Fortunately, Loki solves this problem thoroughly with its SingletonHolder template, described in Chapter 6. (Referring you to subsequent chapters is a pity, but it would be even more pitiful to waste this reuse opportunity.) For now, just think of SingletonHolder as a device that offers you advanced management of a unique instance of a class. If that class is X, you instantiate Singleton<X>. Then, to access the unique instance of that class, you call Singleton<X>::Instance(). The Singleton design pattern is described in Gamma et al. (1995). Using SingletonHolder renders SmallObject's implementation extremely simple: typedef Singleton<SmallObjAllocator> MyAlloc; void* SmallObject::operator new(std::size_t size) { return MyAlloc::Instance().Allocate(size); } void SmallObject::operator delete(void* p, std::size_t size) { MyAlloc::Instance().Deallocate(p, size); } ![]() |
I l@ve RuBoard |
![]() ![]() |