Search code examples
c++ooptemplatesinterfacegeneric-programming

How to avoid exponential increase in code when choosing template arguments at run-time


Consider a bunch of fundamental types, Foo, all with unique implementations of a common method, Bar(). I can combine Foo1, Foo2, Foo5 like so:

CombinedFoo<Foo1, Foo2, Foo5> combined_foo;

Which uses recursive inheritance to make CombinedFoo effectively the same as:

class CombinedFoo <Foo1, Foo2, Foo5>
{
    Foo1 foo1;
    Foo2 foo2;
    Foo5 foo5;

public:

    void Bar ()
    {
        foo1.Bar();
        foo2.Bar();
        foo5.Bar();
    }
};

This is handy, but I run into a problem when I want to choose at run-time which Foo types to combine (into a single object) to send to function, say:

template <typename Foo> void Do (Foo && foo);

An example solution with ifs and switchs to solve the 3 option version:

int result = 0;

if (criteria_for_foo1)
    result += 100;

if (criteria_for_foo2)
    result += 10;

if (criteria_for_foo3)
    result += 1;

switch (result)
{
     case 001 : Do(Foo3());
                  break;

     case 010 : Do(Foo2());
                  break;

     case 011 : Do(CombinedFoo<Foo2, Foo3>());
                  break;

     case 100 : Do(Foo1());
                  break;

     case 101 : Do(CombinedFoo<Foo1, Foo3>());
                  break;

     case 110 : Do(CombinedFoo<Foo1, Foo2>());
                  break;

     case 111 : Do(CombinedFoo<Foo1, Foo2, Foo3>());
                  break;

     default : break; 
}

The if statements are fine, they grow linearly, but the switch statement grows exponentially as we have more choices. My real-world problem has 4 options and so I need to handle 16 cases that I'd rather not have to maintain.

I believe that there's no way to avoid the executable from growing exponentially, but is there a way to avoid this in the c++ code (without introducing significant inefficiencies in the Bar method)? Or is there a known work-around / alternative for this generic problem?

EDIT:

For clarity: Do(Foo1); Do(Foo2) is not the same as Do(CombinedFoo<Foo1, Foo2>()), and it's crucial that the Foos are combined for a single call to Do.

For those who wanting to know the real-world motivation: it's for an optimisation problem, where my Foos are really Generators of fundamental Moves that can edit my solution, this is then sent into various heuristics. If I was to send in just one Generator at a time then my solvers would be repeating the same type of move thousands of times, and so invariably being unproductive / stuck at local minima (considering the same type of move repeatedly is well known to have this effect).

The reason I select some of these template parameters at run-time is because some Moves aren't appropriate for certain problem instances (which my program doesn't become aware of till run-time).


Solution

  • Generic way to choose types from any type list at run-time:

    Here's the interface:

    template <typename ... Types, typename Functor>
    void ChooseTypes (const bool chosen[], Functor && f)
    

    Where chosen[0] = true signals choosing the first type from the list.

    Internally the types are forwarded to the functor like so: f()<ChosenTypes...>() with ordering maintained.


    How to use:

    struct MyFunctor
    {
        // data/references can be stored/passed here
    
        template <typename ... Ts>
        void operator () ()
        {
            // Ts = the chosen types
    
            // Here's where we get to use them...
        }
    };
    
    // later in func/method:
    
    bool chosen[5];
    
    // choose which types you want
    
    MyFunctor f;
    
    // pass data in the functor
    // (const/& by constructor)
    
    // Then this will call the functor with the chosen types:
    ChooseTypes<T1, T2, T3, T4, T5>(chosen, f);
    

    Library code:

    namespace ChooseTypesRecursive // internal use only
    {
    
    // CTR = ChooseTypesRecursive
    
    template <int N, typename Functor>
    struct CTR
    {
        using Next = CTR<N-1, Functor>;
    
        template <typename CandidateType, typename ... Args>
        static void Ctr (const bool * chosen, Functor & f)
        {
            if (*chosen)
            {
                Next::template Ctr<Args..., CandidateType>
                    (++chosen, f);
            }       
            else
            {
                Next::template Ctr<Args...>
                    (++chosen, f);
            }
        }
    };
    
    template <typename Functor>
    struct CTR <0, Functor>
    {
        template <typename ... ChosenTypes>
        static void Ctr (const bool *, Functor & f)
        {
            f.template operator()<ChosenTypes...>();
        }
    };
    
    } // namespace ChooseTypesRecursive
    
    template <typename ... Types, typename Functor>
    void ChooseTypes (const bool chosen[], Functor && f)
    {
        constexpr int N = sizeof...(Types);
    
        using namespace ChooseTypesRecursive;
        
        CTR<N, Functor>::template Ctr<Types...>(chosen, f);
    }
    

    Solving the specific question:

    Live demo