2

Say I want to be able to create objects for a range of template parameter

template<int a> class Myclass : public MyclassSuper{...};

And I want to create objects like

Myclass<runtime_dependent_int> a(b);

Now this is not allowed because runtime_dependent_int is not known at compile time.

How can I circumvent this limitation systematically? I was thinking maybe something Factory-like?

class MyclassFactory {

  static MyclassSuper* createObject(const int runtime_dependent_int, constructor_args b){...}
  ...
};

The easy and ugly way would be just a big switch-case inside createObject which goes through the whole set of ints I am interested in and casts

Myclass<runtime_dependent_int>* 

into

MyclassSuper*

Maybe it can be done better, with for-loop or some clever template meta-magic?

  • 1
    Which integers are allowed as input? All of them? A given interval, for example, 0, 1, ..., N ? An arbitrary but known list of numbers? – pschill Apr 25 at 11:51
  • @pschill: say we have a list of allowed inputs determined at compile time. – mathreadler Apr 25 at 11:58
  • I realize this goes against the spirit of the question, but have you considered not making that thing a template? – Sebastian Redl Apr 26 at 6:45
  • @SebastianRedl yes I have considered it and if I test compilation tools and inspect the generated code having compile time constants allows the compiler to issue very much more efficient SSIMD instructions instead of basically totally non-unrolled loop. – mathreadler Apr 26 at 7:04
4

A switch-case statement isn't that bad: It is easy to implement, easy to understand, and easy to maintain if the number of allowed values is limited (say less than 30). However, if the number of allowed values is large, the switch-case statement might become a beast that is thousands of lines long.

One way to get rid of such a switch-case statement is using a std::map with creator functions. Consider the following:

struct FooBase
{};

template <int N>
struct Foo : public FooBase
{};

using FooCreator = std::function<std::unique_ptr<FooBase>()>;

std::map<int, FooCreator> initializeFooCreatorMap();

std::unique_ptr<FooBase> createFoo(int const n)
{
    static std::map<int, FooCreator> const creatorMap = initializeFooCreatorMap();
    auto const it = creatorMap.find(n);
    if (it != creatorMap.end())
    {
        return it->second();
    }
    return nullptr;
}

Here, the switch-case statement was replace with a map lookup. Performance-wise this isn't too bad: The complexity of std::map::find is logarithmic, so searching a map with 2^32 elements takes about 32 comparisons. The interesting part here is how initializeFooCreatorMap() is implemented. Unfortunately, you have to instantiate the template with all allowed integer values. The easiest way is to hardcode

m.emplace(n, [](){ return std::unique_ptr<Foo<n>>(); });

for all values of n into initializeFooCreatorMap(). However, this isn't much better than a switch-case statement, since you still need one line of code for each allowed value.

You can use template recursion to simplify this. For example, if you want to instantiate the templates for a fixed list of integers, you can do the following:

template <int N, int... Rest>
struct FooCreatorInserter
{
    static void insert(std::map<int, FooCreator>& creatorMap)
    {
        FooCreatorInserter<N>::insert(creatorMap);
        FooCreatorInserter<Rest...>::insert(creatorMap);
    }
};

template <int N>
struct FooCreatorInserter<N>
{
    static void insert(std::map<int, FooCreator>& creatorMap)
    {
        creatorMap.emplace(N, []() { return std::make_unique<Foo<N>>(); });
    }
};

std::map<int, FooCreator> initializeFooCreatorMap()
{
    std::map<int, FooCreator> creators;
    FooCreatorInserter<2, 3, 5, 7, 11, 13, /*...*/>::insert(creators);
    return creators;
}

Instead of maintaining a large switch-case statement, you only need to maintain a single template argument list, namely FooCreatorInserter<2, 3, 5, 7, 11, 13, /*...*/>.

If you cannot use variadic template arguments, you can use other template recursion tricks to fill the list. For example, you can use the following to initialize the creator map with all integers from 100 to 200:

template <int N>
struct RecursiveFooCreatorInserter
{
    static void insert(std::map<int, FooCreator>& creatorMap)
    {
        creatorMap.emplace(N, [](){ return std::make_unique<Foo<N>>(); });
        RecursiveFooCreatorInserter<N - 1>::insert(creatorMap);
    }
};

template <>
struct RecursiveFooCreatorInserter<100>
{
    static void insert(std::map<int, FooCreator>& creatorMap)
    {
        creatorMap.emplace(100, []() { return std::make_unique<Foo<100>>(); });
    }
};

std::map<int, FooCreator> initializeFooCreatorMap()
{
    std::map<int, FooCreator> creators;
    RecursiveFooCreatorInserter<200>::insert(creators);
    return creators;
}
  • 1
    Wow very nice explanation how to use modern c++ to do this. Thank you. – mathreadler Apr 25 at 13:40
  • 1
    Might be worth talking about std::make_integer_sequence for "every number up to N" – Caleth Apr 25 at 14:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.