2

I have a base class and a class template, and I want to be able to instantiate it as follows:

class Base
{
};



template<int i>
class Foo
{
};

namespace SomeEnum
{
enum
{
    First,
    Second,
    Third,
    ...
    Last
};
}

void bar()
{
    std::unique_ptr<Base> ptr{ nullptr };
    int fooType = rand() % SomeEnum::Last;

    ptr = std::make_unique<Foo<fooType>> // Causes error because I'm passing a run-time value to something that expects a compile-time value
}

The enum used to be a class enum but I changed it to a regular enum so that I could use the design presented above, since the enum in question is quite large. I thought I was being clever at the time but I now realize I had kind of forgotten that templates are handled at compile-time.

Is there any way to circumvent this, without completely changing my design? I could obviously do something like this:

switch(fooType)
{
case 0:
    ptr = std::make_unique<Foo<0>>();
    break;
case 1:
    ptr = std::make_unique<Foo<1>>();
    break;
...
}

But it doesn't look very elegant and pretty much removes the motivation for not just using a class enum since I will have to make a switch case for every value in the enum. Is there any other solution, or have I painted myself into a corner with this design?

JensB
  • 839
  • 4
  • 19

3 Answers3

4

A simple solution would be this:

namespace detail {
    template<size_t I>
    std::unique_ptr<Base> makeForIndex() {
        return std::make_unique<Foo<I>>();
    }

    template<size_t... Is>
    auto makeFoo(size_t nIdx, std::index_sequence<Is...>) {
        using FuncType = std::unique_ptr<Base>(*)();
        constexpr FuncType arFuncs[] = { 
            detail::makeForIndex<Is>...
        };
        return arFuncs[nIdx]();
    }
}

auto makeFoo(size_t nIdx) {
    return detail::makeFoo(nIdx, std::make_index_sequence<SomeEnum::Last>());
}

This would not require template recursion either and is rather easy to understand. We create an array of function pointers and index into that using the runtime value provided by the caller.

Now you can create your Foo<n> like

size_t n;
...
auto ptr = makeFoo(n);
Matthias Grün
  • 1,466
  • 1
  • 7
  • 12
2

Some metaprogramming utility stuff:

template<class T>
struct tag_t {using type=T;};
template<class T>
constexpr tag_t<T> tag{};
template<auto I>
using constant_t = std::integral_constant<std::decay_t<decltype(I)>, I>;
template<auto I>
constexpr constant_t<I> constant{};

template<class T, T...ts>
struct sequence{};

We need sequence because std::integer_sequence is overspecified.

Now we have our enum:

enum class SomeEnum {
  A,B,C,
  ValueCount,
};

Some enum helpers:

template<class E, std::size_t...Is>
constexpr auto EnumsSequence( std::index_sequence<Is...> ) {
  return sequence<E, static_cast<E>(Is)...>{};
}
template<class E> // requires E::ValueCount
constexpr std::size_t EnumsSize(tag_t<E> =tag<E>) {
  return static_cast<std::size_t>(E::ValueCount);
}
template<class E> // requires E::ValueCount
constexpr auto EnumsSequence(tag_t<E> =tag<E>) {
  return EnumsSequence<E>(std::make_index_sequence< EnumsSize(tag<E>) >{} );
}
template<class E>
using make_enums_sequence = decltype( EnumsSequence(tag<E>) );

we can now do make_enums_sequence<SomeEnum> and get std::integral_sequence<SomeEnum, SomeEnum::A, SomeEnum::B, SomeEnum::C>. Woo, really basic compile-time reflection. It works on any type with a ValueCount enumerator, or you can override EnumsSize(tag_t<E>) and/or EnumsSequence(tag_t<E>) for a specific type if you have a different convention.

Next we want to make a variant from an integral_sequence:

template<class S>
struct variant_over_helper;
template<class E, E...es>
struct variant_over_helper< sequence<E, es...> > {
  using type=std::variant< constant_t<es>... >;
};
template<class E>
using variant_over_t = typename variant_over_helper<make_enums_sequence<E>>::type;

