Fonction Sequence-zip pour C ++ 11?


99

Avec la nouvelle boucle for basée sur la plage, nous pouvons écrire du code comme

for(auto x: Y) {}

Quelle IMO est une énorme amélioration par rapport (par exemple)

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

Peut-il être utilisé pour boucler sur deux boucles simultanées, comme la zipfonction Pythons ? Pour ceux qui ne connaissent pas Python, le code:

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

Donne comme sortie (1,4) (2,5) (3,6)


Basée sur la plage forne peut être utilisée qu'avec une seule variable, donc non. Si vous voulez accéder à deux valeurs à la fois, vous devez utiliser quelque chose commestd::pair
Seth Carnegie

4
@SethCarnegie: pas directement, mais vous pourriez trouver une zip()fonction qui renvoie des tuples et itérer sur la liste des tuples.
André Caron

2
@ AndréCaron vous avez raison, mon "non" voulait dire que vous ne pouvez pas utiliser deux variables, pas que vous ne pouvez pas parcourir deux conteneurs à la fois.
Seth Carnegie

Il for(;;)est clair que ce comportement peut être obtenu, quoique de longue haleine, la question est donc vraiment la suivante: est-il possible de "auto" sur deux objets à la fois?

Dans une future révision (espérons-le C ++ 17), une refonte de la STL inclura des plages . Ensuite, view :: zip peut fournir la solution préférée.
John McFarlane

Réponses:


88

Attention: boost::zip_iterator et à boost::combinepartir de Boost 1.63.0 (26 décembre 2016) entraînera un comportement indéfini si la longueur des conteneurs d'entrée n'est pas la même (il peut planter ou itérer au-delà de la fin).


À partir de Boost 1.56.0 (7 août 2014), vous pouvez utiliserboost::combine (la fonction existe dans les versions antérieures mais non documentée):

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

Cela imprimerait

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

Dans les versions antérieures, vous pouviez définir vous-même une plage comme ceci:

#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'utilisation est la même.


1
pouvez-vous l'utiliser pour le tri? ie std :: sort (zip (a.begin (), ...), zip (a.end (), ...), [] (tup a, tup b) {a.get <0> () > b.get <0> ()}); ?
gnzlbg


Je serais tenté par des optionaléléments pour des possibilités d'itération au-delà de la fin ...
Yakk - Adam Nevraumont

3
Avez-vous une chance de le faire avec std :: make_tuple et std :: tie? J'essayais de l'utiliser tout en minimisant la dépendance au boost mais je ne pouvais pas le faire fonctionner.
Carneiro du

@kennytm une idée de la raison pour laquelle ils ont décidé d'opter pour UB au lieu de simplement terminer à la fin de la plage la plus courte du groupe?
Catskul

18

J'ai donc écrit ce zip avant quand je m'ennuyais, j'ai décidé de le poster car il est différent des autres en ce qu'il n'utilise pas boost et ressemble plus au 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;
    }

Exemple d'utilisation:

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
Vous devriez vérifier si l' un des itérateurs est à la fin.
Xeo

1
@Xeo, toutes les plages doivent être de la même taille que la première ou plus
aaronman

Pouvez-vous expliquer comment [](int i,int j,float k,float l)fonctionne? Est-ce une fonction lambda?
Accroché le

@Hooked ouais c'est un lambda, cela fonctionne essentiellement std::for_eachmais vous pouvez utiliser un nombre arbitraire de plages, les paramètres du lambda dépendent du nombre d'itérateurs que vous donnez à la fonction
aaronman

1
Un besoin courant est de compresser des plages de tailles différentes, voire avec des plages infinies.
Xeo

16

Vous pouvez utiliser une solution basée sur boost::zip_iterator. Créez une classe de conteneur bidon en conservant les références à vos conteneurs et qui reviennent zip_iteratordes fonctions membres beginet end. Maintenant tu peux écrire

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

Exemple d'implémentation (veuillez tester):

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

Je laisse la version variadic comme un excellent exercice au lecteur.


3
+1: Boost.Range devrait probablement l'intégrer. En fait, je vais leur déposer une demande de fonctionnalité à ce sujet.
Nicol Bolas

2
@NicolBolas: Vous faites bien. Cela devrait être assez facile à implémenter avec boost::iterator_range+ boost::zip_iterator, même la version variadique.
Alexandre C.

1
Je crois que cela ne se terminera jamais (et aura un comportement indéfini) si les plages ne sont pas de la même longueur.
Jonathan Wakely

1
boost::zip_iteratorne fonctionne pas avec des plages de longueurs différentes
Jonathan Wakely

1
Cela devrait également fonctionner même en c ++ 03 propre avec paire au lieu de tuple. Pourtant, cela créera également des problèmes lorsque les longueurs ne sont pas égales. Quelque chose peut être fait avec la fin () en prenant la fin correspondante () du plus petit conteneur. Cela semble être dans la spécification comme dans la question des OP.
Paul

16

std :: transform peut faire cela de manière triviale:

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;

Si la deuxième séquence est plus courte, mon implémentation semble donner des valeurs initialisées par défaut.


1
Si la deuxième séquence est plus courte, je m'attendrais à ce que ce soit UB comme vous le feriez à la fin de b.
Adrian

15

