Click here to Skip to main content
15,891,942 members
Articles / Programming Languages / C++

Undo and Redo the "Easy" Way

Rate me:
Please Sign up or sign in to vote.
4.95/5 (42 votes)
20 Jun 2004CPOL22 min read 290.3K   8.6K   114  
This article introduces a simple approach to in-memory transactions that can be used to implement Undo and Redo. The technique uses SEH and Virtual Memory and requires only STL and Win32.
#pragma warning (disable:4786)
#include "MemManager7.h"

#include <windows.h>

#include <map>
#include <vector>
#include <deque>
#include <algorithm>

#include <math.h>

#include "avl_tree.h"

using namespace std;

#include "Compress.h"

#define AUDIT
#define CHECK_POINTER(s)

namespace Mm
{
	////////////////////////////////////////////////////////////////////////////////
	// result codes
	
	static const Result R_OK				(Result::eOk,		MAKE_STRING_TOKEN(0,22));
	static const Result E_TRANSACTION_OPEN	(Result::eError,	0,		1);
	static const Result E_ROLLBACK_STATE	(Result::eError,	0,		2);
	static const Result E_NO_TRANSACTION	(Result::eError,	0,		3);
	static const Result E_VIRTUAL_ALLOCATE	(Result::eError,	0,		4);
	static const Result E_VIRTUAL_PROTECT	(Result::eError,	0,		5);
	static const Result E_NO_DATA			(Result::eError,	0,		6);
	static const Result E_INVALID_ARGUMENT	(Result::eError,	0,		7);

	////////////////////////////////////////////////////////////////////////////////
	// system invariants
	
	static const int PAGE_SIZE = 4096;
	static const int BLOCK_SIZE = PAGE_SIZE * 16;

	////////////////////////////////////////////////////////////////////////////////
	// freelist node
	
	struct Free {
		Free() :  size(0), next(0) {}; // use only with placement new

	private:
		Free(const Free&) {}
		~Free() {};

	public:
		typedef unsigned long	SIZE_TYPE;
		SIZE_TYPE	size; // length of the availble buffer starting at '&next'
		Free*		next; // TODO: remove

		struct _bylocation {
			_bylocation() : lt(0), gt(0), bf(0) {}
			Free*		lt;
			Free*		gt;
			int			bf;
		} loc;

		static Free* FromPtr(void* p) 
		{ 
			return reinterpret_cast<Free*>((char*)p - sizeof(SIZE_TYPE)); 
		}

		void* UserPtr() const
		{ 
			return static_cast<void*>(const_cast<Free**>(&next));
		}

		void* Begin() const
		{
			return static_cast<void*>(const_cast<Free*>(this));
		}

		void* End() const
		{
			return reinterpret_cast<void*>((char*)UserPtr() + size);
		}

		bool Adjacent(const Free* f) const
		{
			return (f && (this->End() == f->Begin()));
		}

		bool Overlap(Free* f) const 
		{
			return (this->End() > f->Begin());
		}
	};

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

	struct AddressAbstractor { // for AVL tree
		typedef Free* handle;
		typedef unsigned long key;
		typedef size_t size;

		handle get_less(handle h, bool access) { return h->loc.lt; }
		void set_less(handle h, handle lh) { h->loc.lt = lh; }

		handle get_greater(handle h, bool access) { return h->loc.gt; }
		void set_greater(handle h, handle gh) { h->loc.gt = gh; }

		int get_balance_factor(handle h) { return h->loc.bf; }
		void set_balance_factor(handle h, int bf) { h->loc.bf = bf; }

		int compare_key_key(key k1, key k2) 
		{ 
			if (k1 == k2) return(0);
			if (k1 > k2) return(1);
			return(-1);
		}

		int compare_key_node(key k, handle h) { return(compare_key_key(k, (key)h)); }
		int compare_node_node(handle h1, handle h2) { return(compare_key_key((key)h1, (key)h2)); }

		handle null(void) { return(0); }
		static bool read_error(void) { return(false); }
	};

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

	struct AllocationManager
	{
		Free*			sFreeHead;
		abstract_container::avl_tree<AddressAbstractor, 31> locTree;

