【问题标题】:C++ automatic generation of switch statementC++自动生成switch语句
【发布时间】:2014-02-06 19:23:29
【问题描述】:

考虑下面的代码

#include <iostream>

enum MyEnum{
    A,    
    B,
    END
};

template <int N>
class Trait {};

template<>
class Trait<A> {
    public:
        static int funct(int i) {return i*3;}
};

template<>
class Trait<B> {
    public:
        static int funct(int i) {return i*24;}
};


using namespace std;

int main(){
    int i = 1;
    switch(i){
        case A: cout << Trait<A>::funct(i) << endl; break;
        case B: cout << Trait<B>::funct(i) << endl; break;
    }   
} 

这将在屏幕上打印 24。

现在假设我在枚举中有更多的值,并且我定义了所有对应的 类 Trait 的模板特化。

为了避免在 switch 语句中编写所有必要的代码,我编写了一个 REPEAT 宏,它几乎可以像我想要的那样工作:

#include <iostream>

#define REPEAT(N, macro) REPEAT_##N(macro)
#define REPEAT_0(macro)
#define REPEAT_1(macro) REPEAT_0(macro) macro(0)
#define REPEAT_2(macro) REPEAT_1(macro) macro(1)
#define REPEAT_3(macro) REPEAT_2(macro) macro(2)
#define REPEAT_4(macro) REPEAT_3(macro) macro(3)
// etc...

// enum and class definitions

int main(){
   #define MY_MACRO(N) case N: cout << Trait<N>::funct(i) << endl; break;

   switch(i){
      REPEAT(2, MY_MACRO)
   }
}

这种方法的问题是我无法使用

REPEAT(END, MY_MACRO)

因为预处理器不知道我的枚举。

问题:有没有办法自动生成switch语句?

注意事项:

  • 我必须使用它的情况要复杂得多,自动化的东西会非常有帮助。
  • 使用 switch 语句对我来说很重要,因为它可以实现速度(速度对我的应用程序至关重要)。

谢谢!

编辑 1

更多说明:

  • 重要的是,开关的生成取决于枚举中定义的 END 值。

编辑 2/3

我决定在这里做一个补充以更好地解释我的应用程序以及为什么我更喜欢某些解决方案而不是其他解决方案

  • 在我的实际应用程序中,枚举包含近 50 个不同的值,并且将来会扩展(希望由其他人)。枚举包含连续值。
  • “Trait”类有多个成员函数(目前为 5 个)。此外,我需要在 5 个不同的文件中使用所有这些。如果我不使用自动生成所需内容的方式,我最终会多次编写基本相同的代码。
  • Trait 的成员函数始终以相同的方式使用。
  • 目前,在我的开关中,我有一个如下所示的函数调用(in1、in2 和 out 都是通过引用进行双重传递,前两种情况下为 const)。

    案例A:特征::funct(in1, in2, out);休息;

我为什么喜欢模板?

假设 Trait 有 2 个函数 funct1 和 funct2。我可以定义

template <int N>
class Trait {
    public:
        static int funct1(int i){static_assert(N!=N, "You forgot to define funct1");}
        static int funct2(int i){static_assert(N!=N, "You forgot to define funct2");}
};

现在,如果缺少函数定义,编译器将返回一个有意义的错误。当其他人进行添加时,这将很有帮助。

使用 Jarod42 建议的基于 C++11 特性的方法,我可以避免维护容易出错的长函数指针数组。

速度测试

到目前为止,我尝试了 3 种解决方案,但 Trait 中只有两个成员函数:

  • Jarod42 建议的解决方案
  • nndru 和 Ali 建议的简单函数指针数组
  • 带有 RETURN 宏的 switch 语句

前两种解决方案似乎是等价的,而基于 switch 的解决方案快 5 倍。我使用带有标志 -O3 的 gcc 版本 4.6.3。

