【问题标题】:C++ template instantiation: Avoiding long switchesC++ 模板实例化:避免长开关
【发布时间】:2014-10-01 20:38:03
【问题描述】:

我有一个依赖于整数模板参数的类。在我的程序中,我想使用此模板的一个实例化,具体取决于在运行时确定的此参数的值。这是一个简单的示例,演示了我目前如何使用大 switch 语句来解决这个问题:

#include <string>
#include <iostream>
#include <type_traits>

template<unsigned A>
struct Wrapper {
    typedef typename std::conditional<A==1, int, float>::type DataType;
    DataType content[A];
    void foo() {
        std::cout << A << std::endl;
    };
};    

int main(int argc, char *argv[])
{
    std::string arg = argv[1];
    int arg_int = std::stoi(arg);

    switch (arg_int) {
    case 1: {
        Wrapper<1> w;
        w.foo();
        break;
    }
    case 2: {
        Wrapper<2> w;
        w.foo();
        break;
    }
    case 3: {
        Wrapper<3> w;
        w.foo();
        break;
    }
    default:
        return 1;
    };

    return 0;
}

一旦我不仅有一个参数A,而且有多个不同组合的模板参数,这很快就会变得笨拙。我们还假设实际上有一个很好的理由将 A 实现为模板参数。

有没有办法用几乎相同的 case 语句替换巨大的 switch 语句,例如使用一些来自 Boost 或预处理器的元编程魔法?

理想情况下,我希望能够编写如下内容:

INSTANTIATE_DEPENDING(i, {1, 2, 3},
            {
                Wrapper<i> w;
                w.foo();
            }
    );

【问题讨论】:

  • 看起来像是使用虚拟继承来解决的任务,或者,如果整数真的只是代表数组的大小,那么使用std::vector
  • 我会为这些参数创建一个单独的结构来对它们进行分组并将其用作模板参数
  • 模板类确实比简单的例子复杂。无法避免使用不同的模板实例化,我在示例中添加了一些额外的复杂性。
  • 交换机使用的有效数字有什么限制?
  • 在这种情况下,有效数字是{1, 2, 3}。它可能是不同的集合。不过,我也许可以安排它们总是升序的自然数。

标签: c++ templates boost c-preprocessor


【解决方案1】:

您可以使用可变参数模板,可能是这样的:

#include <cstdlib>
#include <string>

int main(int argc, char * argv[])
{
    if (argc != 2) { return EXIT_FAILURE; }

    handle_cases<1, 3, 4, 9, 11>(std::stoi(argv[1]));
}

实施:

template <int ...> struct IntList {};

void handle_cases(int, IntList<>) { /* "default case" */ }

template <int I, int ...N> void handle_cases(int i, IntList<I, N...>)
{
    if (I != i) { return handle_cases(i, IntList<N...>()); }

    Wrapper<I> w;
    w.foo();
}

template <int ...N> void handle_cases(int i)
{
    handle_cases(i, IntList<N...>());
}

【讨论】:

  • 这很漂亮(对于 C++ 标准),并且教会了我关于可变参数模板的知识。我还可以看到如何通过使用元组作为模板参数将其扩展到多个参数。
  • 啊,不幸的是,扩展它并不像我想象的那么简单。 typedef std::tuple&lt;int, char&gt; ParamTuple; template &lt;ParamTuple ...&gt; struct ParamTupleList {}; 无法编译:‘class std::tuple’不是模板常量参数的有效类型
  • @rerx:最终的函数模板只是为了方便。你也可以直接说handle_cases(std::stoi(argv[1]), IntList&lt;1, 2, 3&gt;())。该语法可以直接扩展到多个参数:f(x, y, z, IntList&lt;1,2,3&gt;(), IntList&lt;3,4&gt;(), IntList&lt;&gt;())
  • 我明白了。但是所有参数值都需要是相同的(整数)类型?
  • 它看起来很可爱,但是如果你有一个很长的列表,handle_cases() 是否会在列表的长度上为 O(N),因为它会遍历所有列表?在你的情况下,如果 i == 11
【解决方案2】:

arg_int 是一个运行时参数,因此无法将其直接附加到模板参数。您可以使用某种处理程序表来删除此处的 switch 语句。