		AllocationManager() : sFreeHead(0) {}

		Free* Slice(Free* from, size_t bytes)
		{
			const size_t true_bytes = bytes + sizeof(Free::SIZE_TYPE);
			if ((from->size - true_bytes) < sizeof(from->size)) 
				return 0;

			Free* slice = reinterpret_cast<Free*>((char*)from->UserPtr() + (from->size - true_bytes));
			memset(slice, 0, sizeof(Free));
			slice->size = bytes;
			from->size -= true_bytes;

			return slice;
		}

		void Insert(Free* node)
		{
			AUDIT

			// precautionary
			node->next = 0;
			memset(&node->loc, 0, sizeof(node->loc));

			if (sFreeHead == 0) {
				sFreeHead = node;
				locTree.insert(node);
			} else if (sFreeHead->Begin() > node->Begin()) {
				node->next = sFreeHead;
				sFreeHead = node;
				locTree.insert(node);
				
				AUDIT
				
				Compact(sFreeHead);
			} else {
				Free* cursor = locTree.search((size_t)node, abstract_container::LESS);
				node->next = cursor->next;
				cursor->next = node;
				locTree.insert(node);

				AUDIT

				Compact(node);
				
				AUDIT

				Compact(cursor);

				AUDIT
			}
		}

		void Compact(Free* node)
		{
			if (node->Adjacent(node->next)) {
				locTree.remove((size_t)node->next);
		
				node->size += (node->next->size + sizeof(node->size));
				node->next = node->next->next;
			}
		}

		size_t FreeSpace()
		{
			size_t unused = 0;
			Free* f = sFreeHead;
			while (f) { unused += f->size; f = f->next; }
			return unused;
		}

		size_t FreeCount()
		{
			size_t blocks = 0;
			Free* f = sFreeHead;
			while (f) { ++blocks; f = f->next; }
			return blocks;
		}

		void Audit()
		{
		}
	};

	////////////////////////////////////////////////////////////////////////////////
	// typedef and definition of global page to space map

	typedef map<void*, size_t>	PageSpaceMap;
	PageSpaceMap pages;

	////////////////////////////////////////////////////////////////////////////////
	// representations of transactions and spaces

	typedef map<void*, void*>	PageMap;
	typedef map<void*, size_t>	BlockMap;

	struct Transaction { 
		Transaction() : id(++nextId) {}

		PageMap backups; 
		BlockMap added; 
		size_t id;
		static size_t nextId; 
	};
	size_t Transaction::nextId = 0; 

	typedef deque<Transaction> Transactions;

	struct Space { 
		Space() : transacting(false), id(nextId++), data(0) {}

		Transactions done; 
		Transactions undone; 
		Transactions cleanup; // TODO: cleanup backups at the time of truncations?
		
		bool transacting; 
		
		size_t id;
		static size_t nextId; 

		AllocationManager* data;

		AllocationManager* AssertData() 
		{
			if (!transacting)
				return NULL; // throw?

			if (data == NULL) {
				// we need our own private, transacted memory
				// NOTE: we're putting an entire page in here, find some other
				// uses for it?
				void* p = ::VirtualAlloc(0, PAGE_SIZE, MEM_COMMIT, PAGE_READONLY);

				pages[p] = id;
				done[0].added[p] = PAGE_SIZE;

				data = new(p) AllocationManager();
			}
			return data;
		}
		
		Free* More(size_t size)
		{
			if (!transacting)
				return NULL; // throw?

			const size_t actual_size = ((size / BLOCK_SIZE) + 1) * BLOCK_SIZE;

			void* p = ::VirtualAlloc(0, actual_size, MEM_COMMIT, PAGE_READONLY);
			if (!p) 
				throw "virtual alloc failed";

			for (unsigned int i = 0; i < actual_size; i += PAGE_SIZE) {
				void* q = (char*)p + i;
				pages[q] = id;
			}

			Free* f = new (p) Free();
			f->size = actual_size - sizeof(f->size);

			done[0].added[p] = actual_size;

			AUDIT

			return f;
		}

