Purposes of a handle
Handles are a safer way when handling pointers. We use handles to reference various data. When errors occur, it's safer to return s nullptr compared to an application crash.
struct Handle
{
int index;
int generation;
}
For example, this is dangerous:
Object* obj = &Object[0];
delete obj;
obj->do(); //crash
Contrast this to using handles:
Handle handle = CreateObject();
DeleteObject(handle);
UseObject(handle); //null
Handles 101
Handles are best used when you have the following struct in your program:
- Handle
struct - A place to store the handles
We will start a basic example using a vector of two values for some position value.
struct Handle
{
int index;
int generation;
}
struct Vec2
{
int x;
iny y;
int generation;
}
//store
#define MAX_VECS 16
static Vec2 vec_pool[MAX_VECS];
static int next_free = 0;
Helper Functions
We need a way to get an empty (zeroed out) handle as well as a way to compare two handles. We start with the zero handle as a base.
Handle HandleZero(void)
{
Handle h = {0, 0};
return h;
}
Now we have to create a helper function to ensure that any two handles are matched against the store.
int HandleMatch(Handle a, Handle b)
{
return (a.index == b.index && a.generation == b.generation);
}
Creating an object with return handle
Now we must create a function where we create the struct Vec2 but we have to return it with a handle since the whole point is to elimante the need to directly access the pointer.
Handle Vec2(float x, float y)
{
Handle h = HandleZero();
//check if free slot surpasses the max
if(next_free < MAX_VECS)
{
vec_pool[next_free].x = x;
vec_pool[next_free].y = y;
vec_pool[next_free].generation++;
h.index = next_free;
h.generation = vec_pool[next_free].generation;
//used slot, so use next free
next_free++;
}
return h;
}
You might be asking why you need the geneation for? Since it appears that index might be good enough?
Consider this situation:
Handle h1 = {index: 0, generation 1};
Vec2Delete(h1); //increments generation at index 0 to 2
Handle h2 = Vec2Create(5,6);
Vec2* v = Vec2Handle(h1); //null since generation doesnt match
In other words, the generation will prevent dangling handles. In an analogy, consider a hotel room. The index can be a room number. But there's no way to differentiate who owns what room number. Who can access the room at any given time? That's why we use generation. The generation can be referred to as the keycard for the hotel. If our generation or keycard doesn't work, we don't have access.
Getting data from handle
Vec2* Vec2FromHandle(Handle h)
{
if(h.index >= 0 && h.index < MAX_VECS)
{
if(vec_pool[h.index].generation == h.generation)
{
return &vec_pool[h.index];
}
}
return nullptr;