【问题讨论】:

  • 嗨。为什么你不在你的枚举成员和分配给它的某种函子之间使用映射。
  • 正如我在笔记中所写的,速度对我来说真的很重要。我认为使用地图会使代码变慢。
  • 地图是一个快速搜索容器,我认为是个好主意:D
  • 我知道map很快,但是switch语句通常编译成跳转表会快很多。
  • 好的,你可以针对 map 使用数组:用整数分配枚举成员并将仿函数存储在数组中,其中枚举 id 将是数组单元格 id。

标签: c++ macros enums switch-statement


【解决方案1】:

正如您所说,您的枚举是连续的。在这种情况下,您不需要任何模板或std::mapswitch

只使用一个函数指针数组和枚举作为函数指针数组的索引!

#include <cassert>
#include <cstdio>

enum {
  A,
  B,
  SIZE
};

int A_funct(int i) { return 3*i; }

int B_funct(int i) { return 24*i; }

typedef int (*enum_funct)(int );

enum_funct map[] = { A_funct, B_funct };

// In C++11 use this:
//static_assert( sizeof(map)/sizeof(map[0])==SIZE , "Some enum is missing its function!");

int main() {
  assert(sizeof(map)/sizeof(map[0])==SIZE && "Some enum is missing its function!");
  int i = 1;
  std::printf("case A prints %d\n", map[A](i) );
  std::printf("case B prints %d\n", map[B](i) );
}

更新:来自您的 cmets:

我唯一关心的可维护性是明确写下来 5 个不同的函数指针数组(如果我不自动化)。

好的,现在我了解了维护问题。

我相信只有使用某种源代码生成(使用宏或自己编写)才能实现这一点(无论您使用函数指针数组还是switch 方法)源代码生成器。您还必须制定一些命名约定,以便可以自动生成函数指针数组(或switch 方法中case 语句中的代码)。

由于您没有指定,我只是制定了自己的命名约定。如果您对宏感到满意,这是我通过对example: 进行一些无意识的编辑而与Boost Preprocessor Library 一起破解的内容

#include <boost/preprocessor/repetition.hpp>

#define ENUM_SIZE 2

#define ENUM(z, n, unused) e##n,
enum { 
  BOOST_PP_REPEAT(ENUM_SIZE, ENUM, ~)  
  SIZE
};
#undef ENUM

int fA_e0(int i) { return 3*i; }
int fA_e1(int i) { return 24*i; }

int fB_e0(int i) { return 32*i; }
int fB_e1(int i) { return  8*i; }

typedef int (*enum_funct)(int );

#define MAP(z, n, case) f ## ##case ## _e##n,

enum_funct map_A[] = {
  BOOST_PP_REPEAT(ENUM_SIZE, MAP, A)
};

enum_funct map_B[] = {
  BOOST_PP_REPEAT(ENUM_SIZE, MAP, B)
};

#undef MAP

这是预处理器解析这些宏 (g++ -E myfile.cpp) 后得到的结果:

enum { e0, e1, SIZE };

[...]

typedef int (*enum_funct)(int );

enum_funct map_A[] = {
  fA_e0, fA_e1,
};

enum_funct map_B[] = {
  fB_e0, fB_e1,
};

因此,如您所见,如果您指定自己的命名约定,则可以自动生成映射(函数指针数组)。 documentation 不错。

但是,如果我是你,我会编写自己的源代码生成器。我会指定一个简单的文本文件格式(一行上的键值对,用空格分隔)并编写我自己的工具从这个简单的文本文件生成所需的 C++ 源文件。然后,构建系统将在预构建步骤中调用我的源代码生成器工具。这样,您就不必弄乱宏了。 (顺便说一句,我为自己写了一个小测试框架,为了避免在 C++ 中缺乏反射,我使用了自己的源代码生成器。真的没那么难。)


前两种解决方案似乎是等价的,而基于 开关速度快 5 倍。我使用带有标志的 gcc 版本 4.6.3 -O3。

我必须查看您的源代码、生成的程序集以及您如何测量时间才能了解这是如何发生的。

所以我也做了自己的速度测试。因为它会使这个答案变得混乱,所以源代码在这里:switch approachfunction pointer array 方法。

正如我所料:switch 方法更快,但前提是您有几个分支。 Andrei Alexandrescu 在他的演讲中也说了同样的话 Writing Quick Code in C++, Quickly,大约 38 分钟。在我的机器上,如果枚举大小为 5,switch 方法与函数指针数组方法一样快。如果枚举大小大于 5,则函数指针数组方法始终更快。如果枚举大小为 200 并且有 10^8 次函数调用,那么在我的机器上它会快 10% 以上。 (网上的代码只有10^7次函数调用,否则会超时。)

(我使用了链接时间优化(-O3 -flto 标记编译器链接器),我只能推荐它;它提供了很好的性能提升(在我的代码中高达 2.5 倍) 并且您唯一需要做的就是传递一个额外的标志。但是,在您的情况下,代码非常简单,没有改变任何东西。如果您想尝试:链接时间优化不可用或仅在 gcc 4.6.3 中进行实验。)


来自您的 cmets:

我按照您的基准方法逐步进行了新的实验 但我仍然使用 switch 语句得到更好的结果(当 枚举大小为 150 开关仍然比 带指针的解决方案)。 [...] 在使用我的代码进行的测试中,switch 方法的性能总是更好。我也跑了一些 用你的代码做实验,我得到了和你一样的结果。

我查看了生成的汇编代码,至少有 5 个函数 (5 cases)。如果我们至少有这么多函数,粗略地说,编译器会将switch 方法转换为具有一个明显缺点的函数指针方法。 即使在最好的情况下,与手动编码的函数指针数组方法相比,switch 在调度到要调用的函数时总是经过 1 个额外的分支(整数比较可能后跟跳转)。这个额外的分支属于 default: 标签,即使你在 C++ 代码中故意省略它也会生成;没有办法阻止编译器为此生成代码。 (如果您最多有 4 个 cases 并且所有 4 个函数调用都可以内联,那么情况就不同了;但是您已经有 50 个案例,所以没关系。)

除此之外,使用switch 方法会生成额外的(冗余)指令和填充,对应于case: 标签处的代码。这可能会增加您的缓存未命中率。因此,在我看来,如果您有多个案例(我的机器上有 5 个案例),switch 总是不如函数指针方法。 Andrei Alexandrescu says 在他的演讲中也是如此;他给出了大约 7 个案例的限制。

至于您的速度测试显示相反的原因:这类速度测试总是不可靠,因为它们对对齐和缓存非常敏感。不过,在我的原始测试中,switch的方法总是比函数指针数组稍差,这与我上面对汇编代码的分析是一致的。

函数指针数组的另一个优点是它可以在运行时构建和更改;这是 switch 方法所没有的。

奇怪的是我用函数指针得到的速度 数组根据枚举大小而变化(我希望它是 大致恒定)。

随着枚举大小的增长,你有更多的功能,指令缓存未命中的可能性也更大。换句话说,如果你有更多的功能,程序应该运行得稍微慢一些。 (在我的机器上确实如此。)当然,整个事情都是随机发生的,所以会有很大的偏差,如果ENUM_SIZE=42 的运行速度比41 的运行速度快,请不要感到惊讶。如前所述,对齐会为此增加额外的噪音。

【讨论】:

  • 使用开关可能允许编译器执行进一步的优化(例如,如果某些枚举值正在调用相同的函数),但这些优化通常是无用的,并且会在背景噪音中丢失。我发现您的解决方案是最易于维护和最优雅的。
  • @kuroineko 是的。尽管switch 语句可能会变得较差,如果有很多情况并且它们都不同。那么数组方法可能会更快。如果我没记错的话,在这种情况下,“很多”大于 7,实际上并没有那么多; Andrei Alexandrescu mentioned this 在他的演讲中。
  • 确实如此。我更多地考虑填充稀疏的数组,例如当您想要处理一些值的边缘情况时。坦率地说,所有这些问题对我来说似乎都是错误的。看起来 OP 正在寻求一种优化代码的通用方法,这在术语上是矛盾的。
  • @kuroineko 是的,我明白你的意思,我完全同意。我的评论是想说在其他一些情况下,数组方法可能是更快的方法。 “想要速度吗?测量。(由 Howard Hinnant 撰写)” 它也适用于这种情况。必须仔细分析,看看选择哪种方法是否真的很重要。
  • 完全正确。这种“自动切换”更像是寻找问题的解决方案:)
