🚀 go-pugleaf

RetroBBS NetNews Server

Inspired by RockSolid Light RIP Retro Guy

Article View: comp.sys.mac.programmer.codewarrior
Article #659

The world's simplest C++ memory debugger

#659
From: Howard Hinnant
Date: Wed, 24 Sep 2003 22:50
500 lines
16659 bytes
This post is along the lines of a MSL C++ usage tip.  This is a way to 
build a very simple memory debugger from some of the extensions found in 
MSL C++ (the Metrowerks implementation of the C++ standard library).

Although it can be built using earlier versions of CodeWarrior with some 
work, the article that follows takes advantage of some features of our 
latest CodeWarrior Development Studio for Mac, version 9 product (hint, 
hint, ;-) ).

So what is so special about a /very/ simple memory debugger?  Since it 
is so simple, it is easy to understand and adapt to your own needs.

What will it do for you?

   * It will detect whole program memory leaks.
   * It will detect "delete on new[]" or "delete[] on new" mismatch.
   * It will detect double deletes and non-heap based pointer deletes.
   * You can query it for how much memory is used at any point.
   * Because of the previous point, you can bracket a function call
     with usage queries to find out "memory leaked" for that call.
   * You can query a new-based pointer for how much memory it points to.
   * Performance is quite respectible for a memory debugger (but it is
     not intended for release code).

It is not thread safe.  But it is simple enough it easily could be made 
so.  Hmm... perhaps I'll write about the new Metrowerks::threads next. 
:-)

How does it work?

Overview
--------

Like most memory debuggers, it is based on overriding new and delete.  
This will not manage malloc/free.

The overridden new requests memory from malloc but before returning the 
pointer, registers it in a container with the requested size, and 
whether or not this is an array new.

The overridden delete looks up the pointer in the container.  If it 
can't find the pointer, it asserts.  Next it makes sure the "arrayness" 
of the pointer matches the "arrayness" of the delete.  If it doesn't, it 
asserts.  It then removes the pointer from the container before calling 
free.

At program end, if the container of pointers is not empty, the program 
asserts.

Details
-------

The first thing to realize is that the container that holds all of the 
pointer info better not use new/delete.  Else you would get into an 
infinite recursion situation.  The container must instead be based on 
malloc/free.  That way the container is not including its own heap usage 
in the statistics it collects.  The STL containers can be configured to 
use malloc/free instead of new/delete.  To do this you need a 
"mallocAllocator".

Below is a simple mallocAllocator:

// mallocAllocator.h

#ifndef mallocAllocator_h
#define mallocAllocator_h

#include <cstdlib>
#include <limits>
#include <new>
//#include <cassert>

template <class T>
class mallocAllocator;

template <>
class mallocAllocator<void>
{
public:
    typedef std::size_t    size_type;
    typedef std::ptrdiff_t difference_type;
    typedef void*          pointer;
    typedef const void*    const_pointer;
    typedef void           value_type;
    template <class U>
        struct rebind
        {
            typedef mallocAllocator<U> other;
        };
};

template <class T>
class mallocAllocator
{
public:
    typedef std::size_t    size_type;
    typedef std::ptrdiff_t difference_type;
    typedef T*             pointer;
    typedef const T*       const_pointer;
    typedef T&             reference;
    typedef const T&       const_reference;
    typedef T              value_type;
    template <class U>
        struct rebind
        {
            typedef mallocAllocator<U> other;
        };

    mallocAllocator() throw() {}
    template <class U> mallocAllocator(const mallocAllocator<U>&) 
throw() {}

    pointer address(reference x) const             {return &x;}
    const_pointer address(const_reference x) const {return &x;}

    pointer allocate(size_type n, const void* = 0);
    void deallocate(pointer p, size_type) {std::free(p);}
    size_type max_size() const throw()
        {return std::numeric_limits<size_type>::max() / sizeof(T);}

    void construct(pointer p, const T& val) {::new((void*)p) T(val);}
    void destroy(pointer p)                 {p->~T();}
};

template <class T>
inline
typename mallocAllocator<T>::pointer
mallocAllocator<T>::allocate(size_type n, 
mallocAllocator<void>::const_pointer)
{
    pointer result = (pointer)std::malloc(n * sizeof(T));
    if (result == 0)
//        assert(false);
        throw std::bad_alloc();
    return result;
}

