在我的程序中,我有一些代码看起来像这样:通过模板特化唯一实现一个共同模板的函数或类的集合:
我注意到这段代码的第一件事是它很重复。肯定有某种技巧可以程序生成这段代码。
另外我还注意到,由于它与模板特化耦合,维护起来很不方便。每次我想添加新的模板特化时,我都需要更新解释器。
理想情况下,我应该能够使用某种模板魔法,在编译时自动生成解释器,以便两个代码部分保持解耦,同时我仍然可以保持switch语句的效率。
很遗憾,这段代码无法编译。它无法通过语法检查器,因为 lambda 函数中的 "case" 和 "break" 语句从技术上讲还没有在 switch 语句中。 为了使其正常工作,我需要使用宏而不是模板和 lambda 来实现 "unroll" 函数,以便复制和粘贴源代码发生在语法检查之前而不是之后。
我尝试过的另一种解决方案是模拟 switch 语句在低级别上的操作。我可以创建一个函数指针数组作为跳转表:
这种方法的缺点是,与 switch 语句相比,仅仅访问数组元素就会产生轻微的运行时惩罚。我认为这是固有的,因为 switch 语句在编译时将其跳转表生成指令内存中,而我在这里生成了在运行时数据内存中的跳转表。
我想只要函数的执行时间足够长,那么轻微的运行时惩罚就不太重要了,在这种情况下,开销与其相比就可以忽略不计了。
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 语句在编译时将其跳转表生成指令内存中,而我在这里生成了在运行时数据内存中的跳转表。
我想只要函数的执行时间足够长,那么轻微的运行时惩罚就不太重要了,在这种情况下,开销与其相比就可以忽略不计了。
print_num
?如果没有,为什么不将其转换为常规函数(而不是函数模板),并使用参数调用它?void print_num(size_t x) { static const char* arr[] = {"Zero", "One", ... }; std::cout << arr[x] << '\n'; }
(+如果需要,添加边界检查) - Ted Lyngmostd::function
及其类型转换开销来保存简单的函数指针。typedef void (*fptr)(); std::array<fptr, NUM_SPECIALIZATIONS> jump_table;
。 - Pete Becker