(C++)在编译时自动生成switch语句的case

8
在我的程序中,我有一些代码看起来像这样:通过模板特化唯一实现一个共同模板的函数或类的集合:
constexpr int NUM_SPECIALIZATIONS = 32;

template<int num>
void print_num(){}

template<>
void print_num<0>(){cout << "Zero" << endl;}

template<>
void print_num<1>(){cout << "One" << endl;}

template<>
void print_num<2>(){cout << "Two" << endl;}

// etc, ...

template<>
void print_num<31>(){cout << "Thirty-One" << endl;}

我有一个变量,它的值只有在运行时才知道:

int my_num;
cin >> my_num; // Could be 0, could be 2, could be 27, who tf knows

我需要调用与变量值相对应的模板特化。由于无法将变量用作模板参数,因此我需要创建一种类似“解释器”的东西:

switch(my_num)
{
  case 0:
  print_num<0>();
  break;
  case 1:
  print_num<1>();
  break;
  case 2:
  print_num<2>();
  break;
  // etc, ...
  case 31:
  print_num<31>();
  break;
}

我注意到这段代码的第一件事是它很重复。肯定有某种技巧可以程序生成这段代码。
另外我还注意到,由于它与模板特化耦合,维护起来很不方便。每次我想添加新的模板特化时,我都需要更新解释器。
理想情况下,我应该能够使用某种模板魔法,在编译时自动生成解释器,以便两个代码部分保持解耦,同时我仍然可以保持switch语句的效率。
// Copies and pastes the code found in template lambda "foo",
// Replacing all occurrences of its template parameter with values from
// "begin" until "end"
template<auto begin, auto end>
inline void unroll(auto foo)
{
  if constexpr(begin < end)
  {
    foo.template operator()<begin>();
    unroll<begin + 1, end>(foo);
  }
}

// A template lambda which generates a generic switch case for the interpreter
auto template_lambda = [&]<int NUM>()
{
  case NUM:
  print_num<NUM>();
  break;
};

// The interpreter; contains the code "case NUM: print_num<NUM>(); break;"
// repeated for all ints NUM such that 0 <= NUM < NUM_SPECIALIZATIONS
switch(my_num)
{
  unroll<0,NUM_SPECIALIZATIONS>(template_lambda);
}

很遗憾,这段代码无法编译。它无法通过语法检查器,因为 lambda 函数中的 "case" 和 "break" 语句从技术上讲还没有在 switch 语句中。

为了使其正常工作,我需要使用宏而不是模板和 lambda 来实现 "unroll" 函数,以便复制和粘贴源代码发生在语法检查之前而不是之后。
我尝试过的另一种解决方案是模拟 switch 语句在低级别上的操作。我可以创建一个函数指针数组作为跳转表:
std::array<std::function<void()>,NUM_SPECIALIZATIONS> jump_table;

那么,我可以使用unroll函数来填充跳转表,而无需逐个输入各种模板特化的指针。这样做可以使内容更加简单易懂。

template<auto begin, auto end>
inline void unroll(auto foo)
{
  if constexpr(begin < end)
  {
    foo.template operator()<begin>();
    unroll<begin + 1, end>(foo);
  }
}

unroll<0,NUM_SPECIALIZATIONS>([&]<int NUM>()
{
  jump_table[NUM] = print_num<NUM>;
});

现在解释器和模板特化已经解耦。

当我想要调用与运行时变量my_num的值对应的模板特化时,我可以这样做:

jump_table[my_num](); // Almost like saying print_num<my_num>();

甚至可以在运行时修改跳转表,只需将数组的内容重新分配给不同的函数名称:

jump_table[NUM] = /* a different function name */;

这种方法的缺点是,与 switch 语句相比,仅仅访问数组元素就会产生轻微的运行时惩罚。我认为这是固有的,因为 switch 语句在编译时将其跳转表生成指令内存中,而我在这里生成了在运行时数据内存中的跳转表。
我想只要函数的执行时间足够长,那么轻微的运行时惩罚就不太重要了,在这种情况下,开销与其相比就可以忽略不计了。