		void Audit()
		{
		}
	};
	size_t Space::nextId = 0; 

	typedef map<size_t, Space> Spaces;
	Spaces spaces;

	////////////////////////////////////////////////////////////////////////////////
	// exception filter maps address to space, and creates backup copy of the
	// target page

	int ExceptionFilter(void* exp)
	{
		LPEXCEPTION_POINTERS e = static_cast<LPEXCEPTION_POINTERS>(exp);

		if (e->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION)
			return EXCEPTION_CONTINUE_SEARCH; // return (*previous)(e);

		bool writing = (e->ExceptionRecord->ExceptionInformation[0] != 0);
		void* addr = (void*)e->ExceptionRecord->ExceptionInformation[1];
		void* page = (void*)((unsigned long)addr & ~(PAGE_SIZE - 1));

		if (writing == false)
			return EXCEPTION_CONTINUE_SEARCH; // return (*previous)(e);

		PageSpaceMap::iterator i = pages.find(page);
		if (i == pages.end())
			return EXCEPTION_CONTINUE_SEARCH; // return (*previous)(e);

		Spaces::iterator s = spaces.find(i->second);
		if (s == spaces.end())
			return EXCEPTION_CONTINUE_SEARCH; // my error
	
		Space& space = s->second;
		if (!space.transacting) 
			return EXCEPTION_CONTINUE_SEARCH; // user error

		void* backup = ::VirtualAlloc(0, PAGE_SIZE, MEM_COMMIT, PAGE_READWRITE);
		if (!backup)
			return EXCEPTION_CONTINUE_SEARCH; // out of memory error

		memmove(backup, page, PAGE_SIZE);

		Transaction& t = space.done[0];
		t.backups[page] = backup;

		DWORD old = 0;
		BOOL ok = FALSE;
		
		ok = ::VirtualProtect(backup, PAGE_SIZE, PAGE_READONLY, &old);
		if (ok == FALSE)
			return EXCEPTION_CONTINUE_SEARCH; // return (*previous)(e);

		ok = ::VirtualProtect(page, PAGE_SIZE, PAGE_READWRITE, &old);
		if (ok == FALSE)
			return EXCEPTION_CONTINUE_SEARCH; // return (*previous)(e);

		return EXCEPTION_CONTINUE_EXECUTION;
	}

	////////////////////////////////////////////////////////////////////////////////
	//  API helper functions and implementation

	struct MarkBlockFree
	{
		MarkBlockFree(Space& s) : space(s) {}
		void operator ()(const BlockMap::value_type& b)
		{
			Free* f = reinterpret_cast<Free*>(b.first);
			f->size = b.second - sizeof(Free::SIZE_TYPE);
			
			space.data->Insert(f);
		}
		Space& space;
	};

	static void ReleaseBlock(const BlockMap::value_type& b)
	{
		::VirtualFree(b.first, 0, MEM_RELEASE);
	}

	static void ReleaseDelta(const PageMap::value_type& b)
	{
		delete[] b.second;
	}

	struct RecycleTransaction
	{
		RecycleTransaction(Space& s) : space(s) {}
		void operator() (const Transactions::value_type& t)
		{
			for_each(t.added.begin(), t.added.end(), MarkBlockFree(space));
			for_each(t.backups.begin(), t.backups.end(), ReleaseDelta);
		}
		Space& space;
	};

	static void ReleaseTransaction(const Transactions::value_type& t)
	{
		for_each(t.added.begin(), t.added.end(), ReleaseBlock);
		for_each(t.backups.begin(), t.backups.end(), ReleaseDelta);
	}

	static unsigned long total_rle_len = 0;

