【发布时间】:2021-10-20 05:14:28
【问题描述】:
在我的程序中,我有一些看起来像这样的代码:通过模板专业化唯一实现公共模板的函数或类的集合:
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;
然后我可以用指向各种模板特化的指针填充跳转表,而不必将它们全部输入。相反,我可以只使用展开功能:
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'; }(+ 如果需要,添加边界检查) -
这有点相关stackoverflow.com/questions/2157162/… 和更多细节在这里stackoverflow.com/questions/2157149/…。我认为没有办法绕过宏,除非您想按照 Ted 的建议更改方法
-
是的,函数指针数组可能是一个好方法。您不需要
std::function及其类型转换开销来保存一个简单的函数指针。typedef void (*fptr)(); std::array<fptr, NUM_SPECIALIZATIONS> jump_table;. -
@Ted Lyngmo 这是因为在我的项目中,每个专业都非常独特。我展示的示例经过了简化,但在我的项目中,每个函数的行为以及它可以访问的数据将大相径庭,因此不可推广。
标签: c++ templates metaprogramming template-meta-programming