【问题标题】:Sequence-zip function for c++11?c ++ 11的序列压缩函数?
【发布时间】:2012-01-20 14:44:46
【问题描述】:

使用新的基于范围的 for 循环,我们可以编写如下代码

for(auto x: Y) {}

哪个 IMO 是(例如)巨大的改进

for(std::vector<int>::iterator x=Y.begin(); x!=Y.end(); ++x) {}

它可以用来循环两个同时的循环,比如 Python 的 zip 函数吗?对于不熟悉 Python 的人,代码:

Y1 = [1,2,3]
Y2 = [4,5,6,7]
for x1,x2 in zip(Y1,Y2):
    print x1,x2

作为输出(1,4) (2,5) (3,6)

【问题讨论】:

  • 基于范围的for 只能与一个变量一起使用,所以不能。如果你想一次访问两个值,你必须使用类似std::pair
  • @SethCarnegie:不是直接的,但你可以想出一个 zip() 函数,它返回元组并遍历元组列表。
  • @AndréCaron 你是对的,我的“不”是说你不能使用两个变量,而不是你不能一次迭代两个容器。
  • 显然for(;;) 可以得到这种行为,尽管是长期的,所以问题真的是:是否有可能同时对两个对象进行“自动”?
  • 在未来的修订版中(希望是 C++17),对 STL 的大修将包括 ranges。那么view::zip 可能会提供首选的解决方案。

标签: c++ c++11 sequences


【解决方案1】:

警告: 如果输入容器的长度不同(可能会崩溃或迭代结束)。


从 Boost 1.56.0(2014 年 8 月 7 日)开始,您可以 use boost::combine(该功能存在于早期版本中但未记录):

#include <boost/range/combine.hpp>
#include <vector>
#include <list>
#include <string>

int main() {
    std::vector<int> a {4, 5, 6};
    double b[] = {7, 8, 9};
    std::list<std::string> c {"a", "b", "c"};
    for (auto tup : boost::combine(a, b, c, a)) {    // <---
        int x, w;
        double y;
        std::string z;
        boost::tie(x, y, z, w) = tup;
        printf("%d %g %s %d\n", x, y, z.c_str(), w);
    }
}

这会打印出来

4 7 一 4 5 8 乙 5 6 9 c 6

在早期版本中,您可以像这样自己定义一个范围:

#include <boost/iterator/zip_iterator.hpp>
#include <boost/range.hpp>

template <typename... T>
auto zip(T&&... containers) -> boost::iterator_range<boost::zip_iterator<decltype(boost::make_tuple(std::begin(containers)...))>>
{
    auto zip_begin = boost::make_zip_iterator(boost::make_tuple(std::begin(containers)...));
    auto zip_end = boost::make_zip_iterator(boost::make_tuple(std::end(containers)...));
    return boost::make_iterator_range(zip_begin, zip_end);
}

用法相同。

【讨论】:

  • 你能用这个来排序吗?即 std::sort(zip(a.begin(),...),zip(a.end(),...),[](tup a, tup b){a.get() > b.get()}); ?
  • @gnzlbg: No you can't.
  • 我会被 optional 元素所诱惑,以寻求最终迭代的可能性......
  • 你可以用 std::make_tuple 和 std::tie 做到这一点吗?我试图在最小化提升依赖的同时使用它,但我无法让它工作。
  • @kennytm 知道为什么他们决定选择 UB 而不是仅仅在最短范围的末尾结束吗?
【解决方案2】:

std::transform 可以轻松做到这一点:

std::vector<int> a = {1,2,3,4,5};
std::vector<int> b = {1,2,3,4,5};
std::vector<int>c;
std::transform(a.begin(),a.end(), b.begin(),
               std::back_inserter(c),
               [](const auto& aa, const auto& bb)
               {
                   return aa*bb;
               });
for(auto cc:c)
    std::cout<<cc<<std::endl;

如果第二个序列较短,我的实现似乎给出了默认的初始化值。

【讨论】:

  • 如果第二个序列较短,那么我希望这是 UB,因为您将在 b 的末尾进行迭代。
  • @Adrian 部分 - 应该注意 UB 是由于 vector&lt;&gt;::iterator,而不是 std::transform。用户应该提供自己的迭代器来处理范围结束,如果他们期望的话,例如通过引发错误或在末尾返回零。
