Introduction
When working with languages such as C++ that allow you access to the OS memory, you almost certainly run into situations where the standard memory model does not suit your needs. I want to describe a technique here that might come in handy in situations where you need to have more control over object allocations.
In my particular situation, I needed to create my objects in a static memory area, namely a pre-created array. The reason for that was a badly designed server application that relied on dynamic object creation, which led to memory fragmentation problems. I couldn't introduce too many changes into the existing code, since it would lead to a lot of retesting for which we didn't have the time nor the resources, hence I opted for a less invasive approach. This is the solution I want to describe in this article.
Class level new and delete overrides
As the title of the article states, I used templates and class level new/delete overrides to accomplish this. Let's take a look at how C++ performs object allocation for a moment. At its core, the C++ runtime uses some variation of malloc
and free
to request memory from the Operating System when new
and delete
are called, respectively. Since we can override those operators on the class level, we can control the allocation process of an object of a particular class. Knowing that, we can implement a solution that will allow us to intercept memory allocations and manage them ourselves.
The static member problem
One key aspect of simply overriding new
and delete
on a class level is that those methods are implicitly static, hence, if we want to reuse (through inheritance) our memory allocation class in another set of unrelated classes, we may end up routing calls to the same memory area, which most likely is not what is needed. I call these the static member problem.
Let's take a look at an example.
The slide above shows a possible scenario, where a base class that implements the necessary memory management routines is inherited by two child classes that are not necessarily related. The problem is that since new
and delete
are implicitly static, any member that they access are required to be static as well. In our case, if the memory manager class uses some member as a memory store, that member in turn needs to be static as well, and this will lead to any class that inherits from our memory manager class to use the same member as its store as well - not necessarily what we want.
Templates and the static member problem
As described above, simply overriding new
and delete
on a class level will at best provide an ad-hoc solution, because of the static member limitation described above. There is however a way of creating a generalized version of this solution - we can use templates to overcome this limitation.
A C++ template is a specification of a class, a sort of a proto class. Templates specify the intents of the class and the algorithms that the class implements without imposing the specific types that those algorithms are going to operate on. Templates are full fledged compile and runtime entities - during compilation time, the compiler can detect errors related to incorrect type manipulation because it is able to resolve the types when the template class is instantiated. The compiler generates concrete instances for every parameter combination and by the time the linker picks up the code, it already is working with concrete classes just as if you'd defined them manually in your code. We also say that they are runtime entities, because by the time the code is executed, the objects created in memory are of the types generated out of the instantiation of your template with a particular template parameter combination.
In other words, every time you instantiate a template in your code with a different set of template parameters, the compiler generates a concrete class based on that instantiation, hence at run time, every instantiation of an object of a particular template instance is an object of a completely different type.
Using templates and new and delete overrides together
We know that static members are class level entities, meaning that a static member is shared across all the objects of a particular type. Since we generate a different class type for every template parameter combination, we are effectively eliminating the static member problem. We can effectively use templates to create a different instance of the memory manager class for every class that requires to use it. Let's take a look at a possible implementation:
Notice the use of the CAllocationProxy
template in the above graphic, which is used to route calls to the memory manager class.
And here is the code for the CAllocationProxy
class:
#ifndef ALLOCATION_PROXY_H
#define ALLOCATION_PROXY_H
#include <new>
template < typename TPool, typename TClass >
class CAllocationProxy
{
public:
CAllocationProxy()
{
};
virtual
~CAllocationProxy()
{
};
static void
InitProxyInstance()
{
};
static
void* operator new ( size_t nSize )
{
void* p = NULL;
return p;
};
static
void operator delete ( void *p, size_t nSize ) throw()
{
if ( !p )
{
return; }
};
private:
static
TPool m_Pool;
};
template< typename TPool, typename TClass >
TPool CAllocationProxy<TPool,TClass>::m_Pool;
#endif // ALLOCATION_PROXY_H
The above class takes two template parameters, TPool
and TClass
.
TPool
is our memory allocator - the static member we've been talking about in the previous sections. The type passed in itself is not relevant here, however I ended up writing my own memory manager that I would pass as parameter there. TClass
is the type of objects that would be allocated by the memory manager. It is not used in the above code, however it could be used to pass the size of the object to be allocated to the memory manager.
The rest is pretty self explanatory.
Let's take a look at how this code can be used:
Pseudocode:
class A : public CAllocationProxy<MemoryManager, A> {
}
A::InitProxyInstance();
A myA = new A();
....
delete myA;
Conclusion
I hope that this technique is of use to anyone in the same situation as I was in - where the amount of changes led to by other solutions were not acceptable. However, I also believe that this is not the only reason to use this particular approach. I believe that it offers more control over the code than other, more traditional approaches - such as placement new or some sort of static create method, which could potentially allow you to achieve a similar effect. Besides that, this will allow you to reuse the code across many different objects and memory managers.
One word of caution, beware of class level new
and delete
macro redefines. Many runtimes and frameworks will override new
and delete
with macros, to intercept those calls in order to allow more control and enhance error reporting/catching - usually this is only done in debug builds. To fix it, use something like the following code:
#ifdef new
#define __new new
#undef new
#endif
#ifdef delete
#define __delete delete
#undef delete
#endif
....
...
#ifdef __new
#define new __new
#undef __new
#endif
#ifdef __delete
#define delete __delete
#undef __delete
#endif
Thanks for reading!