【问题标题】:C++ runtime type switching (avoiding switch)C++ 运行时类型切换(避免切换)
【发布时间】:2015-08-18 21:19:18
【问题描述】:

我已经使用 C++ 几年了,但我还没有找到解决我经常遇到的问题的方法。知道如何解决它会很棒。

我现在拥有的是:

// Client code:
switch(currentEnumValue)
    {
    case MyEnum::kValue01:
      processData<MyEnum::kValue01>(data);
      break;
    case MyEnum::kValue02:
      processData<MyEnum::kValue02>(data);
      break;
    default:
      LOG("Invalid command");
      break;
    }

// Declarations

enum class MyEnum {kValue01, kValue02};
class MyClass
{
// code
template <MyEnum> void processData(char*); /* Implemented somewhere else */
}
  template <> void MyClass::processData<MyEnum::kValue01>(char* data); /* Implemented somewhere else */
  MyClass <> void MyClass::processData<MyEnum::kValue02>(char* data); /* Implemented somewhere else */

出于多种原因,我想移除开关。而不是它,我需要类似:processData&lt;runtime-decltype(currentEnumValue)&gt;(data);

我知道 typeid 以及不将编译时间和运行时混合在一起......但尽管如此,我还是想找到一些解决方案,最好排除宏。

【问题讨论】:

  • 切换到动态多态呢?使用抽象接口(具有纯虚拟processData() 函数的类)和各种实现。
  • 我也在寻找避免动态多态性的方法,我忘记写了。无论如何感谢您的提示(只需要最大可能的性能)
  • 这些不是反问句,它们决定了我能想到的几种解决方案中的哪一种最好。 1)为什么需要枚举作为编译时间常数? 2) 交换机的哪些特性确切地有问题? “几个问题”含糊不清。请区分mustlike。 3) 枚举值是否从零开始,并且是连续的? 4) 你提出的解决方案究竟“需要”什么?请避免过度指定:“必须”要求过多的答案可能是“不,你不能”。
  • 好点,好的,我会尝试回答:1)我可能没有理解这个问题,枚举不是编译时间常数?无论如何,在这种特定情况下,我将这些枚举用于许多事情,并且我需要知道它们的值,因为我必须在数据包中通过网络将其传递 2)有问题,因为我想避免编写该开关代码,尤其是在需要时几次(我可以考虑在最后一个问题中使用模板模板参数,但我需要使用类)
  • 您所描述的只是虚拟功能的手动实现版本。您对此有何反对意见?

标签: c++ templates types switch-statement runtime


【解决方案1】:

这个类为给定的Enum 创建一个跳转表,直到某个count 大小基于构造一些模板并使用提供的参数调用它。它假定枚举值从 0 开始,然后转到 Count-1。

template<class Enum, Enum Count, template<Enum>class Z>
struct magic_switch {
  // return value of a call to magic_switch(Args...)
  template<class...Args>
  using R = std::result_of_t<Z<Enum(0)>(Args...)>;
  // A function pointer for a jump table:
  template<class...Args>
  using F = R<Args...>(*)(Args&&...);
  // Produces a single function pointer for index I and args Args...
  template<size_t I, class...Args>
  F<Args...> f() const {
    using ret = R<Args...>;
    return +[](Args&&...args)->ret{
      using Invoke=Z<Enum(I)>;
      return Invoke{}(std::forward<Args>(args)...);
    };
  }
  // builds a jump table:
  template<class...Args, size_t...Is>
  std::array<F<Args...>,size_t(Count)>
  table( std::index_sequence<Is...> ) const {
    return {{
      f<Is, Args...>()...
    }};
  }
  template<class...Args>
  R<Args...> operator()(Enum n, Args&&...args) {
    // a static jump table for this case of Args...:
    static auto jump=table<Args...>(std::make_index_sequence<size_t(Count)>{});
    // Look up the nth entry in the jump table, and invoke it:
    return jump[size_t(n)](std::forward<Args>(args)...);
  }
};

如果你有一个枚举:

enum class abc_enum { a, b, c, count };

还有一个函数对象模板:

template<abc_enum e>
struct stuff {
  void operator()() const {
    std::cout << (int)e << '\n';
  }
};

您可以派送:

magic_switch<abc_enum, abc_enum::count, stuff>{}(abc_enum::b);

在任何情况下,在模板stuff 中,您都会获得枚举值作为编译时间常数。你用运行时常量调用它。

开销应该类似于 switch 语句或 vtable 调用,具体取决于编译器如何优化。

live example.

请注意,将Enum 设置为std::size_t 是有效的。

在 C++11 中,您需要 make_index_sequenceindex_sequence

template<size_t...>
struct index_sequence {};
namespace details {
  template<size_t Count, size_t...szs>
  struct sequence_maker : sequence_maker<Count-1, Count-1, szs...> {};
  template<size_t...szs>
  struct sequence_maker<0,szs...> {
    using type = index_sequence<szs...>;
  };
}
template<size_t Count>
using make_index_sequence=typename details::sequence_maker<Count>::type;
template<class...Ts>
using index_sequence_for=make_index_sequence<sizeof...(Ts)>;

