Jan. 20, 2007, 4:58 p.m.
posted by nogood
Item 36. Class-Specific Memory ManagementIf you don't like the way standard operator new and operator delete are treating one of your class types, you don't have to stand for it. Instead, your types can have their own operator new and operator delete customized to their needs. Note that we can't do anything with the new operator or the delete operator, since their behavior is fixed, but we can change which operator new and operator delete they invoke (see Placement New [35, 119]). The best way to do this is to declare member operator new and operator delete functions:
class Handle {
public:
//...
void *operator new( size_t );
void operator delete( void * );
//...
};
//...
Handle *h = new Handle; // uses Handle::operator new
//...
delete h; // uses Handle::operator delete
When we allocate an object of type Handle in a new expression, the compiler will first look in the scope of Handle for an operator new. If it doesn't find one, then it will use an operator new from the global scope. A similar situation holds for operator delete, so it generally makes sense to define a member operator delete if you define a member operator new, and vice versa. Member operator new and operator delete are static member functions (see Optional Keywords [63, 231]), which makes sense. Recall that static member functions have no this pointer, and these functions are charged with simply getting and releasing the storage for an object, so they have no use for a this pointer. Like other static member functions, they are inherited by derived classes:
class MyHandle : public Handle {
//...
};
//...
MyHandle *mh = new MyHandle; // uses Handle::operator new
//...
delete mh; // uses Handle::operator delete
Of course, if MyHandle had declared its own operator new and operator delete, those would have been found first by the compiler during lookup, and they would have been used instead of the inherited versions from the Handle base class. If you define member operator new and operator delete in a base class, ensure that the base class destructor is virtual:
class Handle {
public:
//...
virtual ~Handle();
void *operator new( size_t );
void operator delete( void * );
//...
};
class MyHandle : public Handle {
//...
void *operator new( size_t );
void operator delete( void *, size_t ); // note 2nd arg
//...
};
//...
Handle *h = new MyHandle; // uses MyHandle::operator new
//...
delete h; // uses MyHandle::operator delete
Without a virtual destructor, the effect of deleting a derived class object through a base class pointer is undefined! The implementation may simply (and probably incorrectly) invoke Handle::operator delete rather than MyHandle::operator delete, but anything at all could happen. Notice also that we've employed a two-argument version of operator delete rather than the usual one-argument version. This two-argument version is another "usual" version of member operator delete often employed by base classes that expect derived classes to inherit their operator delete implementation. The second argument will contain the size of the object being deletedinformation that is often useful in implementing custom memory management. A common misconception is that use of the new and delete operators implies use of the heap (or freestore) memory, but this is not the case. The only implication in using the new operator is that a function called operator new will be called and that function will return a pointer to some memory. The standard, global operator new and operator delete do indeed allocate memory from the heap, but member operator new and operator delete can do whatever they like. There is no restriction as to where that memory comes from; it may come from a special heap, from a statically allocated block, from the guts of a standard container, or from a block of storage local to a function. The only limit to where the memory comes from is your creativity and common sense. For example, Handle objects could be allocated from a static block like this:
struct rep {
enum { max = 1000 };
static rep *free; // head of freelist
static int num_used; // number of slots used
union {
char store[sizeof(Handle)];
rep *next;
};
};
static rep mem[ rep::max ]; // block of static storage
void *Handle::operator new( size_t ) {
if( rep::free ) { // if something on freelist
rep *tmp = rep::free; // take from freelist
rep::free = rep::free->next;
return tmp;
}
else if( rep::num_used < rep::max ) // if slots left
return &mem[ rep::num_used++ ]; // return unused slot
else // otherwise, we're...
throw std::bad_alloc(); // ...out of memory!
}
void Handle::operator delete( void *p ) { // add to freelist
static_cast<rep *>(p)->next = rep::free;
rep::free = static_cast<rep *>(p);
}
A production-quality version of this implementation would take care to be more robust in out-of-memory conditions, deal with types derived from Handle and arrays of Handles, and so on, but this simple code nevertheless shows that new and delete don't necessarily have to deal with heap memory. |
- Comment