	struct CommitBackup
	{
		CommitBackup(Space& s) : space(s) {}
		void operator() (const PageMap::value_type& v)
		{
			DWORD* after = (DWORD*)v.first;
			DWORD* before = (DWORD*)v.second;
			DWORD* delta = (DWORD*)before; // rewrite to conserve memory

			if (!before) 
				throw "missing 'before' page";

			{ // scope
				DWORD old = 0;
				BOOL ok = ::VirtualProtect(before, PAGE_SIZE, PAGE_READWRITE, &old);
				if (ok == FALSE)
					throw "virtual protect failed";
			}

			for (unsigned int j = 0; j < (PAGE_SIZE / sizeof(DWORD)); ++j) {
				const DWORD a = after[j];
				const DWORD b = before[j];
				delta[j] = (a ^ b);
			}

			static char buf[4096 * 2];
			unsigned int len = Compress(delta, buf);

			total_rle_len += len;

			unsigned char* d = new unsigned char[len];
			for (unsigned int i = 0; i < len; ++i)
				d[i] = buf[i];

			Transaction& t = space.done[0]; // current transaction

			{ // scope
				BOOL ok = ::VirtualFree(v.second, 0, MEM_RELEASE);
				if (ok == FALSE)
					throw "virtual free failed in clear backup";
			}

			// assign the compressed delta to the backup's place in the map
			t.backups[v.first] = d;

			{ // scope
				DWORD old = 0;
				BOOL ok = ::VirtualProtect(after, PAGE_SIZE, PAGE_READONLY, &old);
				if (ok == FALSE)
					throw "virtual protect failed";
			}
		}
		Space& space;
	};

	static void CancelBackup(const PageMap::value_type& v)
	{
		void* page = v.first;
		void* backup = v.second;

		if (!backup) 
			throw "missing 'backup' page";

		{ // scope
			DWORD old = 0;
			BOOL ok = ::VirtualProtect(page, PAGE_SIZE, PAGE_READWRITE, &old);
			if (ok == FALSE)
				throw "virtual protect failed in cancel backup";
		}

		memcpy(page, backup, PAGE_SIZE);

		{ // scope
			DWORD old = 0;
			BOOL ok = ::VirtualProtect(page, PAGE_SIZE, PAGE_READONLY, &old);
			if (ok == FALSE)
				throw "virtual protect failed in cancel backup";
		}

		{ // scope
			BOOL ok = ::VirtualFree(backup, 0, MEM_RELEASE);
			if (ok == FALSE)
				throw "virtual free failed in clear backup";
		}
	}

	static void ApplyDelta(PageMap::value_type& v)
	{
		DWORD old = 0;
		BOOL ok = ::VirtualProtect(v.first, PAGE_SIZE, PAGE_READWRITE, &old);
		if (ok == FALSE) 
			throw;

		Mm::ApplyCompressedDelta(v.first, v.second);

		ok = ::VirtualProtect(v.first, PAGE_SIZE, PAGE_READONLY, &old);
		if (ok == FALSE) 
			throw;
	}

	bool IsTransacting(const Spaces::value_type& s)
	{
		return s.second.transacting;
	}

	struct OwnerIs
	{
		OwnerIs(Space& s) : space(s) {}
		bool operator()(const PageSpaceMap::value_type& s)
		{
			return (s.second == space.id);
		}
		Space& space;
	};

	void ClearSpace(Spaces::value_type& s)
	{
		SPACEID sid = s.first;
		Space& space = s.second;

		for_each(space.undone.begin(), space.undone.end(), ReleaseTransaction);
		space.undone.clear();

		for_each(space.done.begin(), space.done.end(), ReleaseTransaction);
		space.done.clear();

		for_each(space.cleanup.begin(), space.cleanup.end(), ReleaseTransaction);
		space.cleanup.clear();

		PageSpaceMap::iterator b = pages.begin();
		while(b != pages.end()) {
			if (b->second == sid) pages.erase(b++);
			else ++b;
		}

		space.data = 0;
	}

////////////////////////////////////////////////////////////////////////////////
// Mm API methods

	Result InitSpaceZero()
	{
		spaces[0].id = 0;

		return R_OK;
	}

	SPACEID CreateSpace(size_t size)
	{
		SPACEID sid = ++Space::nextId;
		Space& space = spaces[sid];
		if (size) {
			space.AssertData();
			space.More(size);
		}
		return space.id;
	}

	Result	Destroy(SPACEID sid)
	{
		Result clear = Clear(sid);
		if (clear.IsError())
			return clear;

		spaces.erase(sid);

		return R_OK;
	}

