【问题标题】:Exponential compilation times with simple typelist implementation. Why?简单类型列表实现的指数编译时间。为什么?
【发布时间】:2020-07-12 17:29:15
【问题描述】:

我正在尝试 C++ 类型列表。下面是一个类型列表过滤函数的简单实现。除了 gcc 和 clang 中的编译时间超出 18 个元素之外,它似乎有效。我想知道我可以做哪些改进来实现这一点。

#include <type_traits>

// a type list
template <class... T> struct tl ;

// helper filter for type list 
template <class IN_TL, class OUT_TL, template <typename> class P>
struct filter_tl_impl;

// Base case
template <class... Ts, template <typename> class P>
// If the input list is empty we are done
struct filter_tl_impl<tl<>, tl<Ts...>, P> {
  using type = tl<Ts...>;
};

// Normal case
template <class Head, class... Tail, class... Ts2, template <typename> class P>
struct filter_tl_impl<tl<Head, Tail...>, tl<Ts2...>, P> {
  using type = typename std::conditional<
      // Does the predicate hold on the head of the input list?
      P<Head>::value,
      // The head of the input list matches our predictate, copy it
      typename filter_tl_impl<tl<Tail...>, tl<Ts2..., Head>, P>::type,
      // The head of the input list does not match our predicate, skip
      // it
      typename filter_tl_impl<tl<Tail...>, tl<Ts2...>, P>::type>::type;
};

template <class TL, template <typename> class P> struct filter_tl {
  using type = typename filter_tl_impl<TL, tl<>, P>::type;
};

// Test code
using MyTypes = tl<
   char*, bool, char, int, long, void,
   char*, bool, char, int, long, void,
   char*, bool, char, int, long, void
   >;


using MyNumericTypes = filter_tl<MyTypes, std::is_arithmetic>::type;

static_assert(std::is_same < MyNumericTypes,
              tl<
              bool,char,int,long,
              bool,char,int,long,
              bool,char,int,long
              >> :: value);

int main(int, char **) {}

【问题讨论】:

  • 很抱歉,如果您有超过 18 个元素,您将创建 60 多个不同的类,而不是原始数据类型,请注意,类,是什么让您认为运行该程序的时间会很快?
  • @YunfeiChen 你住在 1957 年吗?
  • 嗯,你所做的类似于创建 60 个不同的文件并定义 60 个不同的头文件并制作它们......试试看,你会得到一个丑陋的目录,它需要永远运行...... ...从透视角度来看....
  • 那不是云飞。一个文件中的 60 个简单类根本不需要时间来编译。正如下面的 Jarod42 和 HTNW 讨论的那样,我的递归存在问题,两个分支都被占用,导致 ~2^18 类。

标签: c++ variadic-templates template-meta-programming


【解决方案1】:
using type = typename std::conditional<
      // Does the predicate hold on the head of the input list?
      P<Head>::value,
      // The head of the input list matches our predictate, copy it
      typename filter_tl_impl<tl<Tail...>, tl<Ts2..., Head>, P>::type,
      // The head of the input list does not match our predicate, skip
      // it
      typename filter_tl_impl<tl<Tail...>, tl<Ts2...>, P>::type>::type;

因为::type而实例化双方。

您可能会在std::conditional 之后延迟中间实例化:

using type = typename std::conditional<
      // Does the predicate hold on the head of the input list?
      P<Head>::value,
      // The head of the input list matches our predicate, copy it
      filter_tl_impl<tl<Tail...>, tl<Ts2..., Head>, P>,
      // The head of the input list does not match our predicate, skip
      // it
      filter_tl_impl<tl<Tail...>, tl<Ts2...>, P>>::type::type;

这导致实例化的数量是线性的,而不是指数的。

【讨论】:

  • 伙计,非常感谢。这种变化使编译瞬间完成。不过我不明白。您能否详细说明一下为什么会这样?
  • 原算法为O(2^n)。新的是O(n)。在您的代码中,如果您以sizeof...(Tails) == n 开头filter_tl_impl,那么您递归调用filter_tl_impl 两次,每次调用sizeof...(Tails) == n - 1,这将导致总共4 次调用(在上一层每次调用2 次)使用sizeof...(Tails) == n - 2等导致对基本案例 (sizeof...(Tails) == 0) 的最终 2^n 调用。因此,整个事情都接听了O(2^n)(我想到了2^(n + 1)?)的电话。在这个版本中,对filter_tl_impl 的每次调用最多会递归一次,从而使我们回到O(n) 调用。 2^18 &gt;&gt;&gt; 18.
  • 感谢 HTNW 但为什么双方都被实例化了?条件不应该将实例化限制为其中之一吗?
  • 没有。至少,我们必须检查两个分支都没有错误,这需要完全简化它们。还有一个通用化专业化的问题:std::conditional&lt;true, std::type_identity&lt;int&gt;, float&gt; 必须是与std::conditional&lt;true, int, float&gt;完全相同的类型,最简单的方法是在专业化std::conditional 之前将std::type_identity&lt;int&gt; 简化为int。请记住,std::conditional 只是一个普通的类模板。它不像if/else? :。这是一个函数Type conditional(bool b, Type, Type),它只接受评估的参数。
  • 谢谢,我明白了。
【解决方案2】:

如果你想要列表,你要做的第一件事就是定义cons 函数。其余的变得自然而直接。

