Memory Manager

Posted by David Haley on Tue 29 Jul 2003 08:42 AM — 3 posts, 10,811 views.

USA #0

As promised, here is the code for a memory manager. The purpose of this class, from which all "managed" objects must inherit, is to count references to an object, and delete it when there are no more references. Also, an object can be "de-allocated" using DeleteMemoryAllocation(), which means that "it doesn't exist", even if the memory is still physically allocated and all the contents are still there.

To check for memory allocation, code would call the CheckMemoryAllocation() member function, which throws an eNoLongerAllocated exception if the memory is "dead", at which point the caller will dereference the object using the DeleteMemoryReference() global function.

There is a question I haven't quite solved yet: where to put the check for memory allocation.
- Should it be the first statement in any managed member function? This way, it would be the responsibility of the caller to enclose the code in a try block, but sometimes you want to know if it's allocated before calling a function...
- Should the caller, before actually using the object, do a small try block of try { obj->CheckMemoryAllocation() } catch (eNoLongerAllocated) { printf("Uh-oh"); return; } ? Or should it do something else?
Both have their advantages and disadvantages... I'm still trying to find the even point between the two.


And so without further ado, here is the code.



memory.h

#ifndef __MEMORY_H_
#define __MEMORY_H_

#define MEMORY_MANAGER_DEBUG

class eNoLongerAllocated
{
public:
	eNoLongerAllocated();
	virtual ~eNoLongerAllocated();

	const char * GetReason() const { return "Unit of memory is no longer allocated."; }
};

class cMemoryManager
{
protected:
	short memReferences;
	short memAllocations;

	bool DeleteReference(); // decrease reference count by one, and return true if ref = 0
	bool DeleteMemory(); // decrease reference/allocation count by one, return true if ref = 0

public:
	cMemoryManager();
	virtual ~cMemoryManager();

	cMemoryManager * CreateReference(); // increase reference count by one
	cMemoryManager * CreateAllocation(); // increase reference + allocation by one
	
	void CheckMemoryAllocation();

	short getMemoryReferences() { return memReferences; }
	short getMemoryAllocations() { return memAllocations; }

	friend void DeleteMemoryAllocation(cMemoryManager * memory);
	friend void DeleteMemoryReference(cMemoryManager * memory);
};

void DeleteMemoryAllocation(cMemoryManager * memory);
void DeleteMemoryReference(cMemoryManager * memory);

#endif



memory.cpp


// necessary if you want to put in print statements,
// for debugging
#include <stdio.h>

#include "memory.h"

#include <list>
using namespace std;

list<cMemoryManager*> AllocatedMemory;

eNoLongerAllocated::eNoLongerAllocated()
{
}
eNoLongerAllocated::~eNoLongerAllocated()
{
}


cMemoryManager::cMemoryManager()
{
	memReferences = 1;
	memAllocations = 1;
	AllocatedMemory.push_back(this);
}
cMemoryManager::~cMemoryManager()
{
	AllocatedMemory.remove(this);
}


cMemoryManager * cMemoryManager::CreateReference()
{
	memReferences++;
	return this;
}
cMemoryManager * cMemoryManager::CreateAllocation()
{
	memReferences++;
	memAllocations++;
	return this;
}

bool cMemoryManager::DeleteReference()
{
	memReferences--;
	if (memReferences <= 0)
		return true; // no more references - time to delete object
	return false; // do not delete object
}
bool cMemoryManager::DeleteMemory()
{
	memReferences--; memAllocations--;
	if (memReferences <= 0)
		return true; // no more references - time to delete object
	return false; // do not delete object
}

void cMemoryManager::CheckMemoryAllocation()
{
	if ( memAllocations <= 0 )
		throw eNoLongerAllocated();
}


////////////////////////////////////