template <class T, class U>
inline
bool
operator==(const mallocAllocator<T>&, const mallocAllocator<U>&) throw()
{
    return true;
}

template <class T, class U>
inline
bool
operator!=(const mallocAllocator<T>&, const mallocAllocator<U>&) throw()
{
    return false;
}

#endif // mallocAllocator_h

This is the most simple allocator possible, and does nothing but call 
malloc/free instead of new/delete.  This one throws a std::bad_alloc if 
the heap is exhausted.  But if you prefer, you can have it assert 
instead.

The mallocAllocator is a general purpose tool and is presented here only 
because the memory debugger needs it.  Otherwise the mallocAllocator has 
nothing to do with memory debuggers.

Each pointer stored in the container must be mapped to a struct that 
stores the pointer's associated size, and "arrayness".  There are two 
containers in MSL C++ which are appropriate for this job:

std::map
Metrowerks::hash_map

I've chosen Metrowerks::hash_map for performance reasons.  The lookup 
time will be O(1) instead of O(log N).  The latest hash_map has a 
default hash function that will hash pointers (as well as all other 
scalars) which previous versions of our hash_map lack.

This information can be coded into a header, say simple_mem_debug.h like 
so:

// simple_mem_debug.h

#ifndef SIMPLE_MEM_DEBUG_H
#define SIMPLE_MEM_DEBUG_H

#include "mallocAllocator.h"
#include <hash_map>
#include <cstddef>

struct mem_info
{
    std::size_t size_;
    bool array_;
};

typedef Metrowerks::hash_map<void*, mem_info, Metrowerks::hash<void*>, 
                    std::equal_to<void*>,
                    mallocAllocator<std::pair<void*const, mem_info> > >
                    MemMap;

MemMap& get_memmap();

#endif  // SIMPLE_MEM_DEBUG_H

This header creates a struct mem_info to hold a size and an "arrayness" 
bool.  Then a typedef called MemMap is created which refers to a 
hash_map, mapping void* to mem_info, using the defaults for all other 
template parameters except for the allocator (which of course uses our 
mallocAllocator).

And a single function is prototyped:  get_memmap.  This is essentially 
the singleton pattern that will give clients access to the map of 
pointers.

So knowing that you have a hash_map<void*, mem_info, ...bla,bla>, and 
knowing that the user interface of hash_map is very, very close to 
std::map, you now know a great deal about how to use this debugger!  You 
can iterate over the container summing up pointer sizes.  And you can 
search this container for specific pointers and find out all about them 
(if they exist at all in the container).

We're not quite finished yet, but we're really not that far away either!  
There still needs to be a simple_mem_debug.cpp that implements 
everything we said we would do.  But it is only 113 lines long and 
presented below.

// simple_mem_debug.cpp

#include "simple_mem_debug.h"

#include <cassert>
#include <cstdlib>
#include <new>
#include <msl_utility>

First a nice sprinkling of includes.

void check_mem_leak()
{
    assert(get_memmap().empty());
}

MemMap& get_memmap()
{
    static bool init = false;
    static MemMap memmap;
    if (!init)
    {
        std::atexit(check_mem_leak);
        init = true;
    }
    return memmap;
}

get_memmap() has two jobs:

1.  Act as a singleton getter for the one instance of MemMap.
2.  Register the MemMap with atexit in such a way as to assert if the 
map isn't empty.

This second function is accomplished with check_mem_leak and is fairly 
self explanatory.  The atexit registration is done only the first time 
get_memmap is called thanks to the static init flag.

Next we need to implement the new and delete operators.  There are four 
new operators and four matching delete operators:  regular and nothrow 
crossed with non-array and array.  It is easiest to implement all of the 
logic in one function, and then forward the four variants to that 
function.

Here is the meat and potatoes for the new operators:

void* do_new(std::size_t size, bool is_array) throw(std::bad_alloc)
{
    if (size == 0)
        size = 1;
    Metrowerks::alloc_ptr<char, mallocAllocator<char> > ptr;
    while (true)
    {
        ptr.reset((char*)std::malloc(size));
        if (ptr.get() != 0)
            break;
        std::new_handler p = std::set_new_handler(0);
        std::set_new_handler(p);
        if (!p)
            throw std::bad_alloc();
        p();
    }
    mem_info& m = get_memmap()[ptr.get()];
    m.size_ = size;
    m.array_ = is_array;
    return ptr.release();
}

