Funzione sequenza-zip per c ++ 11?


100

Con il nuovo ciclo for basato su intervalli possiamo scrivere codice come

for(auto x: Y) {}

Quale IMO è un enorme miglioramento da (per es.)

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

Può essere utilizzato per eseguire il loop su due loop simultanei, come la zipfunzione Pythons ? Per chi non ha familiarità con Python, il codice:

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

Fornisce come output (1,4) (2,5) (3,6)


Basato su intervallo forpuò essere utilizzato solo con una variabile, quindi no. Se si desidera accedere a due valori alla volta, è necessario utilizzare qualcosa del tipostd::pair
Seth Carnegie,

4
@SethCarnegie: non direttamente, ma potresti inventare una zip()funzione che restituisce tuple e itera sull'elenco delle tuple.
André Caron

2
@ AndréCaron hai ragione, il mio "no" voleva dire che non puoi usare due variabili, non che non puoi iterare su due contenitori contemporaneamente.
Seth Carnegie

Chiaramente for(;;)può ottenere questo comportamento, anche se a mano lunga, quindi la domanda è davvero: è possibile "auto" su due oggetti contemporaneamente?

In una revisione futura (si spera in C ++ 17), una revisione dell'STL includerà intervalli . Quindi view :: zip può fornire la soluzione preferita.
John McFarlane

Risposte:


89

Attenzione: boost::zip_iterator e boost::combinedal Boost 1.63.0 (26 dicembre 2016) provocherà un comportamento indefinito se la lunghezza dei contenitori di input non è la stessa (potrebbe bloccarsi o scorrere oltre la fine).


A partire da Boost 1.56.0 (7 agosto 2014) è possibile utilizzareboost::combine (la funzione esiste nelle versioni precedenti ma non documentata):

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

Questo verrebbe stampato

4 7 a 4
5 8 b 5
6 9 c 6

Nelle versioni precedenti, potresti definire tu stesso un intervallo come questo:

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

L'utilizzo è lo stesso.


1
potresti usarlo per l'ordinamento? cioè std :: sort (zip (a.begin (), ...), zip (a.end (), ...), [] (tup a, tup b) {a.get <0> () > b.get <0> ()}); ?
gnzlbg

@gnzlbg: No, non puoi .
kennytm

Sarei tentato dagli optionalelementi per le possibilità di iterazione oltre la fine ...
Yakk - Adam Nevraumont

3
C'è qualche possibilità che tu possa farlo con std :: make_tuple e std :: tie? Stavo cercando di usarlo riducendo al minimo la dipendenza dal boost ma non sono riuscito a farlo funzionare.
Carneiro

@kennytm, hai idea del perché abbiano deciso di andare con UB invece di finire alla fine della gamma più corta del gruppo?
Catskul

18

Quindi ho scritto questo zip prima quando ero annoiato, ho deciso di postarlo perché è diverso dagli altri in quanto non usa boost e assomiglia più allo stdlib c ++.

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

Esempio di utilizzo:

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
Dovresti controllare se qualcuno degli iteratori è alla fine.
Xeo

1
@Xeo tutti gli intervalli dovrebbero avere le stesse dimensioni del primo o maggiori
aaronman

Puoi spiegare come [](int i,int j,float k,float l)funziona? È una funzione lambda?
Hooked

@Hooked sì, è un lambda, fondamentalmente funziona std::for_eachma puoi usare un numero arbitrario di intervalli, i parametri nel lambda dipendono da quanti iteratori assegni alla funzione
aaronman

1
Una necessità comune è quella di comprimere intervalli di dimensioni diverse o anche con intervalli infiniti.
Xeo

18

std :: transform può farlo banalmente:

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;

Se la seconda sequenza è più breve, la mia implementazione sembra fornire valori inizializzati predefiniti.


1
Se la seconda sequenza è più breve, mi aspetterei che questa sia UB come faresti iterando dalla fine di b.
Adrian

16

Puoi usare una soluzione basata su boost::zip_iterator. Crea una classe contenitore fasulla che mantenga i riferimenti ai tuoi contenitori e che restituisca zip_iteratordalle funzioni membro begine end. Adesso puoi scrivere

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

Esempio di implementazione (si prega di testare):

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

Lascio al lettore la versione variadica come ottimo esercizio.


3
+1: Boost. La gamma dovrebbe probabilmente includerla. In effetti, lascerò loro una richiesta di funzionalità su questo.
Nicol Bolas

2
@NicolBolas: fai bene. Questo dovrebbe essere abbastanza facile da implementare con boost::iterator_range+ boost::zip_iterator, anche la versione variadica.
Alexandre C.

1
Credo che questo non finirà mai (e avrà un comportamento indefinito) se gli intervalli non sono della stessa lunghezza.
Jonathan Wakely

1
boost::zip_iteratornon funziona con gamme di lunghezze diverse
Jonathan Wakely

1
Questo dovrebbe funzionare anche in c ++ 03 pulito con coppia invece di tupla. Tuttavia questo creerà problemi anche quando le lunghezze non sono uguali. Si potrebbe fare qualcosa con end () prendendo la corrispondente fine () del contenitore più piccolo. Questo sembra essere nelle specifiche come nella domanda dei PO.
Paul

15

