我正在尝试重构我正在进行的项目中的许多代码,我的同事们都喜欢复制粘贴。分散在许多类中,你可以找到像这样的调度函数:

void dispatch(const Buffer& buffer)
{
    switch (buffer.getId())
    {
    case Id::Type1:
    {
        Type1 obj;
        obj.deserialize(Buffer);
        // ... other code to print and process the buffer
    }
    break;
    case Id::Type2:
    {
        Type2 obj;
        obj.deserialize(Buffer);
        // ... same code to print and process the buffer as above
    }
    break;
    // ... and again, and again... for 10 to 30 types, depending on context
}

这个问题的一些限制:

  • 我们无法控制Type1Type2… 所以我们无法使用它们的层次结构或接口。在某些情况下,这些类型确实继承自(太通用而无用的)公共基类,但对于此问题的范围,您可以有效地将它们视为不相关的类型。
  • 如果您将所有与类型相关的内容视为可以通过访问的数据,则在每种情况下执行的代码实际上是相同的Id
  • 所有类型和 id 在编译时都是完全已知的。这似乎很明显,但我更喜欢说出来。

对我来说,这意味着 switch-case 在概念上是正确的解决方案,尽管非常冗长且容易出错。我的目标是重构这些函数,这样我只需要编写一次 case 逻辑,然后就可以专注于在其他地方列出 id-type 关联。所有这些都应该可以在不影响性能的情况下获得,这意味着完全优化的代码应该使用跳转表。

现在让我们假设我有一些模板obj_type_t<Id>“返回”与之对应的类型Id(如何构造这将是另一个问题的主题)并且让我们关注 switch-case。

我的解决方案

我的简单方法是使用模板递归来自动生成 switch-case:

// Base case
template <typename T, typename Predicate, DefaultPredicate>
void generateSwitch(T i, std::integer_sequence<T>, Predicate&& predicate, DefaultPredicate&& defaultPredicate)
{
    defaultPredicate();
}

// Recursive step
template <typename T, T First, T... Is, typename Predicate, DefaultPredicate>
void generateSwitch(T i, std::integer_sequence<T>, Predicate&& predicate, DefaultPredicate&& defaultPredicate)
{
    if (i == First)
    {
        predicate(First);
    }
    else
    {
        generateSwitch(i, std::integer_sequence<T, Is...>{}, predicate, defaultPredicate);
    }
}

这样我就可以按如下方式使用它:

void dispatch(const Buffer& buffer)
{
    auto processBuffer = [buffer](auto Id) // this auto is actually an integral_constant, so it's ok to use it as a template parameter
    {
        obj_type_t<Id> obj;
        obj.deserialize(buffer);
        // other code to print and process the buffer
    };
    
    auto invalidId = [](){ std::cout << "Invalid Id"; }
    
    generateSwitch(buffer.getId(), std::integer_sequence<Id, Id::Type1, Id::Type2 /*, other types... */>{}, processBuffer, invalidId);
}

中发现了一个避免模板递归的巧妙技巧,使用可丢弃的 std::initializer_list,如下所示:

template <typename T, T... Is, typename Predicate, DefaultPredicate>
void generateSwitch(T i, std::integer_sequence<T, Is...>, Predicate&& predicate, DefaultPredicate&& defaultPredicate)
{
    bool matched{false};
    std::initializer_list<int>({(i == Is ? predicate(std::integral_constant<T, Is>{}), matched = true, 0 : 0)...});
    if (!matched)
    {
        defaultPredicate();
    }
}

到目前为止一切都很好。我终于把一些代码放入了编译器资源管理器中,并试图确认这两个解决方案是等效的,并且可以优化为跳转表(即等效于 switch-case)。Clang 很高兴地遵守了,但我正在使用的编译器 gcc 却无法遵守。它生成一系列跳转相等语句,即使是未经训练的人眼也可以轻松地将其转换为跳转表。参见

是否有可能将我的代码或其他解决方案编译为跳转表?

7

  • 我确实没有使用过跳转表(第一次听说)。但从我读到的内容来看,我的第一个想法也是进行递归。我会看看是否能找到任何可以提供帮助的方法,但这比我迄今为止所做的要先进得多。


    – 

  • 4
    gcc 有时甚至在编译文字 switch 语句时也不会生成跳转表。请尝试-fjump-tables --param case-values-threshold=2(或您通常使用的 case 数量)。请注意,跳转表可能比(短)比较和跳转指令链更有效,也可能不比它们更有效。


    – 


  • 如何在generateSwitch代码上设置调试器断点?(例如)我想在“switch”中的类型 5 上设置断点。虽然很聪明,但你的方法可能“太聪明了”。你可以在构建过程中使用[元编程] 脚本生成[真实] (例如) ,然后对其进行编译。然后,你就有了一个简单的[每个人都会理解的] 和比你提议的更多的控制。switch/caseperl/pythondynamic_switch.cppswitch/case


    – 

  • 如果Id是连续的(或范围很小),您可以自己创建表(主要是用函数数组替换initializer_list)。


    – 

  • 您始终可以创建自己的跳转表。使用 <key, 函数指针> 表。找到键后,取消引用关联的函数指针。


    – 


最佳答案
2

一种调度方法是使用and

using IdVar = std::variant<
        // std::monostate,
        std::integral_constant<Id, Id::Type1>,
        std::integral_constant<Id, Id::Type2>,
        // ...
    >;

IdVar to_IdVar(Id id)
{
    switch (id)
    {
        case Id::Type1: return std::integral_constant<Id, Id::Type1>{};        
        case Id::Type2: return std::integral_constant<Id, Id::Type2>{};
        // ...
    };
    // throw or have std::monostate as return
}

那么你的旧开关就变成

void dispatch(const Buffer& buffer)
{
    std::visit(
        [&](auto id) {
            if constexpr (std::is_same_v<std::monosate, decltype(id)>) {
                std::cout << "Invalid Id"; 
            } else { // std::integral_constant<Id, xxx>
                obj_type_t<id()> obj;
                obj.deserialize(buffer);
                // other code to print and process the buffer
            }
        }, to_IdVar(buffer.getId()));
}

您只需进行一次手动切换,然后依靠std::visit它基本上相当于虚拟呼叫。

switch作为替代方案(或手动替换to_IdVar),如果枚举值是连续的或具有合理的大小,您可以自己创建表:

// Assuming EndValue<Id>() function returning the max value + 1 (as `std::size_t`)
// that `Id` can take

IdVar to_IdVar(Id id)
{
    if (static_cast<std::size_t>(Id) >= EndValue<Id>() {
        return std::monostate{}; // or throw
    }
    return [&]<std::size_t...Is>(std::index_sequence<Is...>) {
        return std::array<IdVar, sizeof...(Is)> {{
            std::integral_constant<Id, static_cast<Id>(Is)>{}...
        }}[id];
    }(std::make_index_sequence<EndValue<Id>()>());
}

替代建议:

void generateSwitch(int id, auto&& predicate) {
    switch (id) {
        case Type1::id:
            predicate.template operator()<Type1>();
            break;
        ... 
    } 
} 

这样,您就可以以一种我认为仍然可读的方式集中类型解析。