【问题标题】:How is std::tuple implemented?std::tuple 是如何实现的?
【发布时间】:2011-05-01 18:49:48
【问题描述】:

我想知道元组是如何在 C++0x 的标准库中实现的。我尝试阅读description in libstdc++ manual,然后阅读template listing,但很难理解它是如何工作的,尤其是在阅读代码时。

有人可以用几句话向我解释元组实现的想法吗?我想知道这一点,因为我考虑在我的代码中使用元组,我想了解它是如何工作的以及它带来了什么类型的开销(仅延长编译时间,在内存上执行许多复制操作,在构造函数中执行许多其他函数等)。

【问题讨论】:

    标签: c++ c++11 tuples std


    【解决方案1】:

    实现元组的一种方法是使用多重继承。元组元素由叶类持有,元组类本身继承自多个叶。在伪代码中:

    template<typename T0, typename T1, ..., typename Tn>
    class PseudoTuple : TupleLeaf<0, T0>, TupleLeaf<1, T1>, ..., TupleLeaf<n, Tn> {
       ...
    };
    

    每个叶子都有一个索引,因此即使它们包含的类型相同,每个基类也变得唯一,因此我们可以使用简单的 static_cast 访问 nth 元素:

    static_cast<TupleLeaf<0, T0>*>(this);
    // ...
    static_cast<TupleLeaf<n, Tn>*>(this);
    

    我在这里写了关于这个“扁平”元组实现的详细解释:C++11 tuple implementation details (Part 1)

    【讨论】:

    • 是的,这是一个很好的解释!不幸的是,这不是在 libstdc++ 中实现元组的方式,它坚持递归实现。等不及更广泛分布的 libc++ 了!
    • 简要描述非递归实现也可能很有用。
    • @KyleStrand erm,这 is 是非递归实现(T : L1, L2, L3 vs T : L1 : L2 : L3 的递归实现)
    • @mitchnull 抱歉,我的意思是递归实现。
    • 您能否简单介绍一下递归与非递归实现的优缺点?
    【解决方案2】:

    我以为我会添加一个非伪代码的简单递归实现以供参考

    #include <iostream>
    
    // Contains the actual value for one item in the tuple. The 
    // template parameter `i` allows the
    // `Get` function to find the value in O(1) time
    template<std::size_t i, typename Item>
    struct TupleLeaf {
        Item value;
    };
    
    // TupleImpl is a proxy for the final class that has an extra 
    // template parameter `i`.
    template<std::size_t i, typename... Items>
    struct TupleImpl;
    
    // Base case: empty tuple
    template<std::size_t i>
    struct TupleImpl<i>{};
    
    // Recursive specialization
    template<std::size_t i, typename HeadItem, typename... TailItems>
    struct TupleImpl<i, HeadItem, TailItems...> :
        public TupleLeaf<i, HeadItem>, // This adds a `value` member of type HeadItem
        public TupleImpl<i + 1, TailItems...> // This recurses
        {};
    
    // Obtain a reference to i-th item in a tuple
    template<std::size_t i, typename HeadItem, typename... TailItems>
    HeadItem& Get(TupleImpl<i, HeadItem, TailItems...>& tuple) {
        // Fully qualified name for the member, to find the right one 
        // (they are all called `value`).
        return tuple.TupleLeaf<i, HeadItem>::value;
    }
    
    // Templated alias to avoid having to specify `i = 0`
    template<typename... Items>
    using Tuple = TupleImpl<0, Items...>;
    
    int main(int argc, char** argv) {
        Tuple<int, float, std::string> tuple;
        Get<0>(tuple) = 5;
        Get<1>(tuple) = 8.3;
        Get<2>(tuple) = "Foo";
        std::cout << Get<0>(tuple) << std::endl;
        std::cout << Get<1>(tuple) << std::endl;
        std::cout << Get<2>(tuple) << std::endl;
        return 0;
    }
    

    【讨论】:

    • 仅作记录:这是一个递归实现,而不是我描述的平面实现 :)
    • 抱歉,我误解了你的回答。我编辑了我的,以删除它实现了您的解决方案的声明。
    • @AndreaAllais 这真的很酷。但是,我很好奇 Get 函数是如何工作的?忽略使用。如果我使用TupleImpl&lt;0, int, float, std::string&gt; tuple; 进行初始化,当我调用Get&lt;1&gt;(tuple) 时,调用不等于Get(TupleImpl&lt;1, int, {float,std::string}&gt;&amp; tuple)。既然TupleLeaf&lt;1, int&gt; 不存在,这不应该失败吗?
    • @Questionable 当您调用Get&lt;1&gt;(tuple) 时,参数会从TupleImpl&lt;0, int, float, std::string&gt; 向上转换为TupleImpl&lt;1, float, std::string&gt;。由于前导整数模板参数的值,这种向上转换是唯一可能的。请注意,upcast 删除了 int 模板参数。这是因为随着前导整数模板参数的增加,类型模板参数在继承层次结构中被一一删除。因此Get&lt;1&gt;(tuple) 最终调用tuple.TupleLeaf&lt;1, float&gt;::value,这是有效的。
    【解决方案3】:

    元组通常实现为编译时链表。

    代码通过模板语法有点混淆,但通常存在以下元素:

    1. 带有头尾元素(cons-elements)的类链
    2. 一个空的尾部实例,表示列表的结尾。
    3. 递归代码将列表遍历到某个索引,实现为递归模板实例化(在编译时实例化)。

    在 C++03 中存在合理的实现(例如 boost)。

    如 Motti 所述,可变参数模板允许无限数量的元素。

    成本通常是编译时间。复制构造函数可能会在初始化期间(最多 1 个)以及复制元组本身时被调用。

    【讨论】:

      【解决方案4】:

      可以通过引入核心语言的variadic templates 实现std::tuple

      我知道这是在乞求问题,但它为您提供了更好的搜索短语来研究。

      【讨论】:

      • 另外,实现不是微不足道的,OP 提到通过阅读示例实现的代码尝试并未能理解它的事实证明了这一点。
      • 这个答案正在讨论on meta
      • @KyleStrand OP 在 2010 年阅读了代码,当时可变参数模板还没有被普遍理解。我给了他理解可变参数模板的工具。我认为这是一个答案。
      【解决方案5】:

      通过组合而不是继承使用递归数据结构的实现:

      #include <iostream>
      
      template <typename T, typename... Ts>
      struct Tuple {
          Tuple(const T& t, const Ts&... ts)
              : value(t)
              , rest(ts...)
          {
          }
      
          constexpr int size() const { return 1 + rest.size(); }
      
          T value;
          Tuple<Ts...> rest;
      };
      template <typename T>
      struct Tuple<T> {
          Tuple(const T& t)
              : value(t)
          {
          }
      
          constexpr int size() const { return 1; }
      
          T value;
      };
      
      template <size_t i, typename T, typename... Ts>
      struct nthType : nthType<i-1, Ts...> {
          static_assert(i < sizeof...(Ts) + 1, "index out of bounds");
      };
      
      template <typename T, typename... Ts>
      struct nthType<0, T, Ts...> { T value; };
      
      template <size_t i>
      struct getter {
          template <typename... Ts>
          static decltype(nthType<i, Ts...>::value)& get(Tuple<Ts...>& t) {
              return getter<i-1>::get(t.rest);
          }
      };
      template <>
      struct getter<0> {
          template <typename T, typename... Ts>
          static T& get(Tuple<T, Ts...>& t) {
              return t.value;
          }
      };
      
      template <size_t i, typename... Ts>
      decltype(nthType<i, Ts...>::value)& get(Tuple<Ts...>& t) {
          return getter<i>::get(t);
      }
      
      
      int main()
      {
          Tuple<int,int,float> t(1,2,3.4);
          
          std::cout << get<0>(t) << "\n";
          std::cout << get<1>(t) << "\n";
          std::cout << get<2>(t) << "\n";
          // std::cout << get<3>(t) << "\n"; // error with useful information
          
          return 0;
      }
      

      我发现这种方法比其他方法要好得多,因为它在执行 apply map 等递归操作时使用起来非常直观,尤其是如果您曾经在函数式编程中使用过递归数据结构。当然,对于索引检索,我们需要做一些奇怪的模板的东西,但一般使用递归性质是非常直观的。如果有人能解释为什么这种设置不常见,我很想知道。

      【讨论】:

      • 这比可变参数实现更慢且更受限制。模板递归深度限制您使用这种方法创建非常大的元组,并且可变参数实现相对于编译器的递归实现的好处是众所周知的。
      • 有趣,我想了解更多。你有可以分享的资源吗?我找不到关于这个特定主题的任何内容。另外,“递归实现上的可变实现”是什么意思?两种实现都不是递归的吗?
      • 查看接受的答案以了解如何设置非递归实现。您可以从那里推断出大多数步骤是什么。有趣的是,非递归实现允许聚合初始化,这有相当多的用途。
      • 啊,我明白了,我很困惑,因为我认为必须递归使用可变参数模板(这是真的),但这并不意味着结果输出是递归的,如果这有意义的话。
      • 我想是的。有很多方法可以在没有递归的情况下使用可变参数模板,但是在 IMO 中,其中很多都是不直观的。在非递归版本中用于扩展元组的 std::make_index_sequence 的技巧非常有用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-06-15
      • 2013-11-01
      • 2014-07-01
      • 2020-04-26
      • 2010-11-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多