5
除了在运行时才知道值的情况下,您是否还在其他情况下使用print_num?如果没有,为什么不将其转换为常规函数(而不是函数模板),并使用参数调用它?void print_num(size_t x) { static const char* arr[] = {"Zero", "One", ... }; std::cout << arr[x] << '\n'; }(+如果需要,添加边界检查) - Ted Lyngmo
1
这与 https://dev59.com/L0vSa4cB1Zd3GeqPf6B9 有些关联,更多细节可在此处找到 https://stackoverflow.com/questions/2157149/runtime-typeswitch-for-typelists-as-a-switch-instead-of-a-nested-ifs。除非您想像Ted建议的那样改变方法,否则我认为没有绕过宏的办法。 - 463035818_is_not_a_number
是的,函数指针数组可能是一个不错的方法。您不需要使用std::function及其类型转换开销来保存简单的函数指针。 typedef void (*fptr)(); std::array<fptr, NUM_SPECIALIZATIONS> jump_table; - Pete Becker
@Ted Lyngmo,这是因为在我的项目中,每个专业化非常独特。我展示的例子是简化的,但在我的项目中,每个函数的行为和它所能访问的数据将会非常不同,因此无法泛化。 - Logan Schlick
3个回答

5
你可以像这样做:
namespace detail {
    template<size_t I>
    void run(size_t idx) {
        if (I == idx) {
            print_num<I>();
        } 
        else if constexpr (I+1 < NUM_SPECIALIZATIONS) {
            run<I+1>(idx);
        }
    }
}

void run(size_t idx) {
    detail::run<0>(idx);
}

那么就这样调用它:
int my_num;
cin >> my_num;
if (my_num >= 0)
   run(my_num);

然而,由于可能存在深度递归,具体取决于特化的数量,编译时间可能会受到影响。


编译器是否保证将那一系列的if语句简化为单个switch语句? - Logan Schlick
@LoganSchlick 不会。这将是一个递归调用,而且_一些_优化_可能_会消除递归。 - Ted Lyngmo
实际上,我查看了汇编指令,即使启用编译器优化,它也会编译成一长串if语句。这意味着该解决方案具有随着情况数量增加而呈O(n)复杂度。此外,您不能在if语句之前放置else,否则语法检查器会报错。 - Logan Schlick
1
关于 else 的观点很好。我已经编辑了我的答案 :-) - Matthias Grün
1
@LoganSchlick 对 Matthias 的好方法的另一种看法是将其转换为二分查找像这样。如果您有很多专业化,与从开头顺序搜索直到找到数字相比,平均比较次数(<每个分支1.5)可以大大减少 - Ted Lyngmo
显示剩余3条评论

4

除了另一个答案之外,让我发表一个基于Boost.Mp11的解决方案,它只有一行:

std::size_t my_num;
std::cin >> my_num;

boost::mp11::mp_with_index<NUM_SPECIALIZATIONS>(
    my_num, [](auto index) { print_num<index>(); });

这里的index变量具有类型std::integral_constant<std::size_t, i>,该类型可隐式转换为std::size_t。转换运算符是constexpr的。


2
非递归版本
template <class T, class F, T... I>
bool to_const(T value, F&& fn, std::integer_sequence<T, I...>) {
    return (
        (value == I && (fn(std::integral_constant<T, I>{}), true))
        || ... // or continue
    );
}

template <std::size_t Size, class T, class F>
bool to_const(T value, F&& fn) {
    return to_const(value, fn, std::make_integer_sequence<T, Size>{});
}

使用方法

int my_num;
cin >> my_num;

bool found = to_const<NUM_SPECIALIZATIONS>(my_num, [](auto I)
{
    print_num<I>();
});

GCC 11能够完全优化掉to_const()中的比较,使用输入索引构建跳转表来选择正确的方法。
jmp     [QWORD PTR .L4[0+rax*8]]

其中raxmy_num.L4是跳转表。

在此处查看结果here

相同的优化,在GCC版本5中已经可以通过直接实现跳转表来实现。

int my_num;
cin >> my_num;

auto print_jump_table = []<int... I>(std::integer_sequence<int, I...>) {
    static constexpr void(*jump_table[])() = { print_num<I>... };
    return jump_table;
}(std::make_integer_sequence<int, NUM_SPECIALIZATIONS>{});

if (unsigned(my_num) < NUM_SPECIALIZATIONS) {
    print_jump_table[my_num]();
}

点击这里查看结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接