// first, define `cons`      
  template <class Head, class T> struct cons_impl;
  template <class Head, class ... Tail>
  struct cons_impl <Head, tl<Tail...>> {
     using type = tl<Head, Tail...>;
  };
  template <class Head, class T>
  using cons = typename cons_impl<Head, T>::type;

// next, define `filter`
  template <template <typename> class P, class T>
  struct filter_tl_impl;
  template <template <typename> class P, class T>
  using filter_tl = typename filter_tl_impl<P, T>::type;

// empty list case      
  template <template <typename> class P>
  struct filter_tl_impl<P, tl<>> {
    using type = tl<>;
  };
  
// non-empty lust case
  template <template <typename> class P, class Head, class ... Tail>
  struct filter_tl_impl<P, tl<Head, Tail...>> {
    using tailRes = filter_tl<P, tl<Tail...>>;
    using type = std::conditional_t<P<Head>::value,
                                    cons<Head, tailRes>,
                                    tailRes>;
  };

注意tailRes是为了可读性而定义的,可以直接写

    using type = std::conditional_t<P<Head>::value,
                                    cons<Head, filter_tl<P, tl<Tail...>>>,
                                    filter_tl<P, tl<Tail...>>>;

编译时间仍然可以忽略不计。

【讨论】:

    【解决方案3】:

    一种可能的替代方法是在filter_tl_impl 中插入std::conditional

    我是说

    // Normal case
    template <typename Head, typename... Tail, typename... Ts2,
              template <typename> class P>
    struct filter_tl_impl<tl<Head, Tail...>, tl<Ts2...>, P>
     {
       using type = typename filter_tl_impl<tl<Tail...>,
                                            std::conditional_t<
                                               P<Head>::value,
                                               tl<Ts2..., Head>,
                                               tl<Ts2...>>,
                                            P>::type;
    };
    

    【讨论】:

      【解决方案4】:

      现在,为了完全不同的东西......

      我建议将您的“正常情况”(递归情况)分成两种不同的情况:情况“真”和情况“假”。

      不幸的是,这需要额外的自定义类型特征check_first

      template <typename, template <typename> class>
      struct check_first : public std::false_type
       { };
      
      template <typename H, typename ... T, template <typename> class P>
      struct check_first<tl<H, T...>, P>
         : public std::integral_constant<bool, P<H>::value>
       { };   
      

      现在你可以写filter_tl_impl如下

      // declaration and ground case
      template <typename I, typename O, template <typename> class P,
                bool = check_first<I, P>::value>
      struct filter_tl_impl
       { using type = O; };
      
      // recursive-positive case
      template <typename H, typename... T, typename... Ts,
                template <typename> class P>
      struct filter_tl_impl<tl<H, T...>, tl<Ts...>, P, true>
         : public filter_tl_impl<tl<T...>, tl<Ts..., H>, P>
       { };
      
      // recursive-negative case
      template <typename H, typename... T, typename... Ts,
                template <typename> class P>
      struct filter_tl_impl<tl<H, T...>, tl<Ts...>, P, false>
         : public filter_tl_impl<tl<T...>, tl<Ts...>, P>
       { };
      

      我还将filter_tl 重写为更简单的using

      template <typename TL, template <typename> class P>
      using filter_tl = typename filter_tl_impl<TL, tl<>, P>::type;
      

      所以你的原始代码变成了

      #include <type_traits>
      
      // a type list
      template <typename...>
      struct tl;
      
      template <typename, template <typename> class>
      struct check_first : public std::false_type
       { };
      
      template <typename H, typename ... T, template <typename> class P>
      struct check_first<tl<H, T...>, P>
         : public std::integral_constant<bool, P<H>::value>
       { };   
      
      // declaration and ground case
      template <typename I, typename O, template <typename> class P,
                bool = check_first<I, P>::value>
      struct filter_tl_impl
       { using type = O; };
      
      // recursive-positive case
      template <typename H, typename... T, typename... Ts,
                template <typename> class P>
      struct filter_tl_impl<tl<H, T...>, tl<Ts...>, P, true>
         : public filter_tl_impl<tl<T...>, tl<Ts..., H>, P>
       { };
      
      // recursive-negative case
      template <typename H, typename... T, typename... Ts,
                template <typename> class P>
      struct filter_tl_impl<tl<H, T...>, tl<Ts...>, P, false>
         : public filter_tl_impl<tl<T...>, tl<Ts...>, P>
       { };
      
      template <typename TL, template <typename> class P>
      using filter_tl = typename filter_tl_impl<TL, tl<>, P>::type;
      
      // Test code
      using MyTypes = tl<char*, bool, char, int, long, void,
                         char*, bool, char, int, long, void,
                         char*, bool, char, int, long, void>;
      
      using MyNumericTypes = filter_tl<MyTypes, std::is_arithmetic>;
      
      static_assert(std::is_same_v<MyNumericTypes,
                                   tl<bool, char, int, long,
                                      bool, char, int, long,
                                      bool, char, int, long>>);
      
      int main ()
       { }
      

      【讨论】:

      • 不同的是,但不是真的更好,你不觉得吗?
      • @samwise - 恕我直言更好,因为将递归情况拆分为两种不同的情况,因此从根本上避免编译性能问题(编译器选择一种情况,而不是两种情况)。考虑一下,如果您更改代码,使用单个案例和std::conditional 存在再次陷入上述问题的风险。但我承认它的可读性较差,这可能是个问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-16
      • 1970-01-01
      相关资源
      最近更新 更多