【解决方案3】:

所以我之前无聊时写了这个zip,我决定发布它,因为它与其他的不同之处在于它不使用boost并且看起来更像c++ stdlib。

template <typename Iterator>
    void advance_all (Iterator & iterator) {
        ++iterator;
    }
template <typename Iterator, typename ... Iterators>
    void advance_all (Iterator & iterator, Iterators& ... iterators) {
        ++iterator;
        advance_all(iterators...);
    } 
template <typename Function, typename Iterator, typename ... Iterators>
    Function zip (Function func, Iterator begin, 
            Iterator end, 
            Iterators ... iterators)
    {
        for(;begin != end; ++begin, advance_all(iterators...))
            func(*begin, *(iterators)... );
        //could also make this a tuple
        return func;
    }

使用示例:

int main () {
    std::vector<int> v1{1,2,3};
    std::vector<int> v2{3,2,1};
    std::vector<float> v3{1.2,2.4,9.0};
    std::vector<float> v4{1.2,2.4,9.0};
     zip (
            [](int i,int j,float k,float l){
                std::cout << i << " " << j << " " << k << " " << l << std::endl;
            },
            v1.begin(),v1.end(),v2.begin(),v3.begin(),v4.begin());
}

【讨论】:

  • 你应该检查 any 的迭代器是否在末尾。
  • @Xeo 所有范围的大小应与第一个或更大
  • 您能解释一下[](int i,int j,float k,float l) 的工作原理吗?这是一个 lambda 函数吗?
  • 一个常见的需求是压缩不同大小的范围,甚至无限范围。
  • @Xeo 我明白你的意思,只是像这样的 stdlib 函数通常只是假设第一个范围是最小的,这就是我要使用的模式
【解决方案4】:

您可以使用基于boost::zip_iterator 的解决方案。创建一个虚假的容器类,维护对容器的引用,并从 beginend 成员函数返回 zip_iterator。现在你可以写了

for (auto p: zip(c1, c2)) { ... }

示例实现(请测试):

#include <iterator>
#include <boost/iterator/zip_iterator.hpp>

template <typename C1, typename C2>
class zip_container
{
    C1* c1; C2* c2;

    typedef boost::tuple<
        decltype(std::begin(*c1)), 
        decltype(std::begin(*c2))
    > tuple;

public:
    zip_container(C1& c1, C2& c2) : c1(&c1), c2(&c2) {}

    typedef boost::zip_iterator<tuple> iterator;

    iterator begin() const
    {
         return iterator(std::begin(*c1), std::begin(*c2));
    }

    iterator end() const
    {
         return iterator(std::end(*c1), std::end(*c2));
    }
};

template <typename C1, typename C2>
zip_container<C1, C2> zip(C1& c1, C2& c2)
{
    return zip_container<C1, C2>(c1, c2);
}

我将可变参数版本留给读者作为一个很好的练习。

【讨论】:

  • +1:Boost.Range 可能应该包含这个。事实上,我会给他们一个功能请求。
  • @NicolBolas:你做得很好。这应该很容易用boost::iterator_range + boost::zip_iterator 实现,即使是可变参数版本。
  • 我相信如果范围的长度不同,这将永远不会终止(并且具有未定义的行为)。
  • boost::zip_iterator 不适用于不同长度的范围
  • 这也应该在干净的 c++03 中使用 pair 而不是 tuple。当长度不相等时,这也会产生问题。 end() 可以通过获取最小容器的相应 end() 来完成某些事情。这似乎在规范中,就像在 OPs 问题中一样。
【解决方案5】:

参见&lt;redi/zip.h&gt; 了解zip 函数,该函数与range-base for 一起使用并接受任意数量的范围,可以是右值或左值,并且可以是不同的长度(迭代将在最短的末尾停止范围)。

std::vector<int> vi{ 0, 2, 4 };
std::vector<std::string> vs{ "1", "3", "5", "7" };
for (auto i : redi::zip(vi, vs))
  std::cout << i.get<0>() << ' ' << i.get<1>() << ' ';