Voir <redi/zip.h>pour une zipfonction qui fonctionne avec range-base foret accepte n'importe quel nombre de plages, qui peuvent être rvalues ​​ou lvalues ​​et peuvent être de différentes longueurs (l'itération s'arrêtera à la fin de la plage la plus courte).

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

Tirages 0 1 2 3 4 5


2
vous pouvez également utiliser boost/tuple/tuple_io.hpppourcout << i;
kirill_igum

C'est ce qui a fonctionné pour moi. Cependant, dans mon code, j'ai dû utiliser l'équivalent de boost::get<0>(i)et boost::get<1>(i). Je ne sais pas pourquoi l'exemple d'origine n'a pas pu être adapté directement, cela pourrait être dû au fait que mon code prend des références constantes aux conteneurs.
YitzikC

11

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

Le résultat:

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


@ einpoklum-reinstateMonica maintenant ça l'est!
yuyoyuppe

6

Je suis tombé sur cette même question indépendamment et je n'ai pas aimé la syntaxe de l'un des éléments ci-dessus. Donc, j'ai un court fichier d'en-tête qui fait essentiellement la même chose que le boost zip_iterator mais qui contient quelques macros pour rendre la syntaxe plus acceptable pour moi:

https://github.com/cshelton/zipfor

Par exemple, vous pouvez faire

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

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

Le sucre syntaxique principal est que je peux nommer les éléments de chaque conteneur. J'inclus également un "mapfor" qui fait de même, mais pour les maps (pour nommer le ".first" et ".second" de l'élément).


C'est chouette! Peut-il prendre un nombre arbitraire d'arguments, tous ceux qui sont limités par votre modélisation intelligente à un nombre fini?
Accroché le

Actuellement, il ne gère que jusqu'à 9 conteneurs parallèles. Ce serait simple à avancer. Alors que les macros variadiques permettent à une seule macro «zipfor» de gérer différents nombres de paramètres, il faut encore coder une macro distincte pour chacun (à envoyer). Voir groups.google.com/forum/?fromgroups=#!topic/comp.std.c/… et stackoverflow.com/questions/15847837/…
cshelton

Gère-t-il bien les arguments de taille différente? (tel que décrit dans l'OP)
coyotte508

@ coyotte508, il suppose que le premier conteneur a le moins d'éléments (et ignore les éléments supplémentaires dans les autres conteneurs). Il serait facile de modifier pour ne pas faire cette hypothèse, mais cela la ralentirait (actuellement, ce n'est pas plus lent que l'écriture manuscrite) lorsque le nombre d'éléments correspond.
cshelton

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

6

Si vous aimez la surcharge des opérateurs, voici trois possibilités. Les deux premiers utilisent std::pair<>et std::tuple<>, respectivement, comme itérateurs; le troisième étend cela à la gamme for. Notez que tout le monde using namespacen'aimera pas ces définitions des opérateurs, il est donc préférable de les conserver dans un espace de noms séparé et d'avoir un dans les fonctions (pas les fichiers!) Où vous souhaitez les utiliser.

#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

Pour une bibliothèque de traitement de flux C ++ que j'écris, je cherchais une solution qui ne repose pas sur des bibliothèques tierces et fonctionne avec un nombre arbitraire de conteneurs. J'ai fini avec cette solution. C'est similaire à la solution acceptée qui utilise boost (et entraîne également un comportement indéfini si les longueurs de conteneur ne sont pas égales)

#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
lien cassé ... serait utile si le message montre comment l'utiliser, par exemple main ()?
javaLover

@javaLover: vous pouvez l'utiliser de la même manière que cppitertools dans la réponse de @ knedlsepp. Une différence notable est qu'avec la solution ci-dessus, vous ne pouvez pas modifier les conteneurs sous-jacents car le operator*for seq::iteratorrenvoie std::tupledes références const.
winnetou

2

Si vous avez un compilateur compatible C ++ 14 (par exemple gcc5), vous pouvez utiliser zipfourni dans la cppitertoolsbibliothèque par Ryan Haining, ce qui semble vraiment prometteur:

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 a que zip_iteratorvous pouvez utiliser (exemples dans la documentation). Cela ne fonctionnera pas avec range for, mais vous pouvez utiliser std::for_eachet un lambda.


Pourquoi ne fonctionne-t-il pas avec la gamme basée sur? Combinez-le avec Boost.Range et vous devriez être prêt.
Xeo

@Xeo: Je ne connais pas trop bien Range. Je suppose que vous pourriez impliquer un passe-partout et le faire fonctionner, mais la simple utilisation de l'OMI for_eachserait moins compliquée.
Cat Plus Plus

Vous quelque chose de méchant comme ça n'est pas sans tracas: 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
Je devrais lancer une campagne Lambda ne rend pas std :: for_each utile. :)
UncleBens

2
@Xeo: Cela devrait probablement être une question distincte, mais pourquoi oh pourquoi ??
UncleBens

-2

Voici une version simple qui ne nécessite pas de boost. Il ne sera pas particulièrement efficace car il crée des valeurs temporaires, et il ne se généralise pas sur des conteneurs autres que des listes, mais il n'a pas de dépendances et il résout le cas le plus courant de compression.

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

Bien que les autres versions soient plus flexibles, l'intérêt d'utiliser un opérateur de liste est souvent de créer une simple ligne unique. Cette version a l'avantage que le cas commun est simple.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.