【解决方案2】:

在 C++11 中,您可以执行以下操作:

#if 1 // Not in C++11
#include <cstdint>

template <std::size_t ...> struct index_sequence {};

template <std::size_t I, std::size_t ...Is>
struct make_index_sequence : make_index_sequence < I - 1, I - 1, Is... > {};

template <std::size_t ... Is>
struct make_index_sequence<0, Is...> : index_sequence<Is...> {};

#endif

namespace detail {

template <std::size_t ... Is>
int funct(MyEnum e, int i, index_sequence<Is...>)
{
    // create an array of pointer on function and call the correct one.
    return std::array<int(*)(int), sizeof...(Is)>{{&Trait<MyEnum(Is)>::funct...}}[(int)e](i);
}

} // namespace detail

int funct(MyEnum e, std::size_t i)
{
    return detail::funct(e, i, make_index_sequence<std::size_t(END)>());
}

注意:enum 不应该有孔(所以这里A=0B=1 可以)

以下宏可能会有所帮助:

#define DYN_DISPATCH(TRAIT, NAME, SIGNATURE, ENUM, ENUM_END) \
    namespace detail { \
    template <std::size_t ... Is> \
    constexpr auto NAME(ENUM e, index_sequence<Is...>) -> SIGNATURE \
    { \
        return std::array<SIGNATURE, sizeof...(Is)>{{&TRAIT<ENUM(Is)>::NAME...}}[(int)e]; \
    } \
    } /*namespace detail */ \
    template <typename...Ts> \
    auto NAME(ENUM e, Ts&&...ts) \
        -> decltype(std::declval<SIGNATURE>()(std::declval<Ts>()...)) \
    { \
        return detail::NAME(e, make_index_sequence<std::size_t(ENUM_END)>())(std::forward<Ts>(ts)...); \
    }

然后将其用作:

    DYN_DISPATCH(Trait, funct, int(*)(int), MyEnum, END)

    // now `int funct(MyEnum, int)` can be call.

【讨论】:

  • 我将创建数组static const 以使编译器清楚我不想每次都重建它。然后我会对两个版本进行性能测试。
  • 坦率地说,我无法理解此类解决方案的实用性。每次你想定义另一个“自动开关”时都必须复制粘贴 return std::array&lt;int(*)(int), sizeof...(Is)&gt;{{&amp;Trait&lt;MyEnum(Is)&gt;::funct...}}[(int)e](i); 这样的怪物绝对不值得付出努力,恕我直言
  • 实际上我也在使用 C++11,如果我对它很陌生的话……我会试试你的代码。你觉得它会编译成跳转表吗?
  • @Jarod42 如果他的枚举是连续的,一个普通的旧函数指针就足够了,看我的回答。
  • 坦率地说,所有这些用于移动火柴棒的代码都不值得恕我直言。正如我所看到的,如果目标是避免为每个枚举值复制粘贴一些代码,那么预处理器可以更轻松有效地完成这项工作。此外,除非您想优化执行速度(或者您可以使用虚拟类作为开始),否则这是没有意义的,并且隐藏在模板后面的是函数指针数组,与普通的硬编码开关相比不是最佳的。
【解决方案3】:

您根本不需要模板来执行此操作。更像是老好X macros

#define MY_ENUM_LIST VAL(A) VAL(B)

// define an enum
#define VAL(x) x,
enum MyEnum { MY_ENUM_LIST END };
#undef VAL

// define a few functions doing a switch on Enum values  

void do_something_with_Enum (MyEnum value, int i)
{
   switch (value)
   {
      #define VAL(N) case N: std::cout << Trait<N>::funct(i) << std::endl; break;
      MY_ENUM_LIST
      #undef VAL
   }
}

