我正在尝试重构我正在进行的项目中的许多代码,我的同事们都喜欢复制粘贴。分散在许多类中,你可以找到像这样的调度函数:
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
}
这个问题的一些限制:
- 我们无法控制
Type1
,Type2
… 所以我们无法使用它们的层次结构或接口。在某些情况下,这些类型确实继承自(太通用而无用的)公共基类,但对于此问题的范围,您可以有效地将它们视为不相关的类型。 - 如果您将所有与类型相关的内容视为可以通过访问的数据,则在每种情况下执行的代码实际上是相同的
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
最佳答案
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;
...
}
}
这样,您就可以以一种我认为仍然可读的方式集中类型解析。
|
–
-fjump-tables --param case-values-threshold=2
(或您通常使用的 case 数量)。请注意,跳转表可能比(短)比较和跳转指令链更有效,也可能不比它们更有效。–
generateSwitch
代码上设置调试器断点?(例如)我想在“switch”中的类型 5 上设置断点。虽然很聪明,但你的方法可能“太聪明了”。你可以在构建过程中使用[元编程] 脚本生成[真实] (例如) ,然后对其进行编译。然后,你就有了一个简单的[每个人都会理解的] 和比你提议的更多的控制。switch/case
perl/python
dynamic_switch.cpp
switch/case
–
Id
是连续的(或范围很小),您可以自己创建表(主要是用函数数组替换initializer_list)。–
–
|