是否无法将 运行 次整数作为模板参数传递?

Is it impossible to pass a run-time integer as a template argument?

我有一个基础 class 和一个 class 模板,我希望能够按如下方式实例化它:

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
}

枚举曾经是 class 枚举,但我将其更改为常规枚举,以便我可以使用上面介绍的设计,因为所讨论的枚举非常大。我当时以为我很聪明,但我现在意识到我有点忘记了模板是在编译时处理的。

有没有办法在不完全改变我的设计的情况下避免这种情况?我显然可以做这样的事情:

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

但它看起来不是很优雅,而且几乎消除了不只使用 class 枚举的动机,因为我将不得不为枚举中的每个值创建一个 switch case。有没有其他的解决办法,或者我是不是被这个设计逼到了墙角?

有可能让它变得可行。即使用 tuple 存储 std::make_unique 枚举大小对应的函数。

使用 index_sequence

生成 tuple 的辅助方法
template<std::size_t... I>
auto generate(std::index_sequence<I...>)
{
    return std::make_tuple(std::make_unique<Foo<I>>...);
}

接下来,为了能够对其进行迭代并找到与所需值匹配的索引,应用递归调用将其增加 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);
}

结合以上两种方法,你可以简单地做

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

一些元编程实用工具:

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{};

我们需要 sequence,因为 std::integer_sequence 指定过多。

现在我们有了枚举:

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

一些枚举助手:

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>) );

我们现在可以做 make_enums_sequence<SomeEnum> 并得到 std::integral_sequence<SomeEnum, SomeEnum::A, SomeEnum::B, SomeEnum::C>。哇,非常基本的编译时反射。它适用于具有 ValueCount 枚举器的任何类型,或者如果您有不同的约定,您可以覆盖特定类型的 EnumsSize(tag_t<E>) and/or EnumsSequence(tag_t<E>)

接下来我们要从 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;

让我们考虑变体 variant_over_t<SomeEnum>

它包含它持有的备选方案的索引。备选编号0对应SomeEnum::A,其值为0。备选编号1对应SomeEnum::B.

基本上,variant_over_t<SomeEnum> 一个包含与 SomeEnum 的值对齐的整数的结构。除了我们可以 std::visit 这个变体。哇哈哈

接下来我们需要能够将运行时 SomeEnum 转换为 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 );
}

我们完成了。

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.

请注意,此 要求 您的枚举是连续的并从 0 开始(对于 gen[] 数组查找)。可以做一些工作来处理不连续的情况,但这会变得很复杂,所以不要不这样做。 (使用 EnumsSequence 构建一个 if-tree 以找到要与 std::in_place_index_t<I>constant<E> 一起使用的索引,and/or 构建一个枚举到索引的映射。)

这段代码运行时间复杂度为O(1),不包括可以初始化一次的数据。

我使用 ValueCount 作为枚举中的元素数。如果你真的需要它被称为Last,只需添加:

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

tag_t 的命名空间或 SomeEnum 的命名空间,所有代码都通过参数相关查找自动适应。

最精彩的部分?创建 get_variant_enum 的所有元编程都符合:

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

即复制一些字节过来。

访问调用有点复杂。

一个简单的解决方案是这样的:

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>());
}

这也不需要模板递归,而且很容易理解。我们创建一个函数指针数组,并使用调用者提供的运行时值对其进行索引。

现在您可以创建您的 Foo<n>

size_t n;
...
auto ptr = makeFoo(n);