【问题标题】:(C++) Automatically generate switch statement cases at compile time(C++) 编译时自动生成switch语句用例
【发布时间】: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 &lt;&lt; arr[x] &lt;&lt; '\n'; }(+ 如果需要,添加边界检查)
  • 这有点相关stackoverflow.com/questions/2157162/… 和更多细节在这里stackoverflow.com/questions/2157149/…。我认为没有办法绕过宏,除非您想按照 Ted 的建议更改方法
  • 是的,函数指针数组可能是一个好方法。您不需要std::function 及其类型转换开销来保存一个简单的函数指针。 typedef void (*fptr)(); std::array&lt;fptr, NUM_SPECIALIZATIONS&gt; jump_table;.
  • @Ted Lyngmo 这是因为在我的项目中,每个专业都非常独特。我展示的示例经过了简化,但在我的项目中,每个函数的行为以及它可以访问的数据将大相径庭,因此不可推广。

标签: c++ templates metaprogramming template-meta-programming


【解决方案1】:

你可以这样做:

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 语句?
  • @LoganSchlick 否。这将是一个递归调用,一些优化可能删除递归。
  • 实际上我查看了汇编指令,它确实编译成一大串 if 语句。即使打开了编译器优化。这意味着该解决方案具有 O(n) 复杂度和案例数量。此外,您不能将else 放在 if 语句之前,否则语法检查器会生气。
  • 关于else 的优点。我已经编辑了我的答案:-)
  • @LoganSchlick 对 Matthias 不错的解决方案的另一种看法是将其变成二进制搜索 like this。如果您有很多专业,那么与必须从头开始顺序搜索直到找到数字相比,平均比较次数(每个分支
【解决方案2】:

除了另一个答案,让我发布一个基于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&lt;std::size_t, i&gt;,可以隐式转换为std::size_t。转换运算符为constexpr

【讨论】:

    【解决方案3】:

    非递归版本

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

    查看结果here

    【讨论】:

      猜你喜欢
      • 2014-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多