do_new takes a size requested, and whether or not this is for an array 
as parameters.  If the size is 0, it must be bumped to 1 (so says the 
C++ standard).  Next a little jewel from <msl_utility> comes into play:  
Metrowerks::alloc_ptr.  This is essentially a std::auto_ptr that can 
take an allocator for a deletion policy.  Use of this animal means we do 
not have to worry about exception safety while we store the pointer in 
the map.  If an exception is thrown, the alloc_ptr will clean up the 
allocated pointer using free.

The loop is standard fare for getting a pointer, and calling the new 
handler if the allocation doesn't succeed.  This is the type of logic 
that should go into any operator new.

After we get a pointer, then we get the memmap from the singleton 
function, store the pointer, and get a reference to the associated 
mem_info struct.  Then we simply fill in the struct with the parameters 
that were passed in.  And finally the alloc_ptr releases ownership of 
the pointer so that it can be returned to the client.

Now we just need the four operator new variants that will forward to 
do_new:

void* operator new (std::size_t size) throw(std::bad_alloc)
{
    return do_new(size, false);
}

void* operator new (std::size_t size, const std::nothrow_t&) throw()
{
    try
    {
        return operator new(size);
    }
    catch(...)
    {
    }
    return 0;
}

void* operator new [](std::size_t size) throw(std::bad_alloc)
{
    return do_new(size, true);
}

void* operator new [](std::size_t size, const std::nothrow_t&) throw()
{
    try
    {
        return operator new[](size);
    }
    catch(...)
    {
    }
    return 0;
}

The non-array versions pass in false, and the array versions pass in 
true.  The nothrow versions wrap the call with a try/catch and return 0 
on failure.

Next the delete logic:

void do_delete(void* ptr, bool is_array) throw()
{
    if (ptr)
    {
        MemMap& m = get_memmap();
        MemMap::iterator i = m.find(ptr);
        assert(i != m.end());
        assert(i->second.array_ == is_array);
        m.erase(i);
        std::free(ptr);
    }
}

If the pointer is non-null then we look it up in the memmap.  If it 
isn't there, assert.  If the arrayness of the pointer doesn't match the 
arrayness of the delete, assert.  Erase the pointer record from the 
memmap and free the pointer.

And the four variants of operator delete that forward to do_delete:

void operator delete (void* ptr) throw()
{
    do_delete(ptr, false);
}

void operator delete (void* ptr, const std::nothrow_t&) throw()
{
    operator delete(ptr);
}

void operator delete[](void* ptr) throw()
{
    do_delete(ptr, true);
}

void operator delete[](void* ptr, const std::nothrow_t&) throw()
{
    operator delete[](ptr);
}

And that is it!

As far as memory debug tools go, it just doesn't get any simpler than 
that.

Below is some HelloWorld type stuff to exercise our new toy:

#include "simple_mem_debug.h"
#include <iostream>

std::size_t
memory_used()
{
    std::size_t result = 0;
    const MemMap& mem_debug = get_memmap();
    for (MemMap::const_iterator i = mem_debug.begin(),
                                e = mem_debug.end(); i != e; ++i)
        result += i->second.size_;
    return result;
};

#include <cassert>

std::size_t
memory_size(void* p)
{
    const MemMap& mem_debug = get_memmap();
    MemMap::const_iterator i = mem_debug.find(p);
    assert(i != mem_debug.end());
    return i->second.size_;
}

int main()
{
    std::cout << "memory used at point A: " << memory_used() << '\n';
    int* p1 = new int;
    std::cout << "memory used at point B: " << memory_used() << '\n';
    int* p2 = new int[10];
    std::cout << "memory used at point C: " << memory_used() << '\n';
    std::cout << "size of p2 is " << memory_size(p2) << '\n';
    delete p2;
    std::cout << "memory used at point D: " << memory_used() << '\n';
}

I've created a memory_used function which just loops over the memory map 
and adds up the sizes of all of the pointers.  And another utility 
memory_size that will return the size of a pointer given to it.  These 
aren't included in the simple_mem_debug utility because it is supposed 
to be ... well... simple!  You can probably dream up many more little 
helpers like this.  You might even add to the mem_info struct so that 
you can gather even more information, and add still more helpers taking 
advantage of that!

