Tuesday, February 14, 2012

Bridging templates and runtime parameters

I often write template classes in C++ for run-time efficiency. By providing the parameters of a class through template, the compiler can do a better job since it can do constant propagation. However, a problem with using templates like this is that is not obvious to reuse the template code when the parameters is only known at run-time. In this post I'll explain one way of writing template code such that it can be used with run-time parameters as well as compile-time parameters.

The idea is pretty simple: create a template class where the declaration of the (compile-time or run-time) parameters are extracted into a separate class.

The following two classes will be used to illustrate how to it.

template<int I0, int I1> struct CT {
  int func() { return I0 + I1; }
};

struct RT {
   int i0, i1;
   RT(int i0, int i1) : i0(i0), i1(i1) { }
   int func() { return i0 + i1; }
};

First, let's extract the parameters into a separate class to isolate the state of the class from the behaviour.


template<int P0, int P1> struct CTParams {
  static const int i0 = P0;
  static const int i1 = P1;
};
template<typename PARAMS> struct CT {
  int func() { return PARAMS::i0 + PARAMS::i1; }
};


struct RTParams {
   int i0, i1;
   RTParams(int i0, int i1) : i0(i0), i1(i1) { }
};

struct RT {
   RTParams params;
   RT(RTParams const& params) : params(params) { }
   int func() { return params.i0 + params.i1; }
};


Second, to make  the body of CT::func and RT::func look the same we rewrite the code of CT into:

template<typename PARAMS>
struct CT {
  PARAMS params; 
  int func() { return params.i0 + params.i1; }
};


Ok, we're getting closer. Now, the only difference between CT and RT is essentially that RT has a constructor and that CT is a template. We can create a new class CTRT which has both these features and thus replaces CT and RT:

template<typename PARAMS>
struct CTRT {
  PARAMS params;
  CTRT(PARAMS const& params = PARAMS()) : params(params) { }
  int func() { return params.i0 + params.i1; }
};

And we're done! The class CTRT can be used in two ways -- with run-time parameters or compile-time parameters, as follows:

void f() {
  // Look here! Compile-time parameters!
  CTRT<CTParams<10, 20> > ct;
  ct.func();
  // Look here! Run-time parameters! Omg!
  CTRT<RTParams> rt(RTParams(10, 20));
  rt.func();
}

Pretty straight-forward, right? There are a few downsides to this solution. The first is that there is a small overhead in memory usage of CTRT<CTParams<...>> compared to the original CT class. Luckily, this can easily be solved by using private inheritance (and the empty base class optimization).

Second, the implementation of CTRT<...> is much more complicated than the original RT class. However, this is a small price to pay compared to having multiple implementations.

Third, and most important, since CTRT is a template it must (usually) be implemented entirely in the header file, thus, increasing compilation times and potentially cause code bloat.

No comments: