【问题标题】:Inheritance or composition when recursing over a parameter pack?递归参数包时的继承或组合?
【发布时间】:2016-12-23 12:04:20
【问题描述】:

在对参数包进行递归时,如果可以选择,我应该更喜欢通过继承还是通过成员字段(组合)进行递归?这是一刀切吗?有哪些取舍?

我想知道的一件事是,例如,基于组合的表单是否通常被认为具有更好的编译速度、内存使用或错误报告。

为了说明,以下是受 Jonathan Wakely 的回答 here 启发的短路 or_(析取)示例。

基于继承:

#include <type_traits>

// disjunction

template<typename... Conds>
struct or_ : std::false_type {};

template<typename Cond, typename... Conds>
struct or_<Cond, Conds...> 
  : std::conditional<Cond::value, std::true_type, or_<Conds...>>::type
{};

static_assert(or_<std::true_type, std::true_type, std::true_type>::value,"");
static_assert(or_<std::false_type, std::false_type, std::true_type>::value,"");
static_assert(or_<std::false_type, std::false_type, std::false_type>::value == false,"");

我了解此版本具有or_&lt;Ts...&gt; 将从std::integral_constant 继承的功能。为了我的问题,请假设我不关心 or_ 是否继承自 integral_constant

基于作曲:

template<typename... Conds>
struct or_ {
    static constexpr bool value = false;
};

template<typename Cond, typename... Conds>
struct or_<Cond, Conds...> {
    static constexpr bool value = std::conditional<Cond::value, std::true_type, or_<Conds...>>::type::value;
};

这种形式在我看来直觉上更好,因为值总是位于类型本身,而不是某些超类中,但我不确定这是否通常被认为是可取的。

附:我知道在 C++17 中我经常可以使用折叠表达式。但我的目标是与 C++11 兼容。

【问题讨论】:

  • 这是个人喜好问题。最终结果是相同的,并且两种方法都没有任何特别的优势。
  • 这主要是基于意见的。您立即忽略的继承具有重要的好处(简化标签调度,加上integral_constant 提供的几个实用程序)。我想有人可能会争辩说,有人可能会忘记第二种形式的static,尤其是如果你写的是const而不是constexpr,这可能会导致在依赖上下文中令人费解的替换失败。
  • 在 C++11 中,人们仍然可以使用以某种方式预期折叠表达式的技巧作为第三种方法。在 C++14 中,您可以使用模板变量来实现,这是另一种不同的方法。为什么你只对问题中的两个感兴趣?
  • @skypjack:你能指出我提到的另外两种方法的例子吗?我不确定我是否明白你的意思。
  • @RossBencina Here 就是其中之一。

标签: c++ c++11 templates


【解决方案1】:

如果我们真的不关心短路,那么总会有bool_pack

template<bool...> struct bool_pack;

template<class... Ts>
using and_ = typename std::is_same<bool_pack<true, Ts::value...>,
                                   bool_pack<Ts::value..., true>>::type;

template<class T>
using not_ = std::integral_constant<bool, !T::value>;

template<class... Ts>
using or_ = not_<std::is_same<bool_pack<false, Ts::value...>, 
                              bool_pack<Ts::value..., false>>>; 
      // or not_<and_<not_<Ts>...>>