打印0 1 2 3 4 5

【讨论】:

  • 你也可以使用boost/tuple/tuple_io.hppcout &lt;&lt; i;
  • 这对我有用。但是,在我的代码中,我必须使用 boost::get&lt;0&gt;(i)boost::get&lt;1&gt;(i) 的等效项。我不确定为什么不能直接调整原始示例,这可能与我的代码对容器进行常量引用有关。
【解决方案6】:

range-v3:

#include <range/v3/all.hpp>
#include <vector>
#include <iostream>

namespace ranges {
    template <class T, class U>
    std::ostream& operator << (std::ostream& os, common_pair<T, U> const& p)
    {
      return os << '(' << p.first << ", " << p.second << ')';
    }
}

using namespace ranges::v3;

int main()
{
    std::vector<int> a {4, 5, 6};
    double b[] = {7, 8, 9};
    std::cout << view::zip(a, b) << std::endl; 
}

输出:

[(4, 7),(5, 8),(6, 9)]

【讨论】:

    【解决方案7】:

    如果您喜欢运算符重载,这里有三种可能性。前两个分别使用std::pair&lt;&gt;std::tuple&lt;&gt;作为迭代器;第三个将此扩展到基于范围的for。请注意,并不是每个人都会喜欢这些运算符的定义,因此最好将它们保存在单独的命名空间中,并在您要使用它们的函数(不是文件!)中使用 using namespace

    #include <iostream>
    #include <utility>
    #include <vector>
    #include <tuple>
    
    // put these in namespaces so we don't pollute global
    namespace pair_iterators
    {
        template<typename T1, typename T2>
        std::pair<T1, T2> operator++(std::pair<T1, T2>& it)
        {
            ++it.first;
            ++it.second;
            return it;
        }
    }
    
    namespace tuple_iterators
    {
        // you might want to make this generic (via param pack)
        template<typename T1, typename T2, typename T3>
        auto operator++(std::tuple<T1, T2, T3>& it)
        {
            ++( std::get<0>( it ) );
            ++( std::get<1>( it ) );
            ++( std::get<2>( it ) );
            return it;
        }
    
        template<typename T1, typename T2, typename T3>
        auto operator*(const std::tuple<T1, T2, T3>& it)
        {
            return std::tie( *( std::get<0>( it ) ),
                             *( std::get<1>( it ) ),
                             *( std::get<2>( it ) ) );
        }
    
        // needed due to ADL-only lookup
        template<typename... Args>
        struct tuple_c
        {
            std::tuple<Args...> containers;
        };
    
        template<typename... Args>
        auto tie_c( const Args&... args )
        {
            tuple_c<Args...> ret = { std::tie(args...) };
            return ret;
        }
    
        template<typename T1, typename T2, typename T3>
        auto begin( const tuple_c<T1, T2, T3>& c )
        {
            return std::make_tuple( std::get<0>( c.containers ).begin(),
                                    std::get<1>( c.containers ).begin(),
                                    std::get<2>( c.containers ).begin() );
        }
    
        template<typename T1, typename T2, typename T3>
        auto end( const tuple_c<T1, T2, T3>& c )
        {
            return std::make_tuple( std::get<0>( c.containers ).end(),
                                    std::get<1>( c.containers ).end(),
                                    std::get<2>( c.containers ).end() );
        }
    
        // implement cbegin(), cend() as needed
    }
    
    int main()
    {
        using namespace pair_iterators;
        using namespace tuple_iterators;
    
        std::vector<double> ds = { 0.0, 0.1, 0.2 };
        std::vector<int   > is = {   1,   2,   3 };
        std::vector<char  > cs = { 'a', 'b', 'c' };
    
        // classical, iterator-style using pairs
        for( auto its  = std::make_pair(ds.begin(), is.begin()),
                  end  = std::make_pair(ds.end(),   is.end()  ); its != end; ++its )
        {
            std::cout << "1. " << *(its.first ) + *(its.second) << " " << std::endl;
        }
    
        // classical, iterator-style using tuples
        for( auto its  = std::make_tuple(ds.begin(), is.begin(), cs.begin()),
                  end  = std::make_tuple(ds.end(),   is.end(),   cs.end()  ); its != end; ++its )
        {
            std::cout << "2. " << *(std::get<0>(its)) + *(std::get<1>(its)) << " "
                               << *(std::get<2>(its)) << " " << std::endl;
        }
    
        // range for using tuples
        for( const auto& d_i_c : tie_c( ds, is, cs ) )
        {
            std::cout << "3. " << std::get<0>(d_i_c) + std::get<1>(d_i_c) << " "
                               << std::get<2>(d_i_c) << " " << std::endl;
        }
    }
    

    【讨论】:

      【解决方案8】:

      我独立遇到了同样的问题,不喜欢上述任何一种语法。所以,我有一个简短的头文件,它与 boost zip_iterator 基本相同,但有一些宏使语法更适合我:

      https://github.com/cshelton/zipfor

      例如你可以这样做

      vector<int> a {1,2,3};
      array<string,3> b {"hello","there","coders"};
      
      zipfor(i,s eachin a,b)
          cout << i << " => " << s << endl;
      

      主要的语法糖是我可以命名每个容器中的元素。我还包括一个“mapfor”,它做同样的事情,但用于地图(命名元素的“.first”和“.second”)。

      【讨论】:

      • 这很好!是否可以将任意数量的参数都限制为您巧妙的模板限制为有限数量?
      • 目前它最多只能处理 9 个并行容器。这将很容易推进。虽然可变参数宏允许单个“zipfor”宏来处理不同数量的参数,但仍然必须为每个宏编写一个单独的宏(被分派到)。见groups.google.com/forum/?fromgroups=#!topic/comp.std.c/…stackoverflow.com/questions/15847837/…
      • 它能很好地处理不同大小的参数吗? (如 OP 中所述)
      • @coyotte508,它假定第一个容器的元素数量最少(并忽略其他容器中的额外元素)。不做这个假设很容易修改,但是当元素数量匹配时,它会减慢速度(目前不比手写慢)。
      【解决方案9】:
      // declare a, b
      BOOST_FOREACH(boost::tie(a, b), boost::combine(list_of_a, list_of_b)){
          // your code here.
      }
      

      【讨论】:

        【解决方案10】:

        如果你有一个兼容 C++14 的编译器(例如 gcc5),你可以使用 Ryan Haining 的 cppitertools 库中提供的 zip,这看起来很有前途:

        array<int,4> i{{1,2,3,4}};
        vector<float> f{1.2,1.4,12.3,4.5,9.9};
        vector<string> s{"i","like","apples","alot","dude"};
        array<double,5> d{{1.2,1.2,1.2,1.2,1.2}};
        
        for (auto&& e : zip(i,f,s,d)) {
            cout << std::get<0>(e) << ' '
                 << std::get<1>(e) << ' '
                 << std::get<2>(e) << ' '
                 << std::get<3>(e) << '\n';
            std::get<1>(e)=2.2f; // modifies the underlying 'f' array
        }
        

        【讨论】:

          【解决方案11】:

          对于C++ stream processing library,我正在写我正在寻找一种不依赖第三方库并且可以与任意数量的容器一起使用的解决方案。我最终得到了这个解决方案。它类似于使用 boost 的公认解决方案(如果容器长度不相等,也会导致未定义的行为)

          #include <utility>
          
          namespace impl {
          
          template <typename Iter, typename... Iters>
          class zip_iterator {
          public:
            using value_type = std::tuple<const typename Iter::value_type&,
                                          const typename Iters::value_type&...>;
          
            zip_iterator(const Iter &head, const Iters&... tail)
                : head_(head), tail_(tail...) { }
          
            value_type operator*() const {
              return std::tuple_cat(std::tuple<const typename Iter::value_type&>(*head_), *tail_);
            }
          
            zip_iterator& operator++() {
              ++head_; ++tail_;
              return *this;
            }
          
            bool operator==(const zip_iterator &rhs) const {
              return head_ == rhs.head_ && tail_ == rhs.tail_;
            }
          
            bool operator!=(const zip_iterator &rhs) const {
              return !(*this == rhs);
            }
          
          private:
            Iter head_;
            zip_iterator<Iters...> tail_;
          };
          
          template <typename Iter>
          class zip_iterator<Iter> {
          public:
            using value_type = std::tuple<const typename Iter::value_type&>;
          
            zip_iterator(const Iter &head) : head_(head) { }
          
            value_type operator*() const {
              return value_type(*head_);
            }
          
            zip_iterator& operator++() { ++head_; return *this; }
          
            bool operator==(const zip_iterator &rhs) const { return head_ == rhs.head_; }
          
            bool operator!=(const zip_iterator &rhs) const { return !(*this == rhs); }
          
          private:
            Iter head_;
          };
          
          }  // namespace impl
          
          template <typename Iter>
          class seq {
          public:
            using iterator = Iter;
            seq(const Iter &begin, const Iter &end) : begin_(begin), end_(end) { }
            iterator begin() const { return begin_; }
            iterator end() const { return end_; }
          private:
            Iter begin_, end_;
          };
          
          /* WARNING: Undefined behavior if iterator lengths are different.
           */
          template <typename... Seqs>
          seq<impl::zip_iterator<typename Seqs::iterator...>>
          zip(const Seqs&... seqs) {
            return seq<impl::zip_iterator<typename Seqs::iterator...>>(
                impl::zip_iterator<typename Seqs::iterator...>(std::begin(seqs)...),
                impl::zip_iterator<typename Seqs::iterator...>(std::end(seqs)...));
          }
          

          【讨论】:

          • link broken... 如果帖子显示如何使用它会很有用,例如主要()?
          • @javaLover:您可以像 @knedlsepp 的答案中的 cppitertools 一样使用它。一个显着的区别是,使用上述解决方案,您无法修改底层容器,因为 operator* for seq::iterator 返回一个 std::tuple 的 const 引用。
          【解决方案12】:

          Boost.Iterators 有 zip_iterator 您可以使用(文档中的示例)。它不适用于 range for,但您可以使用 std::for_each 和 lambda。

          【讨论】:

          • 为什么它不能与基于范围的 for 一起使用?将它与 Boost.Range 结合使用,您应该已经准备好了。
          • @Xeo:我不太了解 Range。我想您可能会涉及一些样板文件并使其工作,但 IMO 仅使用 for_each 会少一些麻烦。
          • 你的意思是这样的事情并不麻烦:std::for_each(make_zip_iterator(make_tuple(Y1.begin(), Y2.begin())), make_zip_iterator(make_tuple(Y1.end(), Y2.end())), [](const tuple&lt;int, int&gt;&amp; t){printf("%d %d\n", get&lt;0&gt;(t), get&lt;1&gt;(t)); });?
          • 我应该启动一个 Lambda 是否 使 std::for_each 有用的活动。 :)
          • @Xeo:这应该是一个单独的问题,但是为什么哦为什么??
          【解决方案13】:

          这是一个不需要提升的简单版本。它不会特别有效,因为它会创建临时值,并且它不会泛化除列表以外的容器,但它没有依赖关系,它解决了最常见的压缩情况。

          template<class L, class R>
          std::list< std::pair<L,R> >  zip(std::list<L> left, std::list<R> right)
          {
          auto l = left.begin();
          auto r = right.begin();
          std::list< std::pair<L,R> > result;
            while( l!=left.end() && r!=right.end() )
              result.push_back( std::pair<L,R>( *(l++), *(r++) ) );
            return result;
          }
          

          虽然其他版本更灵活,但使用列表运算符的目的通常是制作一个简单的单行符。这个版本的好处是常见的情况很简单。

          【讨论】:

          • 如果你实现了一个迭代器,那么你可以避免创建结果并根据需要返回下一个元素。它需要更多代码,因为您需要定义 ++ * 等(for (auto v : containers) 使用的所有运算符)
          • @Et7f3XIV 没错,但看看 16 年 Andrew 编写的这段代码,我会从轨道上对其进行核对,并使用其他答案之一作为起点。
          猜你喜欢
          • 2018-08-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-07-04
          • 2016-04-07
          相关资源
          最近更新 更多