	Result Clear(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return E_INVALID_ARGUMENT; // user error
	
		Space& space = s->second;
		if (space.transacting)
			return E_TRANSACTION_OPEN;

		ClearSpace(*s);

		return R_OK;
	}

	Result ClearAll()
	{
		if (std::find_if(spaces.begin(), spaces.end(), IsTransacting) != spaces.end())
			return E_TRANSACTION_OPEN;

		std::for_each(spaces.begin(), spaces.end(), ClearSpace);
		return R_OK;
	}

	Result DestroyAll()
	{
		Result clear = ClearAll();
		if (clear.IsError())
			return clear;

		spaces.swap(Spaces());
		return R_OK;
	}

	Result OpenTransaction(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return E_INVALID_ARGUMENT; // user error
	
		Space& space = s->second;

		if (space.transacting)
			return E_TRANSACTION_OPEN;

		Transaction empty;
		space.done.push_front(empty);

		space.transacting = true;

		return R_OK;
	}

	Result CommitTransaction(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return E_INVALID_ARGUMENT; // user error
	
		Space& space = s->second;

		if (!space.transacting)
			return E_NO_TRANSACTION;

		if (space.undone.size() > 0) {
			for_each(space.undone.begin(), space.undone.end(), RecycleTransaction(space));
			space.undone.clear();
		}

		Transaction& t = space.done[0];
		for_each(t.backups.begin(), t.backups.end(), CommitBackup(space));

		space.transacting = false;

		return R_OK;
	}

	Result CancelTransaction(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return E_INVALID_ARGUMENT; // user error
	
		Space& space = s->second;

		if (!space.transacting)
			return E_NO_TRANSACTION;

		Transaction& t = space.done[0];
		for_each(t.backups.begin(), t.backups.end(), CancelBackup);
		t.backups.clear();

		space.done.pop_front();
		space.transacting = false;

		return R_OK;
	}

	TXNID  GetLastTransactionId(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return 0; // user error
	
		const Space& space = s->second;
		return (space.done.size() > 0) ? (TXNID)(space.done[0].id) : 0;
	}

	TXNID  GetNextTransactionId(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return 0; // user error
	
		const Space& space = s->second;
		return (space.undone.size() > 0) ? (TXNID)(space.undone[0].id) : 0;
	}

	TXNID  GetOpenTransactionId(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return 0; // user error
	
		const Space& space = s->second;
		return (space.transacting && space.done.size() > 0) ? (TXNID)(space.done[0].id) : 0;
	}

	Result Undo(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return E_INVALID_ARGUMENT; // user error
	
		Space& space = s->second;
		
		if (space.transacting) 
			return E_TRANSACTION_OPEN;

		if (space.done.size() < 1) 
			return E_NO_DATA;

		{ // scope t
			Transaction& t = space.done[0];
			for_each(t.backups.begin(), t.backups.end(), ApplyDelta);
		}

		space.undone.push_front(space.done[0]);
		space.done.pop_front();

		return R_OK;
	}

	Result Redo(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return E_INVALID_ARGUMENT; // user error
	
		Space& space = s->second;
		if (space.transacting) 
			return E_TRANSACTION_OPEN;

		if (space.undone.size() < 1) 
			return E_NO_DATA;

		{ // scope t
			Transaction& t = space.undone[0];
			for_each(t.backups.begin(), t.backups.end(), ApplyDelta);
		}

		space.done.push_front(space.undone[0]);
		space.undone.pop_front();

		return R_OK;
	}

	Result TruncateUndo(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return E_INVALID_ARGUMENT; // user error
	
		Space& space = s->second;
		if (space.transacting) 
			return E_TRANSACTION_OPEN;

		space.cleanup.insert(space.cleanup.end(), space.done.begin(), space.done.end());
		space.done.clear();
		
		return R_OK;
	}

	Result TruncateRedo(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return E_INVALID_ARGUMENT; // user error
	
		Space& space = s->second;
		if (space.transacting) 
			return E_TRANSACTION_OPEN;

		for_each(space.undone.begin(), space.undone.end(), ReleaseTransaction);
		space.undone.clear();

		return R_OK;
	}

