Recently, I found myself wondering how I could set up something like this in C++:
DeferredCalls<SomeClass> d(new SomeClass);
d.defer<void>(&SomeClass::SomeVoidReturningMember)
.defer<int>(&SomeClass::SomeIntReturningMember, &someFuncToValidateReturnValue)
.defer<float>(&SomeClass::SomeFloatReturningMember, &someFuncToValidateReturnValue, someParameter);
d.callChain(); // or d() if using operator()();
Google gave me almost nothing, mostly because I wasn't sure about what to Google. I did, however, find an interesting, heavily templated, approach that the project LuaBrigde uses to enable calling of arbitrary member functions from Lua. After skimming through the code, I decided that it should be possible for me to do something similar. This is how I did it.
Disclaimer: If you don't like templates, you have been warned.
First, we need a factory class to enable us to do what I described above. Let's start with a member function that has no parameters and some arbitrary return value.
template <class T>
class DeferredCallFactory {
public:
explicit DeferredCallFactory(T * const t) : t_(t) {}
template <typename Ret, typename Sig = Ret(T::*)()>
DeferredCallFactory &deferCall(Sig fp) {
//do something
return *this;
}
private:
T * const t_;
};
Simple enough, right? However, we need some object to hold information about the call we want to (later) make. Let's stop for a while and think about this. The information holding object(s) will also need to be templated as we want to support several different types of member functions. But we also need to store these objects somewhere, and since they will be templated (differently!) we can't just use a homogenous STL-container like std::list. This is where one of the limitations of the system comes into play. If we do the obvious, use a non-templated base class which every other information holder inherits from, we can use any homogenous container we want. But any virtual methods we implement cannot be templated. Do you see where I'm going with this?
class DeferredBase {
public:
virtual void call() = 0;
};
Now, there's our problem. We can't capture return values (if we don't overload that call method with every conceivable return type) with this approach. So we need to invent something that allows us to at least inspect what the chained functions are returning. More on that later, let's continue with the most basic Deferred.
template <class T, typename Ret, typename Sig>
class DeferredCall : public DeferredBase {
public:
DeferredCall(T * const t, Sig method) :
t_(t), m_(method) {}
virtual void call() {
(t_->*m_)();
}
private:
T *const t_;
Sig m_;
};
Now we modify the factory, first adding a container:
std::list<DeferredBase *> deferred_;
Then implementing the deferCall stub:
template <typename Ret, typename Sig = Ret(T::*)()>
DeferredCallFactory &deferCall(Sig fp) {
deferred_.push_back(
new DeferredCall<T, Ret, Sig>(t_, fp)
);
return *this;
}
Now we have a system that can chain calls to arbitrary member functions with the signature
ReturnValue (ClassType::*)(void)
Let's make it more interesting by adding validator functions. Time for a new derived class.
template <class T, typename Ret, typename Sig, typename Vald>
class DeferredCallWithValidator : public DeferredBase {
public:
DeferredCallWithValidator(T * const t, Sig method, Vald validator) : t_(t), m_(method), v_(validator) {}
virtual void call() {
Ret val = (t_->*m_)();
v_(val);
}
private:
T * const t_;
Sig m_;
Vald v_;
};
And then we modify the factory to support this new deferred.
template <typename Ret, typename Sig = Ret(T::*)(), typename Vald = void(*)(Ret)>
DeferredCallFactory &deferCall(Sig fp, Vald v) {
deferred_.push_back(
new DeferredCallWithValidator<T, Ret, Sig, Vald>(t_, fp, v)
);
return *this;
}
Now were golden! This approach is a breeze to generalize further, the next step being to add DeferredXYZ-classes that support templated parameters to the member function calls.
Finally, some syntactic sugar:
//(in DeferredCallFactory)
void operator()() {
std::list<DeferredBase *>::const_interator it;
for (it = deferred_.begin(); it != deferred_.end(); ++it) {
(*it)->call();
}
}
What's the use case you wonder? In my case it's being able to specify calls to an API under test in a nice way at one point in the program, and then being able to make those calls (multiple times), by just writing:
factory();
The boilerplate code for actually doing this is horrible, but when it's done you don't have to see it anymore.
1 comment:
Post a Comment