【讨论】:

    【解决方案2】:

    两种形式都会产生类似的效果。如果您想让代码更好,而不是在这两者之间进行选择,您可以尝试避免递归...您的 or_ 代码可以成功替换为以下代码:

    #include <utility>
    #include <type_traits>
    #include <iostream>
    
    template <std::size_t, class T>
    struct indexed_condition;
    
    template <std::size_t I>
    struct indexed_condition<I, std::false_type> { };
    
    template <class...>
    struct voider {
        using type = void;
    };
    
    template <class... Conds>
    struct helper: std::true_type {
    };
    
    template <std::size_t... Is, class... Conds>
    struct helper<typename voider<decltype(indexed_condition<Is, Conds>())...>::type, std::index_sequence<Is...>, Conds...>: std::false_type {
    };
    
    template <class... Conds>
    struct my_or: helper<void, std::make_index_sequence<sizeof...(Conds)>, Conds...> { };
    
    int main() {
        std::cout << my_or<std::true_type, std::false_type, std::true_type>::value << std::endl;
        std::cout << my_or<std::false_type, std::false_type, std::false_type>::value << std::endl;
    }
    

    [live demo]

    输出:

    1
    0

    直截了当,它不会影响运行时,但会影响编译时的效率。

    还有一件事 - std::integer_sequence 是 c++14 构造,但也可以在 c++11 中实现,参见例如this implementation

    【讨论】:

    • 虚拟编译时继承。哈。
    • 您会说这是当今的首选方法吗?您对为什么它编译得更快有任何进一步的指示吗?
    • @RossBencina 在 Yakk 的评论之后,我开始怀疑它......如果我找到在没有虚拟继承的情况下做同样事情的方法,我将编辑我的答案
    • 我仍然希望看到针对您的新方法的基准。我没有看到模板实例化的数量因此而大大减少;远非如此。
    • @t.c.递归深度大大减少。和名字长度!至于虚拟编译时继承,我觉得很有趣,但并不令人反感。
    【解决方案3】:

    这是试图使@W.F. 的答案更简洁的版本。

    template<std::size_t, class T, class=void>
    struct detect_truthy : virtual std::false_type {};
    template<std::size_t I, class T>
    struct detect_truthy<I, T,
      typename std::enable_if<T::value>::type
    > : virtual std::true_type {};
    
    template<class...>struct pack {};
    
    template<class Pack, class Is=void>
    struct detect_truthies;
    template<class...Ts>
    struct detect_truthies<pack<Ts...>,void>:
      detect_truthies<pack<Ts...>, std::make_index_sequence<sizeof...(Ts)>>
    {};
    template<class...Ts, std::size_t...Is>
    struct detect_truthies<pack<Ts...>,std::index_sequence<Is...>>:
      detect_truthy<Is, Ts>...
    {};
    
    std::true_type or_helper_f( std::true_type* );
    std::false_type or_helper_f( ... );
    std::false_type and_helper_f( std::false_type* );
    std::true_type and_helper_f( ... );
    
    template<class...Bools>
    struct my_or:
      decltype( or_helper_f( (detect_truthies<pack<Bools...>>*)0 ) )
    {};
    template<class...Bools>
    struct my_and:
      decltype( and_helper_f( (detect_truthies<pack<Bools...>>*)0 ) )
    {};
    

    live example.

    它使用index_sequencemake_index_sequence,来自C++14,但可以使用高质量的C++11版本。

    这种技术的要点是,唯一的递归/线性/二进制模板扩展发生在index_sequence 内。在其他任何地方,我们都直接展开参数包。

    由于index_sequence 可以以牺牲可读性为代价写成高效(例如,MSVC 将其实现为内在函数!其他编译器使用复杂的二叉树生成器。),这集中了那里的高性价比问题。

    通常,您希望在编译时隔离您执行 O(N^2) 工作的位置。当你线性继承时,你做了 O(N^2) 的工作。

    【讨论】:

      【解决方案4】:

      还有第三种选择是使用 constexpr 函数

      static constexpr bool any_of(){
        return false;
      }
      
      template<class... T>
      static constexpr bool any_of(bool b, T... rest){
        return b || any_of(rest...);
      }
      
      
      template<class... Cond>
      using or_ = std::integral_constant<bool, any_of(Cond::value...)>;
      
      int main(){
        static_assert(or_<std::true_type, std::true_type, std::true_type>::value,"");
        static_assert(or_<std::false_type, std::false_type, std::true_type>::value,"");
        static_assert(or_<std::false_type, std::false_type, std::false_type>::value == false,"");
      }
      

      继承方法的优点是您可以将 _or 类传递给任何同时使用 std::true_type 和 std::false_type 重载的函数,这在您使用 sfinae 时会有所帮助。

      就编译速度而言,很难说,因为它完全依赖于编译器(编译器人员正在努力提高 TMP 的编译速度,所以现在任何好的建议都可能在没有警告的情况下变成坏建议。

      编译速度的主要内容是密切关注您的算法。在 C++ 11/14 中进行条件编译非常困难,编译器将评估 std::conditional 的两侧。 (他们必须让您可以合法地将 std::conditional 专门用于 _or 类之一,并且编译器无法证明您是一个理性的人)

      这意味着在这两种情况下,编译器都会为每个部分案例列表实例化 or 类。这可能还不错,但有时您可以突然将 O(N) 算法转换为 O(2^N) 算法,这时编译时间才真正开始受到影响。

      【讨论】:

      • 查看comments to the question 以获得更短的版本。
      • 请注意,上面的代码在编译时已经是 O(N^2) 了。 any_of 接受 N 个参数,然后递归接受 N-1 个参数等。这些总和为 O(N^2)。您实际上可以使用基于二叉树的递归来解决此问题,但它很混乱。
      【解决方案5】:

      or_ 的编译时间方法的上下文中更有效:

      #include <type_traits>
      #include <utility>
      
      template <class... Conds>
      std::false_type or_impl(Conds... conds);
      template <class... Conds>
      std::true_type or_impl(...);
      
      template <class, class T>
      using typer = T;
      
      template <class... Conds>
      using or_ = decltype(or_impl<typer<Conds, std::false_type>...>(Conds{}...));
      
      int main() {
          static_assert(or_<std::false_type, std::true_type, std::false_type>::value, "!");
          static_assert(!or_<std::false_type, std::false_type, std::false_type>::value, "!");
      }
      

      [live demo]

      【讨论】:

        猜你喜欢
        • 2021-06-21
        • 1970-01-01
        • 2011-01-24
        • 1970-01-01
        • 2016-04-04
        • 2022-07-29
        • 2017-07-08
        • 1970-01-01
        • 2010-12-24
        相关资源
        最近更新 更多