C ++ 11的Sequence-zip函数?


98

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

for(auto x: Y) {}

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

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

它可以用来像Pythons 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
Seth Carnegie

4
@SethCarnegie:不是直接的,但是您可以提出一个zip()返回元组并遍历元组列表的函数。
安德烈·卡隆(

2
@AndréCaron,您是对的,我的“不”是指您不能使用两个变量,而不是不能一次遍历两个容器。
塞斯·卡内基

显然for(;;)可以得到这种行为,尽管是长期有效的,所以问题确实存在:是否可以一次“自动”处理两个对象?

在将来的修订版中(希望是C ++ 17),对STL的全面检查将包括range。然后view :: zip可能会提供首选的解决方案。
约翰·麦克法兰

Answers:


88

警告: boost::zip_iteratorboost::combine作为升压1.63.0(2016年12月26)将导致未定义的行为的,如果输入容器的长度是不相同的(它可能会崩溃或者iterate超过端)。


从Boost 1.56.0(2014年8月7日)开始,您可以使用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);
}

用法是相同的。


1
你可以用它来排序吗?即std :: sort(zip(a.begin(),...),zip(a.end(),...),[](tup a,tup b){a.get <0>() > b.get <0>()}); ?
gnzlbg 2012年


我会optional为过去的迭代可能性而受元素的诱惑……
Yakk-Adam Nevraumont

3
您是否有机会使用std :: make_tuple和std :: tie来做到这一点?我试图在最小化boost依赖的同时使用它,但是我无法使其工作。
卡内罗2014年

@kennytm知道为什么他们决定选择UB而不是只在最短范围内结束吗?
Catskul

18

所以我在无聊之前写了这个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());
}

4
您应该检查是否有任何迭代器在末尾。
Xeo 2013年

1
@Xeo所有范围都应与第一个范围相同或更大
-aaronman

你能解释一下如何[](int i,int j,float k,float l)工作吗?这是lambda函数吗?
钩上2013年

@Hooked是的,它是一个lambda,它基本上可以正常工作,std::for_each但是您可以使用任意数量的范围,lambda中的参数取决于您为该函数提供了多少个迭代器
aaronman 2013年

1
通常需要压缩不同大小的范围,甚至无限范围。
Xeo 2013年

16

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

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);
}

我把可变参数版本作为对读者的极好的练习。


3
+1:Boost.Range可能应该包含此内容。实际上,我将为此给他们一个功能请求。
Nicol Bolas

2
@NicolBolas:你做得很好。使用boost::iterator_range+ boost::zip_iterator甚至可变参数版本都应该很容易实现。
Alexandre C.

1
我相信,如果范围的长度不同,它将永远不会终止(并且具有不确定的行为)。
Jonathan Wakely 2013年

1
boost::zip_iterator不适用于不同长度的范围
Jonathan Wakely 2013年

1
即使在带有对而不是元组的干净c ++ 03中,这也应该起作用。当长度不相等时,这仍然会产生问题。通过使用最小容器的相应end(),可以对end()进行处理。这似乎在规范中,就像在OP问题中一样。
保罗

16

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;

如果第二个序列较短,则我的实现似乎正在提供默认的初始化值。


1
如果第二个序列更短,那么我希望这是UB,因为您将迭代的结尾b
阿德里安

15

在参考资料中<redi/zip.h>找到了一个zip适用于范围为基础for并接受任意数量范围的函数,该函数可以是rvalue或lvalues,并且可以是不同的长度(迭代将在最短范围的末尾停止)。

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


2
你也可以使用boost/tuple/tuple_io.hppcout << i;
kirill_igum

这对我有用。然而,在我的代码,我不得不使用相当于 boost::get<0>(i)boost::get<1>(i)。我不确定为什么不能直接修改原始样本,这可能与我的代码对容器的引用不断有关。
YitzikC '16

11

使用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)]


@ einpoklum-reinstateMonica现在是!
yuyoyuppe

6

我独立地遇到了同样的问题,不喜欢上面任何一个的语法。因此,我有一个简短的头文件,其本质上与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”)。


这很整齐!是否可以采用任意数量的参数,而这些参数是否都受到您聪明地将模板限制为有限数量的限制?
2014年

目前,它最多只能处理9个并行容器。这很容易进行。尽管可变参数宏允许单个“ zipfor”宏处理不同数量的参数,但仍然必须为每个宏编写一个单独的宏(要分派给该参数)。见groups.google.com/forum/?fromgroups=#!topic/comp.std.c/...stackoverflow.com/questions/15847837/...
cshelton

它能很好地处理不同大小的参数吗?(如OP中所述)
coyotte508,2016年

@ coyotte508,它假定第一个容器的元素数最少(并忽略其他容器中的多余元素)。修改为不做此假设将很容易,但是当元素数量匹配时,这会使它变慢(当前不比手写慢)。
cshelton '16

6
// declare a, b
BOOST_FOREACH(boost::tie(a, b), boost::combine(list_of_a, list_of_b)){
    // your code here.
}

6

如果您喜欢运算符重载,则有以下三种可能性。前两个分别使用std::pair<>std::tuple<>作为迭代器。第三个将其扩展到基于范围的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;
    }
}

3

对于我正在编写的C ++流处理库,我正在寻找一种不依赖第三方库并且可以使用任意数量的容器的解决方案。我最终得到了这个解决方案。这类似于使用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)...));
}

1
链接断开...如果帖子显示如何使用它,例如main(),将很有用?
javaLover

@javaLover:您可以使用与@knedlsepp答案中的cppitertools相同的方法。一个显着的区别是,使用上述解决方案,您无法修改基础容器,因为operator*for seq::iterator返回std::tupleconst引用的a。
winnetou

2

如果您有C ++ 14兼容的编译器(例如gcc5),则可以使用Ryan Haining库中zip提供的编译器cppitertools,它看起来非常有前途:

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
}

0

Boost.Iterators zip_iterator是否可以使用(文档中的示例)。它不适用于range,但是您可以使用std::for_eachlambda。


为什么它不能与基于范围的用于?将其与Boost.Range结合使用,就可以设置好了。
Xeo

@Xeo:我不太了解Range。我猜您可能需要一些样板并使其正常工作,但是仅使用IMO for_each便会减少麻烦。
Cat Plus Plus

你的意思是这样的事情是不是麻烦:std::for_each(make_zip_iterator(make_tuple(Y1.begin(), Y2.begin())), make_zip_iterator(make_tuple(Y1.end(), Y2.end())), [](const tuple<int, int>& t){printf("%d %d\n", get<0>(t), get<1>(t)); });
UncleBens 2011年

2
我应该启动Lambda 不会使std :: for_each有用的活动。:)
UncleBens 2011年

2
@Xeo:这可能应该是一个单独的问题,但是为什么呢?
UncleBens 2011年

-2

这是一个不需要增强的简单版本。它不会产生特别的效果,因为它会创建临时值,并且不会对列表以外的其他容器进行泛化,但是它没有依赖关系,并且可以解决最常见的压缩情况。

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;
}

尽管其他版本更灵活,但使用列表运算符的要点通常是使简单的单行代码成为现实。此版本的好处是普通情况很简单。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.