还有这个别名:

template<class Sig>
using result_of_t=typename std::result_of<Sig>::type;

然后在上面的代码中去掉std::的使用。

live example.

【讨论】:

  • @user3770392 该表是静态表。您的优化器理论上可以预先计算它(遗憾的是,lambda 的运算符函数指针不是 constexpr,所以我不能强制它)。创建后,每次调用都是对跳转表地址的数组查找。
  • @user3770392 我不明白那个评论。 “没有 lambda 运算符”——没有什么 lambda 运算符?在您发表评论之前,此页面上从未出现过“lambda 运算符”一词,那么您指的是什么“the”?
  • @user3770392 确定。只需在不使用 lambda 的情况下重写 +[](Args&amp;&amp;...args)-&gt;ret{ using Invoke=Z&lt;Enum(I)&gt;; return Invoke{}(std::forward&lt;Args&gt;(args)...); };,然后撒上 constexpr。应该可以做一个函数指针工厂。在你浪费时间之前,看看你的编译器是否实际上没有预先计算 static 数组。
  • @user3770392 添加了 C++11 更改;但是,您最初的问题没有提到“C++ 11”或那里的标签。除非您指定,否则您应该期望人们使用该语言的最新标准化版本而不是 4 年前的变体来回答。
  • @user3770392 gcc 4.7 仅提供实验性 C++11 支持。在 gcc 4.7.3 中,第一个错误消息是关于 using F,而不是关于 f,它明确告诉您这是一个未实现的功能,正在妨碍您。为了解决编译器未实现的 C++11 功能,我所做的是将 using F 别名替换为 template&lt;class...As&gt; struct F_t { using type=R&lt;As...&gt;(*)(As&amp;&amp;...); }; template&lt;class...Args&gt; using F = typename F_t&lt;Args...&gt;::type; 和 gcc 4.7.3 现在编译它。
【解决方案2】:

为了扩展我的评论,理想情况下我们应该有编译时反射并且能够编写一个通用的调度函数。如果没有它,一种选择是不幸地使用宏来为您使用 X 宏模式:

#define LIST_OF_CASES   \
    X_ENUM(kValue0)     \
    X_ENUM(kValue1)     \
    X_ENUM(kValue2)


enum MyEnum
{
#   define X_ENUM(a) a,
    LIST_OF_CASES
#   undef X_ENUM
};

void dispatch(MyEnum val)
{
    switch (val)
    {
#       define X_ENUM(a) case a: processData<a>(); break;
        LIST_OF_CASES
#       undef X_ENUM
    default:
        // something's really wrong here - can't miss cases using this pattern
    }
}

这种方法的一个好处是它可以扩展到大量枚举,很难省略一个案例,而且您可以使用多参数 X_ENUM 宏附加额外信息。

我知道你说过你想避免使用宏,但是没有虚函数的替代方法是拥有某种由枚举索引的函数指针静态表,这只是变相的虚函数(使用诚然,开销较低,但仍要承受间接函数调用的成本)。

【讨论】:

  • 为此干杯。是的,有赞成和反对使用宏。关于您正在谈论的第二个选项,是的,我们需要将该方法与标准交换机进行比较。为这个问题找到解决方案就像找到 C++ 魔法石,所以值得研究它;)
  • 性能可能没有那么不同 - 取决于开关的实现方式,您要么依赖分支预测器通过准确预测条件跳转来以低开销将您带到正确的开关案例,或者您再次依赖它来正确预测跳转表中的间接跳转;使用虚拟函数,您依赖于间接跳转预测;如果您的运行时枚举发生很大变化,那么一切都是无望的;如果不是所有解决方案的开销都可能很低。
  • 是的,我们只需要进行测量并比较它们,但您可能是对的。我记得 Alexandrescu 使用静态地图来解决类似问题。最好再去看看那本书
【解决方案3】:

Boost 变体的作用类似于您正在做的事情。它使您可以用基于模板的结构替换 switch 语句,该结构可以检查所有案例是否在编译时定义,然后在运行时选择一个。

例如,

using namespace boost;
using Data = variant<int, double>;

struct ProcessDataFn: static_visitor<void>
{
    char* data;
    void operator()(int& i)
    {
        // do something with data
    }

    void operator()(double& d)
    {
        // do something else
    }
};

void processData(char* data, Data& dataOut)
{
    apply_visitor(ProcessDataFn{data}, dataOut);
}

void example(char * data)
{
    Data d = 0;
    processData(data, d); // calls first overload of operator()
    Data d = 0.0;
    processData(data, d); // calls second overload
}

【讨论】:

  • 感谢您的反馈。我也会排除访问者模式,因为在这个例子中,整个逻辑都在 ProcessDataFn 中移动,而我想获得最大的解耦性,使一切独立(尽可能)
  • 我正在考虑的一件事是使用构建可以推断出我传递的枚举类型的东西,并使用重载来使用正确的函数成员。我必须检查一下这个东西
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多