	////////////////////////////////////////////////////////////////////////////////
	// memory allocation implementation (very simple free list)

	void* Allocate(size_t size, SPACEID sid)
	{
		AUDIT

		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return NULL;
	
		Space& space = s->second;
		if (!space.transacting) 
			return NULL;

		size = max(size, sizeof(Free));
		
		// TODO: assert that "data" is allocated in space
		space.AssertData();

		// are there any more free chunks?
		if (!space.data->sFreeHead) {
			space.data->Insert(space.More(size));
		}
		
		AUDIT

		// find the first chunk at least the size requested
		Free* prev = 0;
		Free* f = space.data->sFreeHead;
		while (f && (f->size < size)) {
			prev = f;
			f = f->next;
		}

		AUDIT
		// if we found one, disconnect it
		if (f) {
			space.data->locTree.remove((size_t)f);

			if (prev) prev->next = f->next;
			else space.data->sFreeHead = f->next;
			
			f->next = 0;
			memset(&f->loc, 0, sizeof(f->loc));
		} else {
			f = space.More(size);
		}

		// f is disconnected from the free list at this point

		AUDIT

		// if the free chunk is too(?) big, carve a peice off and return
		// the rest to the free list
		if (f->size > (2*(size + sizeof(Free)))) {
			Free* tmp = space.data->Slice(f, size); // slice size byte off 'f'
			space.data->Insert(f); // return the remainder to the free list
			f = tmp;
		}
		
		AUDIT

		CHECK_POINTER(f)

		void* p = reinterpret_cast<void*>((char*)f + sizeof(Free::SIZE_TYPE));

		CHECK_POINTER(p)

		return p;
	}

	void* Allocate(void* hint, size_t size)
	{
		void* page = (void*)((unsigned long)hint & ~(PAGE_SIZE - 1));

		PageSpaceMap::iterator i = pages.find(page);
		if (i == pages.end())
			return NULL;

		SPACEID sid = i->second;
		return Allocate(size, sid);
	}

	void Deallocate(void* p, SPACEID sid)
	{
		if (!p) return;

		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			throw "attempt to reallocate unmanaged memory";
	
		Space& space = s->second;
		Free* f = Free::FromPtr(p);

		AUDIT

		space.data->Insert(f);

		AUDIT
	}

	void Deallocate(void* p)
	{
		void* page = (void*)((unsigned long)p & ~(PAGE_SIZE - 1));
		PageSpaceMap::iterator i = pages.find(page);
		if (i == pages.end())
			throw "attempt to deallocate unmanaged memory";

		SPACEID sid = i->second;
		Deallocate(p, sid);
	}

	void Deallocate(void* p, size_t size)
	{
		if (!p) return;

		Deallocate(p);
	}

	void* Reallocate(void* p, size_t size)
	{
		if (!p) return p;

		void* page = (void*)((unsigned long)p & ~(PAGE_SIZE - 1));

		PageSpaceMap::iterator i = pages.find(page);
		if (i == pages.end())
			return p;

		SPACEID sid = i->second;
		return Reallocate(p, size, sid);
	}

	void* Reallocate(void* p, size_t size, SPACEID sid)
	{
		AUDIT

		if (!p) 
			return Allocate(size, sid);

		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return p;
	
		Space& space = s->second;

		Free* f = Free::FromPtr(p);
		if (size < f->size)
			return p;

		void* r = Allocate(size, sid);
		memcpy(r, p, f->size);

		space.data->Insert(f);

		return r;
	}

	size_t FreeSpace(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return 0;
	
		Space& space = s->second;
		return (space.data ? space.data->FreeSpace() : 0);
	}

	size_t FreeCount(SPACEID sid)
	{
		Spaces::iterator s = spaces.find(sid);
		if (s == spaces.end())
			return 0;
	
		Space& space = s->second;
		return (space.data ? space.data->FreeCount() : 0);
	}
};

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
A compiler warns of bogasity, ignore it at your peril. Unless you've done the compiler's job yourself, don't criticize it.

Comments and Discussions