let us think about the variant variant_over_t<SomeEnum>.

It contains an index of what alternative it holds. Alternative number 0 corresponds to SomeEnum::A, whose value is 0. Alternative number 1 corresponds to SomeEnum::B.

Basically, variant_over_t<SomeEnum> a struct containing an integer that lines up with SomeEnum's value. Except we can std::visit this variant. Bwahahah.

Next we need to be able to convert a runtime SomeEnum to a variant_over_t<SomeEnum>.

template<class E, E... es>
variant_over_t<E> get_variant_enum( sequence<E, es...>, E e ) {
  using generator = variant_over_t<E>(*)();
  const generator gen[] = {
    []()->variant_over_t<E> {
      return constant<es>;
    }...
  };
  return gen[ static_cast<std::size_t>(e) ]();
}
template<class E>
variant_over_t<E> get_variant_enum( E e ) {
  return get_variant_enum( EnumsSequence(tag<E>), e );
}

and we are done.

struct Base{
  virtual ~Base() {}
};

enum class SomeEnum
{
    First,
    Second,
    Third,
    ValueCount
};

template<SomeEnum i>
struct Foo:Base{};

void bar()
{
  std::unique_ptr<Base> ptr;
  SomeEnum fooType = static_cast<SomeEnum>( rand() % (int)SomeEnum::ValueCount );
  auto vFooType = get_variant_enum(fooType);
  ptr = std::visit( [](auto efoo)->std::unique_ptr<Base>{
    return std::make_unique<Foo<efoo>>();
  }, vFooType );
}

Live example.

Note that this requires that your enum be contiguous and start at 0 (for the gen[] array lookup). Some work could be done to handle non-contiguous cases, but that gets complex, so no don't do it. (use EnumsSequence to either build a if-tree to find what index to use with std::in_place_index_t<I> or constant<E>, and/or build an enum-to-index mapping.)

This code runs in O(1) time, not counting data that can be initialized once.

I used ValueCount as the number of elements in the enum. If you really need it to be called Last, simply add:

constexpr std::size_t EnumsSize( tag_t<decltype(SomeEnum::Last)> ) {
  return static_cast<std::size_t>(SomeEnum::Last);
}

to the namespace of tag_t or the namespace of SomeEnum, and all of the code automatically adapts via argument dependent lookup.

Best part? All of that metaprogramming to create get_variant_enum complies down to:

    movzx   edx, ah
    mov     WORD PTR [rsp+14], ax

ie, copy some bytes over.

The visit call is a bit more complex.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • It's also worth noting that as written this requires that the value passed to get_variant_enum be less than ValueCount (neither ValueCount or a valid enum value above the enumerators). I'd probably at least assert that before the lookup. – Jeff Garrett Aug 19 '21 at 15:32
1

There is possibility to make it feasible. That is to use tuple which stores std::make_unique functions corresponding to enum size.

A helper method to generate the tuple using index_sequence

template<std::size_t... I>
auto generate(std::index_sequence<I...>)
{
    return std::make_tuple(std::make_unique<Foo<I>>...);
}

Next, to be able to iterate over it and find the index matching with the wanted value the recursive call is applied to increase it by 1.

template<std::size_t I = 0, typename... Tp, typename std::enable_if<I == sizeof...(Tp)>::type* = nullptr>
void make(const std::tuple<Tp...> &, std::unique_ptr<Base>&, int) // Unused arguments are given no names.
  { return;}

template<std::size_t I = 0, typename... Tp, typename std::enable_if<I < sizeof...(Tp)>::type* = nullptr>
void make(const std::tuple<Tp...>& t, std::unique_ptr<Base>& p, int value)
{
  if (value == I) {
    p = std::get<I>(t)();      
  }
  make<I + 1, Tp...>(t, p, value);
}

Combine two above methods, you can simply do

auto t = generate(std::make_index_sequence<4>{});
std::unique_ptr<Base> ptr{ nullptr };
int fooType = rand() % SomeEnum::Last;       
make(t, ptr, fooType);
nhatnq
  • 1,173
  • 7
  • 16