int do_something_else_with_Enum (MyEnum value)
{
   switch (value)
   {
      #define VAL(x) case x: yet_another_template_mayhem(x);
      MY_ENUM_LIST
      #undef VAL
   }
}

我已经浪费了足够多的时间了。如果您认为模板是解决方案,只需将您的问题更改为“仅限模板专家,预处理器不够好”之类的。

您不会是第一个在无用模板上浪费时间的人。许多人靠为不存在的问题提供臃肿、无用的解决方案谋生。

此外,您对 switch 比函数指针数组更快的假设是值得商榷的。这完全取决于枚举中值的数量以及案例语句中代码的可变性。

现在,如果优化不是一个大问题,您可以简单地使用虚拟方法来专门化您的枚举选择的任何对象的行为,并让编译器为您处理整个“自动切换”的东西。

这种方法的唯一好处是避免重复代码,如果您的对象足够相似,让您认为您会比编译器以专门的方式处理它们做得更好。

您似乎要求的是优化未知代码模式的通用解决方案,这在术语上是矛盾的。

编辑:感谢 Jarod42 清理示例。

【讨论】:

  • 使用我的方法,我设法在 switch 语句中执行了真正不同的功能。我看不出如何使用您的方法实现相同的行为。
  • 那么简单地定义 CASEVAL 来执行您需要的任何函数调用。还是有其他问题?
  • 我需要 switch 语句中的每个 case 执行不同的代码。此外,我需要使用枚举中定义的 END 值生成 switch 语句。
  • 你的问题是“C++自动生成switch语句”。我向你展示了一种方法来做到这一点。开关的内容可以是任何你想要的,但这是另一个问题。如果您问我,如果您不尽快在数据唯一性和可读性之间划清界限,那么进行自动代码生成是一条直截了当的道路。但这只是一种意见。
  • 我用一些代码开始了我的问题,以解释我想在哪里使用自动生成。我需要许多略有不同的开关,我认为手动编写它们太容易出错。
【解决方案4】:

您似乎希望将整数 id 与每个函数相关联,并通过 id 查找函数。

如果您的 id 是连续的,您可以拥有一个由该 id 索引的函数指针数组,这会给您 O(1) 的查找复杂度,例如:

typedef int Fn(int);

enum FnId {
    A,
    B,
    FNID_COUNT
};

int fn_a(int);
int fn_b(int);

Fn* const fns[FNID_COUNT] = {
    fn_a,
    fn_b
};

int main() {
    fns[A](1); // invoke function with id A.
}

如果 id 不是连续的,你仍然可以有一个 {id, function_ptr} 元组的排序数组并对其进行二进制搜索,O(lg(N)) 查找复杂度。

这些都不需要宏或模板。

