Remember that a templated function will actually be converted to a real function at compile time. This means that we can treat it just like any other function, pass it to other functions, store the address of it in array etc etc...
You can even do things that you probably didn't think you could do, such as create a static function to a class's constructor and destructor!
How to do this is simple, here's a snippit of a template function that will construct an object using the class's default constructor (note that we assume the memory has already been allocated):
template<typename T>
static void ConstructObject(T* aThisPtr) {
new (aThisPtr)T();
}
template<typename T>
static void DestroyObject(T* aThisPtr) {
aThisPtr->~T();
}
Now we can call this like so:
struct SomeObject {
SomeObject() {}
void Foo() {}
void Foo(int arg1) {}
};
//manually construct the object
SomeObject* so = (SomeObject*)malloc(sizeof(SomeObject));
ConstructObject(so);
DestroyObject(so);
free(so);
A non-contrived example is using constructors and destructors to implement custom memory allocators that support non-trivial data types.
Remember that any functions called by a class has a "this" pointer passed as an implicit argument. So whenever we bind a static template function to a class function we will always need to pass in a "this" pointer.
For example:
SomeObject so;
int arg1 = 0;
so.Foo(arg1);
The call to Foo can really be thought of as this:
SomeObject::Foo(SomeObject* this, int arg1);
So any static template function that binds to a member function is going to need a "this" pointer.
Following our example, lets bind the static function SomeObjectFoo to SomeObject::Foo().
static void SomeObjectFoo(SomeObject* aThisPtr, int arg1) {
aThisPtr->Foo(arg1);
}
//now call the bound foo function
SomeObject so;
SomeObjectFoo(&so, arg1);
Pretty simple huh? Let's generalize this to all Objects that have a Foo() member function.
template<typename T>;
static void CallFoo(T* aThisPtr, int arg1) {
aThisPtr->Foo(arg1);
}
Okay this is getting better, now we can use templates to accept any class that has a Foo() function.
This is an example of duck typing (if it looks like a duck and quack likes a duck then it's a duck, or in other words, if it has a Foo() function that looks like a Foo() and takes arguments like a Foo() then it has a Foo()).
Still this is all academic, what's the point?
Callbacks! Yes callbacks can be implemented this way and you can pass whatever member function you want by binding it to a static function. The callback can even be a non-template. How do we do this?
Recall that making a static call out of a member call requires that we have a "this" pointer. In the examples so far, we've assumed that "this" has a known type. In a callback system that takes in a static function we won't know specifically what type "this" is, so we'll have to assume that it's void*. This may seem shocking, as generally passing around void* is bad. Bear with me for a moment, I'll convince you that this is that 5% time that it's okay :)
Our callback system is going to be typesafe by construction. That is, all the reinterpret_cast's will be hidden away in the implementation which the caller won't have access to. Lets define a callback struct as so:
typedef void (*FunctionCallback0)(void*); //a function call that takes 0 args
struct Callback0 { //takes in 0 args
Callback0(void* aThisPtr, FunctionCallback0 aCallBack) :
thisPtr(aThisPtr), callback(aCallBack) {
}
void Call() {
callback(thisPtr);
}
private:
void* const thisPtr;
const FunctionCallback0 callback;
};
Now assuming that we have a valid Callback0 object we would call it like so
Callback0 *callBack = //...//;
callback->Call();
Okay so all the pieces are in place and all the remains is how to construct a callback.
The non-template way:
static void CallFoo(void* aThisPtr) {
SomeObject *so = reinterpret_cast<SomeObject*>(aThisPtr);
so->Foo();
}
SomeObject so();
FunctionCallback0 funCallfoo = CallFoo;
Callback0 callBackFoo(&so, funCallfoo);
callBackFoo.Call();
Ahhh! But this is bad, because the programmer has to think about
type safety everytime they have to create a new static function! This can really screw things up. So let the compiler generate this function using templates and maybe we can get typesafety for free!
The template way:
//this function creates a local static function which
//acts as the callback to the T's member function Foo()
//all typesafety checks are done via template instantiation
template<typename T>
static Callback0 MakeCallFoo(T* aThisPtr) {
struct CallFooFunctor { //trick to get a local function, see previous post
//this static function is the callback site
static void CallFoo(void* aThisPtr) {
T* t = reinterpret_cast<T*>(aThisPtr);
t->Foo();
}
};
//get the address of the callback defined above
FunctionCallback0 funCallFoo = &CallFooFunctor::CallFoo;
//construct a new callback and return it
return Callback0(aThisPtr, funCallFoo);
}
//main(){...}
SomeObject so();
Callback0 callBackFoo = MakeCallFoo(&so);
//callback now has the static inner function CallFooFunctior::CallFoo
callBackFoo.Call();
Wow! Look at that! ALL the yucky type-casting is done internally, transparent from the user! We now have a typesafe callback!
So putting this all together it looks like this:
static void Dispatch(Callback0& callback) {
callback.Call();
}
SomeObject so();
Callback0 callback = MakeCallFoo(&so);
Dispatch(callback);
Now for the grand finale, lets declare another class, SomeOtherObject, that also has a function foo, what do you think will happen?
struct SomeOtherClass {
SomeOtherObject() {}
void Foo() {}
};
SomeOtherObject soo;
Callback0 callback = MakeCallFoo(&soo);
Dispatch(callback);
Yes it works! And it will work with any object that declares Foo in the same way!
Practical uses:
1) Callbacks, use this as an alternative to multiple inheritance and virtuals (which REALLY slow down and bloat C++ code, especially in tight loops common in Observer patterns).
2) Allocators which need to know how to construct/destruct the objects they allocate/deallocate.
3) Interfaces, you can guarantee that a class fulfills the interface contract if the interface can bind to all the contracted member functions of a class (duck typing).
~Z~
No comments:
Post a Comment