So today was working on more stuff for RocketFrogz, and ran into a tricky situation. Basically I was working with the sprite system which supports animations. You basically do this:
SpriteSheet* s = SpriteSheetType::spriteSheet ("Filename");
s->loadTextures (renderer);
s->signalFrameChanged().connect (functionPointer (this, &Object::frameChanged));
s->setAnimation ("idle");
Whenever the sprite changes frames while running the animation, it notifies you via frameChanged. At the moment, this is a function void f (void) with no parameters. Now i want to load tons of these, but hook
them to the same function. So the signal is going to change to a Signal1 which passes the sprite sheet that changed (this way i can tell which sheet changed, and act acoordingly).
Now the issue:
I have lots of code depending on the old method.
Now I could go back and change all the previous code. I’ve done this before, and generally thats a good thing. However, i’ve ran into this in several places. Timers is another good example : you sometimes want the parameter and sometimes not. So what can i do?
Solutions:
- Deal with it : its a limitation and have to live with it.
- Find some other method to pass stuff around. For example, create an object as a responder, and use that as an identifier.
- Change FP so it can upgrade itself to higher types
I chose the last one.
So what does this mean, upgrade itself? Basically I want a function pointer to be able to represent itself as a higher class of function pointer than what it is, and throw away extra parameters.
As a quick example: basically if i have
void f (void)
i want to be able to do
f (1, 2);
and it just throw away 1 and 2. (using FP, this doesnt help you for direct calls like that)
Doesn’t sound so bad when i put it that way. Qt does something similar with signal/slots, but they have moc to handle those connections : i just have c++, and as you can probably tell : c++ does NOT like flexibility like that.
Its picky enough about regular types, but now i want to relax some of C++’s most fundemental rules about function pointers. Then again, Gail::Core::FunctionPointer was designed completely to work around c++’s rules, so its the place to do it if any!
My key requirements for this:
- No change to previous code. If it worked before with function pointer with direct type matching, it’ll work now.
- Low overhead (if any) in both speed and memory. Function pointers are used everywhere in gail : they slow down, everything slows down. A few bytes can easily add up.
- Ability to upgrade as needed (previously defined)
- Minimal changes to usage : if at all possible, use the exact same syntax as now and just ‘magically’ convert it
- Hopefully no massive rework of FunctionPointer (related to the first point). Its a tad messy atm (mostly because of the power it allows), and i’d rather it not get much worse.
So there were 3 major iterations for this. They mostly did the same thing, but tweaked to fix problems found.
First approach: create the higher level and write the conversion function yourself (cast operator)
This approach was almost perfet. Basically we have a cast operator (operator Type ()) that upgrades a pointer to the higher level pointers. This then points it to the lower level pointer’s ‘run as lower’ function, which then knows how to run the FP as the lower level. So essentially it creates a new FP2 that calls the actual FP1. So run() becomes
FP2.run() -> FP1.run1As2() -> FP1.run() -> <actual call> : pass return back up
Heres the actual return from the operator (just cause i think its one of the coolest lines ever):
return functionPointer<FunctionPointer1<ReturnValue, Param1>, ReturnValue, Param1, NewParam2> (const_cast<FunctionPointer1<ReturnValue, Param1>*>(this), FunctionPointer1<ReturnValue, Param1::runFunctionPointer2As2<NewParam2>);
Little more overhead on the call, but no more memory needed and is essentially the same as what people have to do now. Theirs only one issue. Since your creating and returning the FP2 as the final type, the FP1 is left destroyed on the stack. So if you wait long enough, the FP1 would be destroyed and randomly it would crash on calls. So the problem becomes the lifetime of the original FP1, since it wasnt needed. Other than that, this approach was exactly what i wanted. But you cant deal with FP’s crashing half the time on calls, so something else is needed.
Second approach: use constructors and more helpers to convert types appropriately.
This exploits the fact that a function pointer of a type is always the same size as another function pointer of the same type, even if they have different amount of parameters. We allow a function pointer of a higher level to use a function pointer of a lower level, copies all the parameters into the higher level like normal, but when it runs it switches to the lower level and runs accordingly. So internally, this is something like:
FunctionPointer2 (Return (*function) (Param1))
: internalPointer ( cast<(Return (*) (Param1, Param2))>(function)), numOfParams (1)
{}
Then on run, you check the number of params and run accordingly:
Return run (Param1 p, Param2 p2)
{
if (numOfParams == 2) { return this->operator(); } // normal
else if (numOfParams == 1 ) { return (internalFP1*)(run()); } // degraded pointer
}
Cost becomes an integer pointer per FP, and an int lookup + jump table for figuring out how to run it. Not that bad. So now public side we add more helpers to functionPointer to create these hybrid pointers…
Except c++ cant tell the difference between the various helpers we have now. It cant tell these two are different functions, and requires you to specify.
FunctionPointer2<Return, Param1, Param2> functionPointer (Int8* null, Return (*function) (Param1))
FunctionPointer1<Return, Param1> functionPointer (Int8* null, Return (*function) (Param1))
Ouch. So rename them all to have the number in the name (such as functionPointer2), and reserve ‘functionPointer’ for ones that completely match the type.
Not that bad, but it does require us to think every time we create a function pointer. And everyone knows, thinking about minor details while programming is bad.
So what can we do better? We did have the right usage in the first approach, maybe we can combine them.
Third approach: same as second, but replace helpers with the cast operator in the first approach to simplify the api
So now we keep the constructors and the override from the second approach, but we also add the upcast operators from the first one. Instead of the cast operator setting up another function pointer, it uses the constructors to create the new function pointer : that way all the information is passed, and we dont care if the FP1 gets destroyed. The FP2 then knows its degraded, and youve got a FP2 that can call an FP1. So this is now legal (from Gail/Core/Tests/):
static void funcPointfunc1 ( void ) { funcPointfunc1_ran = true; }
GAIL_TEST (functionPointersCanUpcastProperly0To2)
{
// funcPointfunc1 is <void>
Gail::Core::FunctionPointer2<void, int, int> g = Gail::Core::functionPointer (NULL, &funcPointfunc1); // upcast to a <void, int, int> where the extra param is ignored
g.run(2, 3); // drops both
}
Which as a byproduct, means signals accept lower argument functions while still keeping type safety!
– thothonegan