【讨论】:

    【解决方案5】:

    对于数字(数据库)类型标识符,我有一个包含标识符的模板。通过可变参数模板的调度调用具有匹配类型特征的函子:

    #include <iostream>
    #include <stdexcept>
    
    // Library
    // =======
    
    class TypeIdentifier
    {
        public:
        typedef unsigned Integer;
    
        enum Value
        {
            Unknown,
            Bool,
            Int8,
            UInt8,
            Int16,
            UInt16,
            Int32,
            UInt32,
            Int64,
            UInt64,
            Float,
            Double,
            String,
            LargeObject,
            Date,
            Time,
            DateTime
        };
    
        template <Value ...Ids>  struct ListType {};
        typedef ListType<
            Bool,
            Int8,
            UInt8,
            Int16,
            UInt16,
            Int32,
            UInt32,
            Int64,
            UInt64,
            Float,
            Double,
            String,
            LargeObject,
            Date,
            DateTime,
            // Always the last value:
            Unknown
        >
        List;
    
        public:
        TypeIdentifier(Integer value = Unknown)
        :   m_id(value)
        {}
    
        Integer id() const { return m_id; }
    
        /// Involve a functor having a member function 'Result apply<Traits>()'.
        template<typename Functor>
        typename Functor::result_type dispatch(const Functor&);
    
        private:
        Integer m_id;
    };
    
    template<TypeIdentifier::Value I>
    struct TypeTraits
    {
        static constexpr TypeIdentifier::Value Id = I;
        static constexpr bool is(TypeIdentifier::Integer id) { return (Id == id); }
        static bool is(TypeIdentifier type_identifier) { return (Id == type_identifier.id()); }
    
        // And conversion functions
    };
    
    
    namespace TypeIdentifierDispatch {
    
    template <typename Functor, TypeIdentifier::Value I, TypeIdentifier::Value ... Ids> struct Evaluate;
    
    template <typename Functor>
    struct Evaluate<Functor, TypeIdentifier::Unknown> {
        static typename Functor::result_type
        apply(TypeIdentifier::Integer id, const Functor&) {
            throw std::logic_error("Unknown Type");
        }
    };
    
    template <typename Functor, TypeIdentifier::Value I, TypeIdentifier::Value ... Ids>
    struct Evaluate {
        static typename Functor::result_type
        apply(TypeIdentifier::Integer id, const Functor& functor) {
            if(TypeTraits<I>::is(id))
                return functor.template apply<TypeTraits<I>>();
            else return Evaluate<Functor, Ids...>::apply(id, functor);
        }
    };
    
    template <typename Functor, TypeIdentifier::Value ... Ids>
    inline typename Functor::result_type
    evaluate(TypeIdentifier::Integer id, const Functor& functor, TypeIdentifier::ListType<Ids...>)
    {
        return Evaluate<Functor, Ids...>::apply(id, functor);
    }
    
    } // namespace TypeIdentifierDispatch
    
    template<typename Functor>
    inline
    typename Functor::result_type TypeIdentifier::dispatch(const Functor& functor) {
        return TypeIdentifierDispatch::evaluate(id(), functor, TypeIdentifier::List());
    }
    
    
    
    // Usage
    // =====
    
    struct Print {
        typedef void result_type;
    
        template <typename Traits>
        result_type apply() const {
            std::cout << "Type Identifier: " << Traits::Id << '\n';
        }
    };
    
    inline void print_identifier(unsigned value) {
        TypeIdentifier(value).dispatch(Print());
    }
    
    
    int main ()
    {
        print_identifier(TypeIdentifier::String);
        return 0;
    }
    

    向库中添加新类型需要调整 TypeIdentfier 并(可能)添加专门的 TypeTraits。

    注意枚举值可以是任意的。

    【讨论】:

      【解决方案6】:

      使用递归模板可以自动生成等价于的构造

         if (i = A)
            Trait<A>::funct(i);
         else if (i = B)
            Trait<B>::funct(i);
      

      我认为它的性能类似于 switch 语句。您的原始示例可以重写如下。

      #include <iostream>
      
      using namespace std;
      
      enum MyEnum {
         A,
         B,
         END
      };
      
      template <MyEnum N>
      class Trait 
      { public:
         static int funct(int i)
         { 
            cout << "You forgot to define funct" << i;
            return i; 
         } 
      };
      
      template<>
      class Trait<A> {
      public:
         static int funct(int i) { return i * 3; }
      };
      
      template<>
      class Trait<B> {
      public:
         static int funct(int i) { return i * 24; }
      };
      
      template <MyEnum idx>
      int Switch(const MyEnum p, const int n)
      {
         return (p == idx) ? Trait<idx>::funct(n) : Switch<(MyEnum)(idx - 1)>(p, n);
      }
      
      template <>
      int Switch<(MyEnum)(0)>(const MyEnum p, const int n)
      {
         return Trait<(MyEnum)(0)>::funct(n);
      }
      
      int funct(MyEnum n)
      {
         return Switch<END>(n, n);
      }
      
      int main() {
         MyEnum i = B;
         cout << funct(i);
      }
      

      【讨论】:

        猜你喜欢
        • 2021-10-20
        • 1970-01-01
        • 1970-01-01
        • 2016-05-08
        • 1970-01-01
        • 2011-06-08
        • 2017-02-03
        • 2021-07-24
        • 1970-01-01
        相关资源
        最近更新 更多