void DeleteMemoryAllocation(cMemoryManager * memory)
{
	try {
		memory->CheckMemoryAllocation();
		if (memory->DeleteMemory())
			delete memory;
	} catch (eNoLongerAllocated e) {
		if (memory->DeleteReference())
			delete memory;
	}
}

void DeleteMemoryReference(cMemoryManager * memory)
{
	if ( memory->DeleteReference() )
		delete memory;
}


Australia Forum Administrator #1
Looks intertesting, can you give a small example program that demonstrates its use?
USA #2
Well, it's sort of hard to write out a small example program that properly demonstrates what this is for. Instead, I'll try to explain it by showing the big picture of a MUD, skipping a few steps that aren't really relevant :)


First and foremost, the idea here is memory management, more specifically, reference counting. There are many problems of cross-referencing all over MUDs: connections referring to characters referring to connections, characters referring to other characters, objects referring to other objects referring to characters... The problem I'm trying to solve is to find a manageable way to keep track of references, and only physically delete memory allocation when there are no longer any resources referring to it.


So, for example, imagine Fred follows Joe.

(inside the Fred character object - I'm making up code as I go along)

this->followingWhom = Joe->CreateReference();

What we just did here was tell Joe that a new reference was made.

Now, later on, Fred stops following Joe.

DeleteReference(this->followingWhom); // == Joe
this->followingWho = NULL;


Since we no longer need to reference Joe, we indicate that we're removing a reference.


Now, imagine Fred is following Joe. And then imagine Joe quits. Now Fred is referencing memory that is no longer valid. If this memory were to be accessed after having been freed, a crash would almost certainly result. That is where my code comes in.

Joe's memory isn't actually FREED, when he quits. Instead, the DeleteMemoryAllocation() function is called. This decreases the Ref count, as well as the allocation count - which is now zero. What does this mean? It means that the memory is "no longer there", but still there to avoid crashes. However, lo and behold - the reference count ISN'T zero, since Fred is still referring to us!

Now, Fred is going to, at some point, want to know who he's following around. The code, which knows that a character has a tendancy to disappear without warning, first tests Joe's presence. If Joe is no longer allocated, then, oh well, we remove a reference count and say we're not following anybody.


try {
    Joe->CheckMemoryAllocation();
} catch (eNoLongerAllocated) {
    DeleteMemoryReference(Fred->followingWhom);
    Fred->followingWhom = NULL;
}


So what just happened? Well, the CheckMem function was called, and it threw an exception saying "abort! I no longer exist!" So, Fred says oh well, and the DeleteMemRef function is called. Now, this function decreases Joe's ref count, which now hits 0 - it had 2, one for Joe "pointing to itself", and one for Fred referring to it. When Joe's memory allocation was deleted, the ref count went to one, which is why the memory didn't delete itself. Instead, it stayed around, waiting for everything that references to it to realize it's gone. In this case, Fred eventually realized that Joe no longer existed, and so dereferenced it, allowing it to finally delete itself.


So what's the point? Why can't Joe, upon quitting, just tell everyone to stop following him? Well, there are a few reasons:

a) the coder might forget something that needs to be told that Joe is quitting. It could be anything - it could be a spell waiting to trigger, it could be a mob hunting, it could be somebody's reply pointer. The advantage gained here is one of making the coder's life a little easier: s/he doesn't have to remember every little place that needs to be dereferenced, and, assuming checks are introduced before accessing something "volatile", nothing should go wrong.

b) performance is probably improved. Instead of having to loop through every character to find which one is following Joe, we simply let the character do the work on its own when the time comes. Imagine we had to loop through every pending spell, every object, every room, every character... nightmare!

c) entities might want special reactions in a case where the target disappears in the middle. For example, a time-spell could fizz out instead of casting (or crashing the MUD) when the time runs out, if the target quit. It would seem more accurate, in any case.

Sorry I couldn't give precise code to draw the picture... I use it in my network code, and if you want I can paste snippets from there, but it's basically the same as what I wrote above. :) Hope this clears things up.