您会使用类似lookup_handler( int N ) 的东西返回一个类型handler,它可能是调用这些模板函数之一的lambda。

可以从允许的最高编号开始递归地在表格上注册所有 lambda。

template< unsigned N > register_lambda()
{
     table.add( Wrapper<N>() );
     register_lambda< N-1 >;
}

专攻register_lambda&lt;0&gt;

然后你打电话给register_lambda&lt;32&gt;说你已经注册了从0到32的所有数字。

实现这种表的一种方法是:

class lambda_table
{
 typedef std::function<void()> lambda_type; 
    public:
        void add( lambda_type );
        bool lookup( size_t key, lambda_type & lambda ) const;
};

从 main() 或任何你想调用它的地方你有一个对该表的引用(称之为表)然后调用

lambda_type lambda;
if( table.find( arg_int, lambda ) )
        lanbda();
else
      default_handler();

您可以更改此设置,为表本身提供一个默认处理程序,但没有为该编号提供任何处理程序。

虽然 lambdas 可以包装各种数据成员,但您实际上可能希望您的模板是层次结构中的类,而不是 lambdas,因为它们中存储了数据。

【讨论】:

  • 嘿,register 是关键字>o
  • 是的,我替换了它,尽管它是为了说明,展示它是如何完成的。
  • 你能写一个更完整的答案,明确地展示如何更换开关吗?
  • 所有这些示例都直接替换了您的开关。关键是将它全部放入一个数据结构中,其中您的arg_int 是关键并检索指向处理您的包装器-foo-thing 的函数的指针。
【解决方案3】:

作为开关的一般替代方案,您可以使用函数指针的向量或映射来移除开关:

template <int i>
int foo()
{
    Wrapper<i> w;
    w.foo();
    return i;
}

static std::vector<int(*)()> m;

void init()
{
    m.push_back(&foo<0>);
    m.push_back(&foo<1>);
}

void bar(int i)
{
    m[i]();
}

在 C++11 中,您可以使用初始化列表来初始化向量或映射。

【讨论】:

  • 那行得通!使用初始化列表就足够了。
  • 但是我仍然会有类似std::map&lt;int, void(*)()&gt; cases = { {1, &amp;wrapper_case&lt;1&gt;}, {2, &amp;wrapper_case&lt;2&gt;}, {3, &amp;wrapper_case&lt;3&gt;} }; cases[arg_int](); 的东西,它最终不会比带有宏的开关更冗长。
  • 这些是您至少需要的信息,因为有一个号码arg_int 以及您要连接到该号码的方法。不要忘记,这种方法也比 switch 快得多,它会编译成类似于 if-else 的东西。
【解决方案4】:

只使用宏!

template<unsigned A>
struct Wrapper {
    int content[A];
    void foo() { };
};

#define WRAPPER_SWITCH_CASE(i) case i: Wrapper<i>().foo(); break;

int main(int argc, char *argv[])
{
    std::string arg = argv[1];
    int arg_int = std::stoi(arg);

    switch (arg_int) {
        WRAPPER_SWITCH_CASE(1)
        WRAPPER_SWITCH_CASE(2)
        WRAPPER_SWITCH_CASE(3)
        default: return 1;
    };

    return 0;
}

(live example)

但如您所知,宏是有害的;我认为Wrapper 应该在运行时分配content,而不是模板。