The above program (in PEF or Mach-O) prints out:

memory used at point A: 508
memory used at point B: 568
memory used at point C: 608
size of p2 is 40
Assertion (i->second.array_ == is_array) failed in 
"simple_mem_debug.cpp", function "do_delete", line 72

It is asserting because the delete p2 should read:

delete [] p2;

Fixing that bug the test now outputs:

memory used at point A: 508
memory used at point B: 568
memory used at point C: 608
size of p2 is 40
memory used at point D: 568
Assertion (get_memmap().empty()) failed in "simple_mem_debug.cpp", 
function "check_mem_leak", line 11

Hmm... leaked p1.

Add delete p1 right after delete [] p2:

memory used at point A: 508
memory used at point B: 568
memory used at point C: 608
size of p2 is 40
memory used at point D: 564

Everything looks good!

The reason you see 508 bytes in use at the start of main is because the 
iostreams (cout, cin, etc.) are being initialized before main, and they 
use new/delete.  Note, don't include <iostream> if you don't need it.  
There is a cost of simply including this header!

The reason 56 more bytes are in use at the end of main than at the 
beginning is again the iostream's fault.  As they are used, they further 
initialize themselves, using up more memory.  But that memory is cleaned 
up by the time our check_mem_leak() runs, else it would fire.  Yes, our 
little simple_mem_debug is even leak checking cout! :-)

It is rare that you can get so much power out of so little code.  I hope 
simple_mem_debug is as useful for you as it has been for me.

-Howard
The MSL C++ guy
Metrowerks
Technical Details
Message-ID: <hinnant-83DA26.18503024092003@syrcnyrdrs-02-ge0.nyroc.rr.com>
Path: arc1-ca.pugleaf.net!archiver1.google.com!news1.google.com!newsfeed.stanford.edu!cyclone.bc.net!sjc70.webusenet.com!news.webusenet.com!elnk-nf2-pas!elnk-pas-nf1!newsfeed.earthlink.net!newsfeed2.easynews.com!newsfeed1.easynews.com!easynews.com!easynews!news-west.rr.com!news.rr.com!news-rtr.nyroc.rr.com!news-out.nyroc.rr.com!twister.nyroc.rr.com.POSTED!53ab2750!not-for-mail

Show Full Headers
Complete Article Headers
X-Google-Language: ENGLISH,ASCII-7-bit
X-Google-Thread: 10bcce,efb176352c161b08,start
X-Google-Attributes: gid10bcce,public
X-Google-Thread: f635e,efb176352c161b08,start
X-Google-Attributes: gidf635e,public
X-Google-ArrivalTime: 2003-09-24 15:51:04 PST
Path: arc1-ca.pugleaf.net!archiver1.google.com!news1.google.com!newsfeed.stanford.edu!cyclone.bc.net!sjc70.webusenet.com!news.webusenet.com!elnk-nf2-pas!elnk-pas-nf1!newsfeed.earthlink.net!newsfeed2.easynews.com!newsfeed1.easynews.com!easynews.com!easynews!news-west.rr.com!news.rr.com!news-rtr.nyroc.rr.com!news-out.nyroc.rr.com!twister.nyroc.rr.com.POSTED!53ab2750!not-for-mail
From: Howard Hinnant <hinnant@metrowerks.com>
Newsgroups: comp.sys.mac.programmer.codewarrior,codewarrior.mac
Subject: The world's simplest C++ memory debugger
Organization: Metrowerks
User-Agent: MT-NewsWatcher/3.3b1 (PPC Mac OS X)
Message-ID: <hinnant-83DA26.18503024092003@syrcnyrdrs-02-ge0.nyroc.rr.com>
Lines: 500
Date: Wed, 24 Sep 2003 22:50:30 GMT
NNTP-Posting-Host: 24.58.14.109
X-Complaints-To: abuse@rr.com
X-Trace: twister.nyroc.rr.com 1064443830 24.58.14.109 (Wed, 24 Sep 2003 18:50:30 EDT)
NNTP-Posting-Date: Wed, 24 Sep 2003 18:50:30 EDT