Jon Purdy Jon Purdy - 2 months ago 9
C++ Question

Emulating Dynamic Dispatch in C++ based on Template Parameters

This is heavily simplified for the sake of the question. Say I have a hierarchy:

struct Base {
virtual int precision() const = 0;
};

template<int Precision>
struct Derived : public Base {

typedef Traits<Precision>::Type Type;

Derived(Type data) : value(data) {}
virtual int precision() const { return Precision; }

Type value;

};


I want a non-template function with the signature:

Base* function(const Base& a, const Base& b);


Where the specific type of the result of the function is the same type as whichever of
a
and
b
has the greater
Precision
; something like the following pseudocode:

Base* function(const Base& a, const Base& b) {

if (a.precision() > b.precision())

return new A( ((A&)a).value + A(b.value).value );

else if (a.precision() < b.precision())

return new B( B(((A&)a).value).value + ((B&)b).value );

else

return new A( ((A&)a).value + ((A&)b).value );

}


Where
A
and
B
are the specific types of
a
and
b
, respectively. I want
function
to operate independently of how many instantiations of
Derived
there are. I'd like to avoid a massive table of
typeid()
comparisons, though RTTI is fine in answers. Any ideas?

Answer

You can't have function() delegate to templated code directly without selecting between a massive list of all possible types, because templates are expanded at compile-time, and at compile-time function() does not know what derived types it will actually be called with. You need to have compiled instantiations of the templated code for every version of your templated operation function that will be required, which is potentially an infinite set.

Following that logic, the only place that knows all of the templates that might be required is the Derived class itself. Thus, your Derived class should include a member:

Derived<Precision> *operation(Base& arg2) {
  Derived<Precision> *ptr = new Derived<Precision>;
  // ...
  return ptr;
}

Then, you can define function like so, and do the dispatching indirectly:

Base* function(const Base& a, const Base& b) {
  if (a.precision() > b.precision())
    return a.operation(b);
  else 
    return b.operation(a);
}

Note that this is the simplified version; if your operation is not symmetric in its arguments, you'll need to define two versions of the member function -- one with this in place of the first argument, and one with it in place of the second.

Also, this has ignored the fact that you need some way for a.operation to get an appropriate form of b.value without knowing the derived type of b. You'll have to solve that one yourself -- note that it's (by the same logic as earlier) impossible to solve this by templating on the type of b, because you're dispatching at runtime. The solution depends on exactly what types you've got, and whether there is some way for a type of higher precision to pull a value out of an equal-or-lower precision Derived object without knowing the exact type of that object. This may not be possible, in which case you've got the long list of matching on type IDs.

You don't have to do that in a switch statement, though. You can give each Derived type a set of member functions for up-casting to a function of greater precision. For example:

template<int i>
upCast<Derived<i> >() {
  return /* upcasted value of this */
}

Then, your operator member function can operate on b.upcast<typeof(this)>, and will not have to explicitly do the casting to get a value of the type it needs. You may well have to explicitly instantiate some of these functions to get them to be compiled; I haven't done enough work with RTTI to say for sure.

Fundamentally, though, the issue is that if you've got N possible precisions, you've got N*N possible combinations, and each of these will in fact need to have separately-compiled code. If you can't use templates in your definition of function, then you have to have compiled versions of all N*N of these possibilities, and somehow you have to tell the compiler to generate them all, and somehow you have to pick the right one to dispatch to at runtime. The trick of using a member function takes out one of those factors of N, but the other one remains, and there's no way to make it entirely generic.

Comments