【讨论】:

  • 我绝对赞成使用宏(在本地,即之后使用'#undef',这应该不会太有害)。现在我们可以通过一些递归模板技巧或其他东西来扩展这个解决方案,以便我们摆脱显式开关吗?我想在问题中有类似于INSTANTIATE_DEPENDING(i, {1, 2, 3}, ... 的内容。
  • 宏不是“有害的”,但会使代码难以遵循和维护,并且在许多情况下会导致细微的错误。
  • @rerx 是的,在 C++11 中,有 varidic 宏,它是从 C99 派生的。
  • @ikh 将宏定义中的“1”替换为“i”
【解决方案5】:

使用用于 Wrappers 的递归生成器的概念应用程序简介:

#include <iostream>
#include <vector>

struct FooProvider
{
    virtual void foo() = 0;
};

template<unsigned A>
struct Wrapper : public FooProvider {
    Wrapper() {std::cout << A << std::endl;}
    int content[A];
    virtual void foo() { std::cout << "call:" << A << std::endl;};
};

static std::vector<FooProvider*> providers;

template <unsigned CTR>
struct Instantiator
{
    Instantiator()
    {
        providers.insert(providers.begin(), new Wrapper<CTR>);
        Instantiator<CTR - 1>();
    }
};

template <>
struct Instantiator<0>
{
    Instantiator() {}
};

int main()
{
    Instantiator<100>();
    providers[4]->foo();

    // do not forget to delete the providers
}

【讨论】:

  • 你们都建议,从 0..N 开始的所有数字都已注册?问题的核心是,如何更换开关。
  • 我更喜欢没有任何虚函数的解决方案。
  • 这只是我在递归注册到表格时建议的更精细的扩展。您可以通过 virtual 使用 lambda 而不是多态。
  • Wrapper 究竟是如何使用它的数组数据的呢?如果您希望每次调用该类时都生成这些实例,那么就可以了。如果您希望其中的每一个都存在,这是一个不同的问题。如果您希望这些数据持续存在,那么让它们都派生的类的方法可能会更好
【解决方案6】:

这是另一种方法:

template<int N>
void do_foo()
{
    Wrapper<N> w;
    w.foo();
}

template<int N, int... Ns>
struct fn_table : fn_table<N - 1, N - 1, Ns...>
{
};

template<int... Ns>
struct fn_table<0, Ns...>
{
    static constexpr void (*fns[])() = {do_foo<Ns>...};
};

template<int... Ns>
constexpr void (*fn_table<0, Ns...>::fns[sizeof...(Ns)])();

int main(int argc, char *argv[])
{
    std::string arg = argv[1];
    int arg_int = std::stoi(arg);

    // 4 if you have Wrapper<0> to Wrapper<3>.
    fn_table<4>::fns[arg_int]();
}

【讨论】:

    【解决方案7】:

    您可以只使用将块实现传递给通用循环扩展器的高阶循环宏:

    #define M_NARGS(...) M_NARGS_(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
    #define M_NARGS_(_10, _9, _8, _7, _6, _5, _4, _3, _2, _1, N, ...) N
    
    #define M_CONC(A, B) M_CONC_(A, B)
    #define M_CONC_(A, B) A##B
    #define M_ID(...) __VA_ARGS__
    
    #define M_FOR_EACH(ACTN, ...) M_CONC(M_FOR_EACH_, M_NARGS(__VA_ARGS__)) (ACTN, __VA_ARGS__)
    
    #define M_FOR_EACH_0(ACTN, E) E
    #define M_FOR_EACH_1(ACTN, E) ACTN(E)
    #define M_FOR_EACH_2(ACTN, E, ...) ACTN(E) M_FOR_EACH_1(ACTN, __VA_ARGS__)
    #define M_FOR_EACH_3(ACTN, E, ...) ACTN(E) M_FOR_EACH_2(ACTN, __VA_ARGS__)
    #define M_FOR_EACH_4(ACTN, E, ...) ACTN(E) M_FOR_EACH_3(ACTN, __VA_ARGS__)
    #define M_FOR_EACH_5(ACTN, E, ...) ACTN(E) M_FOR_EACH_4(ACTN, __VA_ARGS__)
    //...etc
    
    
    #define INSTANTIATE_DEPENDING(L, C) M_FOR_EACH(C, M_ID L)
    
    //...
    #define CASE_BLOCK(n) case n: { Wrapper<n> w; w.foo(); break; }
    
    INSTANTIATE_DEPENDING((1, 2, 3), CASE_BLOCK)
    
    #undef CASE_BLOCK  //if you like, not essential to the concept
    

    对此没什么好说的:循环重复传递的列表长度的块,将列表中的项目传递给它要扩展的宏。所以你把你的实现放在那个宏中(如果你希望它是本地的,也可以#undef它)。

    更优雅(让您嵌套参数化代码以在它所属的表达式内展开,而不是第二个定义),您可以使用相当高端的Order metaprogramming library

    #include <order/interpreter.h>
    
    ORDER_PP(    // runs Order code
      8for_each_in_range(8fn(8I,
                             8print( (case) 8I (: { )
                                        (Wrapper<) 8I (> w; w.foo(); break; }) )),
                         1, 4)
    )
    

    (对于非连续列表,使用8for-each 而不是8for_each_in_range。订单具有完整的函数式编程语义,所以这些都是小问题。)

    【讨论】:

    • +1 向我介绍了 Order。由于只需使用可变参数模板就可以相当干净地解决我的问题,因此我将采用这种方法。
    【解决方案8】:

    Kerrek SB's answer 的启发,使用可变参数模板,这是一个可以轻松扩展到任何类型的多个参数的解决方案:

    template <int param1_>
    struct Params
    {
        const static int kParam1 = param1_;
        // Add other parameters here if needed
    };
    
    // Default case: list is empty
    template <typename T>
    void handle_cases(const T param1) { }
    
    // Regular case: list is not-empty
    template <typename T, typename head, typename ...tail>
    void handle_cases(const T param1)
    {
        if (head::kParam1 == param1)
        {
            Wrapper<head::kParam1> w;
            w.foo();
        }
        else
        {
            handle_cases<T, tail...>(param1);
        }
    }
    

    请注意,typename T 只是一个附加模板参数的示例,它不属于头/尾列表的一部分。

    下面是如何使用它:

    int main(int argc, char * argv[])
    {
        if (argc != 2) { return EXIT_FAILURE; }
        handle_cases<int, Params<1>, Params<3>, Params<4>, Params<9>, Params<11>>(std::stoi(argv[1]));
    }
    

    【讨论】:

      【解决方案9】:

      解释@Simple基于静态函数表的解决方案:

      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      template<int N>
      void do_foo()
      {
          cout << N << endl;
      }
      
      template<int N, int... Ns>
      struct fn_table : fn_table<N - 1, N - 1, Ns...> {
      };
      
      template<int... Ns>
      void p()
      {
          int a[] = {Ns...};
          for (int i = 0; i < sizeof(a)/sizeof(int); ++i) 
              cout << a[i] << endl;
      }
      
      // Recursion-base instantiation with leading 0 parameter.
      template<int... Ns>
      struct fn_table<0, Ns...> {
          // calling fn_table<4> would call recursively with template parameters: <4>, <3, 3>, <2, 2, 3>, <1, 1, 2, 3>, <0, 0, 1, 2, 3>. The last call would create 4 (we expand Ns without the first 0) do_foo functions using a variadic parameter pack "...".
          static constexpr void (*fns[])() = {
              p<Ns...> // call a function that prints Ns... for illustration, expanding the parameters inside p instead of duplicating it.
              //do_foo<Ns>...
          };
      };
      
      template<int... Ns>
      constexpr void (*fn_table<0, Ns...>::fns[sizeof...(Ns)])();
      
      int main(int argc, char *argv[])
      {
          int arg_int = 0;
      
          // 4 if you have Wrapper<0> to Wrapper<3>.
          fn_table<4>::fns[arg_int]();
      }
      

      【讨论】:

        【解决方案10】:

        使用 integer_sequence 构建表。我还添加了:i)序列开始,ii)函数类型的参数,例如接收和返回值。

        #include <iostream>
        #include <vector>
        
        using namespace std;
        
        struct Foo {
            template<int N>
            static void foo(int &a) {
                cout << N << endl;
                a = N + 1;
            }
        };
        
        template<int start, typename F, typename R, typename T, T... ints>
        auto fn_table_( integer_sequence<T, ints...> int_seq )
        {
            vector<R> expand = { F::foo<ints+start>... };
            vector<R> dummy( start );
            expand.insert( expand.begin(), dummy.begin(), dummy.end() );
        
            return expand;
        }
        
        template<int start, typename F, typename R, int N>
        auto fn_table()
        {
            return fn_table_<start, F, R>( make_integer_sequence<int, N-start>{} );
        }
        
        void main()
        {
            int arg_int = 5;
        
            typedef void (*fun_type)( int & );
            auto fns = fn_table<4, Foo, fun_type, 7>();
            int a;
            fns[arg_int]( a );
            cout << a << endl;
            cout << "all:\n";
            for (int i = 0; i < fns.size() ; ++i) 
                if ( fns[i] )
                    fns[i]( a );
        }
        

        【讨论】:

          猜你喜欢
          • 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
          相关资源
          最近更新 更多