Sunday, April 1, 2012

Custom Memory Management: Replacing new and delete

I couldn't find any concise articles on custom memory management in C++ on the internet, so here's mine:

Basically, there's two ways to handle memory management: replacing operator new and operator delete, or using custom made allocators with STL containers. I will only talk about the former here. One thing you'll need to know is the difference between a new expression and operator new.

When you write:
A* a = new A; // new expression

That is a new expression. It does two things: first, it calls operator new(sizeof(A)), which allocates memory for the object A. Then, the proper constructor is called (in this case, the default constructor).
If we write:

A* a = new A(12);

This will first call operator new(sizeof(A)), then the constructor A(int), or any other constructor according to the conversion rules (it's unwise to depend too much on such conversions, as it causes confusion).


Delete does exactly the opposite, in reverse order:
delete a; // calls the destructor first, then operator delete(a) to free the memory.

Now that you know, let's move on.

Why would you want to replace operator new and delete? The most useful reason may not be efficiency, as the standard implementation is usually good enough. Since C++ does not provide garbage collection (by default), it is a good idea to check that you have equally many calls to new and delete throughout a program run. This can be done on a per class basis as follows:


// interface: A.hpp
#include <new>

class A {
public:
  // ...
#ifdef DEBUG // Only replace when compiling in debug mode
  static void* operator new(size_t);
  static void operator delete(void*);
#endif

};

// implementation: A.cpp
#ifdef DEBUG
#include <set>
int newc = 0;
int delc = 0;
int del0 = 0;
set<const A*> allocs; // store all allocations

void* A::operator new(size_t s)
{
  ++newc;
  A* ptr = static_cast<A*>(::operator new(s)); // "delegate" to global operator new
  allocs.insert(ptr);
  return ptr;
}

void A::operator delete(void* p)
{
  if (!p) {
    ++del0;
    return;
  }
  ++delc;
  allocs.erase(static_cast<const A*>(p));
  ::operator delete(p); // "delegate" to global operator delete
}

void check_memory()
{
  cerr << "A's allocated:     " << newc << "\n";
  cerr << "A's deallocated: " << delc << " \n";

  if (!allocs.empty()) {
    cerr << "Forgot to deallocate " << allocs.size() << " objects of type A\n";
    for_each(allocs.begin(),allocs.end(),[&](const A* a){
      cerr << "Unallocated: " << *a << "\n";
    });
  }
}
#endif

// in main.cpp
#include <cstdlib> // atexit


int main(int argc, char* argv[]) {
#ifdef DEBUG
  atexit(check_memory); // check memory upon exiting the program
#endif
  // ...
}


The code essentially keeps track of every call to new and delete, and stores the allocated object addresses in a set. This obviously impairs performance, so the code only replaces new and delete when ran in debug mode (that is, when the DEBUG macro is set).

There's one caveat: if you call abort(), exit(), or quick_exit(), at least objects with automatic storage will not be deallocated. This means some local objects (for instance, inside a std::vector) may not be deleted, hence giving you a wrong count even though a normal program run would not suffer from such problems.

To solve this, replace all occurrences of exit() calls with throwing an exception instead:


if (something_went_wrong) {
   // exit(1); <--- don't do this
   throw destruct_and_exit(); // <--- this will cleanup properly
}

So we need to define destruct_and_exit:


struct destruct_and_exit : public std::exception {
  destruct_and_exit() : std::exception("destruct_and_exit") {}
};

Then wrap our main() function around a catch block:


int main(int argc, int argv) 
{
  try {
#ifdef DEBUG
    atexit(check_memory); // check memory upon exiting the program
#endif
    // ...
  } catch (destruct_and_exit) {
    return 1;
  }
}


No comments:

Post a Comment