(C++)编译时自动生成switch语句case
在我的程序中,我有一些看起来像这样的代码:一组函数或类,它们通过模板特化独特地实现了一个通用模板:
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 来实现“展开”功能,以便源代码的复制和粘贴发生在语法检查之前而不是之后。
我玩过的另一种解决方案是模仿 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 语句在编译时在指令内存中生成它们的跳转表,而我在这里所做的是在运行时在数据内存中生成跳转表。
我认为只要函数具有足够长的执行时间,轻微的运行时损失就无关紧要,相比之下,此时的开销可以忽略不计。
回答
你可以这样做:
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);
然而,由于可能存在深度递归,编译时间可能会受到影响,具体取决于专业化的数量。