Search code examples
c++templatesfactory-pattern

C++ Select Template Non-Type Parameters from Compile-Time Known Set/Enum at Run-Time


TL;DR: Looking for a pre-processor macro to generate all the if/else-if/else-error statements for all the combinations of pre-defined sets/enums of template parameters.


I have 3 subclasses (SubA<int a>, SubB<int a>, SubC<int a, int b>) of an abstract class (Base), so I can't initialise the abstract class but can initialise the subclasses. These subclasses also have one or two non-type template parameters.

class Base {...};  // abstract
class SubA<int a> : public Base {...};
class SubB<int a> : public Base {...};
class SubC<int a, int b> : public Base {...};

I have a benchmarking tool which pulls different benchmark configurations from a database (the subclass to run, template parameters, and arguments/workloads). The template parameters are definitive sets (a in {256, 512, 1024, 2048, 4096, 8192, 16384} and b in {1, 2, 3}).

I want to be able to have a Base object be instantiated as a subclass with the correct template parameters, without having to if/else if all of the possibilities/combinations. Is there a clean method to do this in C++, either with enums, arrays, or even preprocessor macros?

A long winded method would be to have many if-else statements, but I want a cleaner solution to pull the values out of an enum, set, or array if possible. Surely there's either a preprocessor way of generating the combinations, or a way to select out of an enum (so the compiler creates classes of all the enum combinations)?

Base *base = nullptr;

if (sub == "SubA") {
  if (a == 512) {
    if (b == 1) {
      base = new SubA<512, 1>();
    } else if (b == 2) {
      base = new SubA<512, 2>();
    } else if (b == 3) {
      base = new SubA<512, 3>();
    }
  } else if (a == 1024) {
    // ...
  }
} else if (sub == "SubB") {
  // ...
} else if (sub == "SubC") {
  // ...
}

if (base == nullptr) {
  throw std::exception();
}

As a further explanation, here's an equivalent solution written in JS:

class Base = {...};
function SubAFactory(a, b) = {return class SubA {... definition ...}};
function SubBFactory(a, b) = {return class SubB {... definition ...}};
function SubCFactory(a, b) = {return class SubC {... definition ...}};

const SubFactories = {
  SubA: SubAFactory,
  SubB: SubBFactory,
  SubC: SubCFactory
};

function BaseFactory(sub, a, b) {
  // NOTE: an if-else between subclasses would also be fine
  //       as long as the template args are "dynamic".
  return SubFactories[sub](a, b);
}

// retrieved from db at runtime, a and b values will
// always be part of a finite set known at compile time
const dbResult = {sub: 'SubA', a: 2048, b: 2};  
const sub = dbResult.sub;
const a = dbResult.a;
const b = dbResult.b;

const base = new BaseFactory(sub, a, b)(/* class constructor args */);

Solution

  • First let me tell you that if you have another way to solve your problem, do it. Then, there is a way to factorize your switching if your destination set of template instantiation is finite and not too large. Here is how:

    First, we need to be able to create references to macro and expand them so we can write something generic we can undef as soon as our factory is created. Those macro are:

    //force another dereferencing cycle
    # define EXPAND(...) __VA_ARGS__
    //black magic
    # define EMPTY(...)
    
    //dereference a macro reference on expansion
    # define DEFER(...) __VA_ARGS__ EMPTY()
    

    then come the real part: constructing a switch taking the code to forward compile-time types in each runtime case:

    #define makeCase(_value, appendTo, ...) case _value: \
                DEFER(appendTo)()(__VA_ARGS__, _value) \
                break; 
    
    #define makeRuntimeSwitch(_runtimeVal, appendTo, ...) switch( _runtimeVal) \
        { \
            makeCase(1, appendTo, __VA_ARGS__) \
            makeCase(2, appendTo, __VA_ARGS__) \
            makeCase(3, appendTo, __VA_ARGS__) \
            makeCase(4, appendTo, __VA_ARGS__) \
            makeCase(5, appendTo, __VA_ARGS__) \ 
        }
    

    this will append our template parameters to VA_ARGS until we have them all and are able to consume them with another macro :

    #define makeRuntimeConsume(_p1, _p2) return new templatedStuff<_p1, _p2>();
    

    now all we have to do is create references to our macro and use them to build our factory :

    #define makeRuntimeConsumeId() makeRuntimeConsume
    #define makeRuntimeSwitchId() makeRuntimeSwitch
    baseStuff* makeStuff(int a, int b)
    {
        EXPAND(EXPAND(makeRuntimeSwitch( a, makeRuntimeSwitchId, b, makeRuntimeConsumeId)));
    }
    //undef all macro, you don't need them anymore
    

    and spoof ugly switch generated by a macro for you. This can switch on anything runtime and return anything compile time (My example switched on n enums to forward as types to a (variadic) templated method)

    generated code will look like:

    switch (a) {
        case 1:
            switch (b) {
                case 1:
                    return new templatedStuff<1, 1>();
                case 2:
                ...
            }
        case 2:
            switch (b) {
                ...
            }
    }