The Solution: A Mixture of Compile-time and Run-time Programming
First, you need a way to pass a type without passing an instance of the type, and you need a way to store that type and be able to use it afterwards. You also need a way to encapsulate an interface contract by defining only the signatures of the functions you will call and making them depend on the types that will be passed to you.
Passing Types Without Passing Instances
In the different libraries you have at your disposal, there are two that are very interesting for passing types around: loki, which contains a Type2Type class and Boost.MPL, which contains boost::mpl::identity. The latter suits our purposes a bit better because we'll be using the Boost MPL later on, and Boost.MPL has a coherent way of presenting meta-functions [2].
So, to pass a type to a function foo without passing an object of that type, the following code will work just fine:
#include <boost/mpl/identity.hpp> template < typename Type > void foo(const boost::mpl::identity< Type > &) { /* do something with Type */ } int main() { foo(boost::mpl::identity< int >()); return 0; }
Here, main passes the type int to the function foo. The nice thing about this method is that no object is actually created: The compiler will use the boost::mpl::identity instance only to determine the type to use, but will optimize-away its instance (usually even in the lowest optimization settings[3]).
Determining and Implementing a Common Interface and Interface Contract
Most of our data-batch types implement a function that accepts a visitor, takes a reference to it as a parameter, and returns void. The type of the data batch and of the visitor change, as do the names of the functions that accept those visitors, but the "signature" described above is a common denominator that we can translate into code:
void (DataBatchType::*accept_visitor_)(VisitorType &)
is a pointer to a function in a class called DataBatchType that takes a reference to an instance of something of type VisitorType. The pointer itself is called accept_visitor_[4].
This means that if we have both types, we can, in theory, create a pointer to something that we can call with a visitor as a parameter. One problem is, though, that some data-batch types don't implement quite the same signature; for example, one of the data-batch types we have to support looks like this:
struct DataBatch { Visitor & visit(Visitor & visitor) { /* do something with the visitor */ return visitor; } };
A straightforward approach to a solution for this problem would be to create a type trait for the data-batch type, as follows:
template <> struct DataBatchTraits< DataBatch > { static void acceptVisitor(DataBatch * data_batch, Visitor & visitor) { data_batch->visit(visitor); } };
which is one way of implementing compile-time polymorphism: Conceptually, trait classes are much like compile-time interfaces to a given class. Though you can't create an instance of a trait class without knowing the complete type of the class that implements its interface, you don't have to know that while writing the code that uses the interface, as long as you've determined the interface you need when writing that code. We've determined that we needed something that returned void and takes a reference to a visitor as parameter, so our traits class will provide just that, while wrapping the actual implementation, which only the compiler needs to know at the right time.
This means that once we've determined a common interface for all data-batch types, we retain a certain flexibility on the data-batch implementation side (which we would not have if we had a common base class [5].This is especially useful as we're integrating into exisiting code which painfully lacks coherency but is also painfully cohesive.
However, just using a trait is not quite enough: We need to remember that the Task class' implementation will not know the type of the data-batch, and will therefore not be able to create an instance of a trait class, specialized for the type of the data-batch. We therefore need to return to run-time polymorphism and implement an Adapter class that exposes the common interface the traits will also expose. That Adapter class will look like this:
struct DataBatchAdapter { virtual ~DataBatchAdapter(); virtual void visit() = 0; };
To use this class, a derived class will need to be implemented that uses the trait class discussed above. Once we've come to this conclusion, the solution is self-evident:
namespace Details { template < typename DataBatchType, typename VisitorType > struct DataBatchAdapter : public ::DataBatchAdapter { DataBatchAdapter(DataBatchType * data_batch) : data_batch_(data_batch) { /* no-op */ } virtual void visit() { VisitorType visitor(VisitorTraits< VisitorType >::create(data_batch_)); DataBatchTraits< DataBatchType >::acceptVisitor(data_batch_, visitor); } DataBatchType * data_batch_; }; }
The following code will, therefore, create an instance of the adapter and call the visit function on it:
DataBatch data_batch; std::auto_ptr< DataBatchAdapter > adapter(new Details::DataBatchAdapter< DataBatch, Visitor >(&data_batch)); adapter->visit();
Note, by the way, that this code explicitly uses a pointer in order to keep the run-time polymorphism possible.