Vedi <redi/zip.h>per una zipfunzione che funziona con range-base fore accetta un numero qualsiasi di intervalli, che possono essere rvalues ​​o lvalues ​​e possono essere di diverse lunghezze (l'iterazione si fermerà alla fine dell'intervallo più breve).

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>() << ' ';

Stampe 0 1 2 3 4 5


2
è anche possibile utilizzare boost/tuple/tuple_io.hpppercout << i;
kirill_igum

Questo è ciò che ha funzionato per me. Tuttavia, nel mio codice ho dovuto usare l'equivalente di boost::get<0>(i)e boost::get<1>(i). Non sono sicuro del motivo per cui l'esempio originale non possa essere adattato direttamente, potrebbe avere a che fare con il fatto che il mio codice prende riferimenti costanti ai contenitori.
YitzikC

11

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

Il risultato:

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


@ einpoklum-reinstateMonica ora lo è!
yuyoyuppe

6

Mi sono imbattuto in questa stessa domanda in modo indipendente e non mi è piaciuta la sintassi di nessuno dei precedenti. Quindi, ho un breve file di intestazione che essenzialmente fa la stessa cosa di boost zip_iterator ma ha alcune macro per rendere la sintassi più appetibile per me:

https://github.com/cshelton/zipfor

Ad esempio puoi fare

vector<int> a {1,2,3};
array<string,3> b {"hello","there","coders"};

zipfor(i,s eachin a,b)
    cout << i << " => " << s << endl;

Lo zucchero sintattico principale è che posso nominare gli elementi di ogni contenitore. Includo anche un "mapfor" che fa lo stesso, ma per le mappe (per nominare ".first" e ".second" dell'elemento).


Questo è pulito! Può richiedere un numero arbitrario di argomenti sono tutti quelli limitati dal tuo modello intelligente a un numero finito?
Agganciato il

Attualmente gestisce solo fino a 9 container paralleli. Sarebbe semplice avanzare. Mentre le macro variadiche consentono a una singola macro "zipfor" di gestire un numero diverso di parametri, è comunque necessario codificare una macro separata per ciascuna (a cui inviare). Vedere groups.google.com/forum/?fromgroups=#!topic/comp.std.c/... e stackoverflow.com/questions/15847837/...
cshelton

Gestisce bene argomenti di dimensioni diverse? (come descritto nell'OP)
coyotte508

@ coyotte508, presuppone che il primo contenitore abbia il minor numero di elementi (e ignora gli elementi extra in altri contenitori). Sarebbe facile modificare per non fare questo presupposto, ma ciò lo rallenterebbe (attualmente non è più lento di quanto scritto a mano) quando il numero di elementi corrisponde.
cshelton

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

6

Se ti piace il sovraccarico dell'operatore, ecco tre possibilità. I primi due utilizzano std::pair<>e std::tuple<>, rispettivamente, come iteratori; il terzo estende questo a basato sulla gamma for. Nota che non a tutti piaceranno queste definizioni degli operatori, quindi è meglio tenerle in uno spazio dei nomi separato e avere un using namespacenelle funzioni (non nei file!) Dove vorresti usarle.

#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

Per una libreria di elaborazione del flusso C ++ che sto scrivendo cercavo una soluzione che non si basasse su librerie di terze parti e funzionasse con un numero arbitrario di contenitori. Ho finito con questa soluzione. È simile alla soluzione accettata che utilizza boost (e si traduce anche in un comportamento indefinito se le lunghezze del contenitore non sono uguali)

#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
link interrotto ... sarebbe utile se il post mostra come usarlo, ad esempio main ()?
javaLover

@javaLover: puoi usarlo allo stesso modo dei cppitertools nella risposta di @ knedlsepp. Una differenza notevole è che con la soluzione precedente non è possibile modificare i contenitori sottostanti poiché il operator*for seq::iteratorrestituisce a std::tupledei riferimenti const.
winnetou

2

Se hai un compilatore conforme a C ++ 14 (ad esempio gcc5) puoi usare quello zipfornito nella cppitertoolslibreria da Ryan Haining, che sembra davvero promettente:

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 ha zip_iteratorche puoi usare (esempio nei documenti). Non funzionerà con range per, ma puoi usare std::for_eache un lambda.


Perché non funziona con il range-based per? Combinalo con Boost.Range e dovresti essere impostato.
Xeo

@Xeo: non conosco molto bene Range. Immagino che potresti coinvolgere un po 'di boilerplate e farlo funzionare, ma IMO semplicemente usarlo for_eachsarebbe meno seccante.
Cat Plus Plus

È qualcosa di dire come questo non è senza problemi: 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

2
Dovrei avviare una campagna Lambda Does Not Make std :: for_each. :)
UncleBens

2
@Xeo: questa dovrebbe probabilmente essere una domanda a parte, ma perché oh perché ??
UncleBens

-2

Ecco una versione semplice che non richiede boost. Non sarà particolarmente efficiente in quanto crea valori temporanei e non generalizza su contenitori diversi dalle liste, ma non ha dipendenze e risolve il caso più comune di zippare.

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

Sebbene le altre versioni siano più flessibili, spesso lo scopo di utilizzare un operatore di elenco è creare una semplice riga. Questa versione ha il vantaggio che il caso comune è semplice.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.