Quelle est la meilleure façon d'itérer simultanément sur deux ou plusieurs conteneurs

114

C ++ 11 offre plusieurs façons d'itérer sur des conteneurs. Par exemple:

Boucle basée sur la plage

for(auto c : container) fun(c)

std :: for_each

for_each(container.begin(),container.end(),fun)

Cependant, quelle est la manière recommandée d'itérer sur deux (ou plus) conteneurs de même taille pour accomplir quelque chose comme:

for(unsigned i = 0; i < containerA.size(); ++i) {
  containerA[i] = containerB[i];
}
memecs
la source
1
qu'en est-il transformprésent dans #include <algorithm>?
Ankit Acharya
À propos de la boucle d'assignation: si les deux sont des vecteurs ou similaires, utilisez à la containerA = containerB;place de la boucle.
emlai
Une question similaire: stackoverflow.com/questions/8511035/…
knedlsepp
1
Possible duplication de la fonction Sequence-zip pour c ++ 11?
underscore_d

Réponses:

53

Plutôt tard à la fête. Mais: je voudrais itérer sur les indices. Mais pas avec la forboucle classique mais plutôt avec une forboucle basée sur une plage sur les indices:

for(unsigned i : indices(containerA)) {
    containerA[i] = containerB[i];
}

indicesest une simple fonction wrapper qui renvoie une plage (évaluée paresseusement) pour les indices. Puisque l'implémentation - bien que simple - est un peu trop longue pour la publier ici, vous pouvez trouver une implémentation sur GitHub .

Ce code est aussi efficace que l'utilisation d'une forboucle classique manuelle .

Si ce modèle se produit souvent dans vos données, envisagez d'utiliser un autre modèle zipcomposé de deux séquences et produisant une plage de tuples, correspondant aux éléments appariés:

for (auto& [a, b] : zip(containerA, containerB)) {
    a = b;
}

La mise en œuvre de zipest laissée comme un exercice pour le lecteur, mais elle découle facilement de la mise en œuvre de indices.

(Avant C ++ 17, vous devriez plutôt écrire ce qui suit :)

for (auto items&& : zip(containerA, containerB))
    get<0>(items) = get<1>(items);
Konrad Rudolph
la source
2
La mise en œuvre de vos indices présente-t-elle un avantage par rapport à booster counting_range? On pourrait simplement utiliserboost::counting_range(size_t(0), containerA.size())
SebastianK
3
@SebastianK La plus grande différence dans ce cas est la syntaxe: la mienne est (je le prétends) objectivement meilleure à utiliser dans ce cas. En outre, vous pouvez spécifier une taille de pas. Voir la page Github liée, et en particulier le fichier README, pour des exemples.
Konrad Rudolph
Votre idée est très belle et je suis venu avec l'utilisation de counting_range seulement après l'avoir vu: clear upvote :) Cependant, je me demande si cela apporte une valeur supplémentaire pour (ré) implémenter cela. Par exemple, concernant les performances. Syntaxe plus agréable, je suis d'accord, bien sûr, mais il suffirait d'écrire une simple fonction génératrice pour compenser cet inconvénient.
SebastianK
@SebastianK J'avoue que lorsque j'ai écrit le code, je l'ai jugé assez simple pour vivre en vase clos sans utiliser de bibliothèque (et c'est le cas!). Maintenant, je l'écrirais probablement comme un wrapper autour de Boost.Range. Cela dit, les performances de ma bibliothèque sont déjà optimales. Ce que je veux dire par là, c'est que l'utilisation de mon indicesimplémentation produit une sortie de compilateur qui est identique à l'utilisation de forboucles manuelles . Il n'y a aucun frais généraux.
Konrad Rudolph
Puisque j'utilise quand même boost, ce serait plus simple dans mon cas. J'ai déjà écrit ce wrapper autour de boost range: une fonction avec une ligne de code est tout ce dont j'ai besoin. Cependant, je serais intéressé si les performances des gammes boost sont également optimales.
SebastianK
38

Pour votre exemple spécifique, utilisez simplement

std::copy_n(contB.begin(), contA.size(), contA.begin())

Pour le cas plus général, vous pouvez utiliser Boost.Iterator's zip_iterator, avec une petite fonction pour le rendre utilisable dans des boucles for basées sur la plage. Dans la plupart des cas, cela fonctionnera:

template<class... Conts>
auto zip_range(Conts&... conts)
  -> decltype(boost::make_iterator_range(
  boost::make_zip_iterator(boost::make_tuple(conts.begin()...)),
  boost::make_zip_iterator(boost::make_tuple(conts.end()...))))
{
  return {boost::make_zip_iterator(boost::make_tuple(conts.begin()...)),
          boost::make_zip_iterator(boost::make_tuple(conts.end()...))};
}

// ...
for(auto&& t : zip_range(contA, contB))
  std::cout << t.get<0>() << " : " << t.get<1>() << "\n";

Exemple en direct.

Cependant, pour une généricité à part entière, vous voulez probablement quelque chose de plus comme ça , qui fonctionnera correctement pour les tableaux et les types définis par l'utilisateur qui n'ont pas de membre begin()/ end()mais qui ont begin/ des endfonctions dans leur espace de noms. En outre, cela permettra à l'utilisateur d'accéder spécifiquement constà travers les zip_c...fonctions.

Et si vous êtes un partisan de bons messages d'erreur, comme moi, alors vous voulez probablement ceci , qui vérifie si des conteneurs temporaires ont été passés à l'une des zip_...fonctions, et affiche un joli message d'erreur si c'est le cas.

Xeo
la source
1
Merci! Une question cependant, pourquoi utilisez-vous auto &&, qu'est-ce que cela signifie &&?
memecs
@memecs: Je recommande de lire cette question , ainsi que ma réponse qui explique un peu comment la déduction et la réduction des références sont effectuées. Notez que cela autofonctionne exactement comme un paramètre de modèle, et T&&dans un modèle se trouve une référence universelle comme expliqué dans le premier lien, donc auto&& v = 42sera déduit comme int&&et auto&& w = v;sera ensuite déduit comme int&. Il vous permet de faire correspondre lvalues ​​ainsi que rvalues ​​et de laisser les deux être mutables, sans faire de copie.
Xeo
@Xeo: Mais quel est l'avantage de l'auto && sur l'auto et dans une boucle foreach?
Viktor Sehr
@ViktorSehr: Il permet de se lier à des éléments temporaires, comme ceux produits par zip_range.
Xeo
23
@Xeo Tous les liens vers les exemples sont rompus.
kynan
34

je me demande pourquoi personne n'a mentionné ceci:

auto ItA = VectorA.begin();
auto ItB = VectorB.begin();

while(ItA != VectorA.end() || ItB != VectorB.end())
{
    if(ItA != VectorA.end())
    {
        ++ItA;
    }
    if(ItB != VectorB.end())
    {
        ++ItB;
    }
}

PS: si les tailles des conteneurs ne correspondent pas, vous devrez mettre le code dans les instructions if.

Joseph
la source
9

Il existe de nombreuses façons de faire des choses spécifiques avec plusieurs conteneurs, comme indiqué dans l'en- algorithmtête. Par exemple, dans l'exemple que vous avez donné, vous pouvez utiliser à la std::copyplace d'une boucle for explicite.

D'un autre côté, il n'existe aucun moyen intégré d'itérer génériquement plusieurs conteneurs autre qu'une boucle for normale. Ce n'est pas surprenant car il existe de nombreuses façons d'itérer. Pensez-y: vous pouvez parcourir un conteneur avec une étape, un conteneur avec une autre étape; ou à travers un conteneur jusqu'à ce qu'il arrive à la fin, puis commencez à insérer pendant que vous passez à l'extrémité de l'autre conteneur; ou une étape du premier conteneur pour chaque fois que vous passez complètement par l'autre conteneur, puis recommencez; ou un autre modèle; ou plus de deux conteneurs à la fois; etc ...

Cependant, si vous souhaitez créer votre propre fonction de style "for_each" qui ne parcourt que deux conteneurs jusqu'à la longueur du plus court, vous pouvez faire quelque chose comme ceci:

template <typename Container1, typename Container2>
void custom_for_each(
  Container1 &c1,
  Container2 &c2,
  std::function<void(Container1::iterator &it1, Container2::iterator &it2)> f)
{
  Container1::iterator begin1 = c1.begin();
  Container2::iterator begin2 = c2.begin();
  Container1::iterator end1 = c1.end();
  Container2::iterator end2 = c2.end();
  Container1::iterator i1;
  Container1::iterator i2;
  for (i1 = begin1, i2 = begin2; (i1 != end1) && (i2 != end2); ++it1, ++i2) {
    f(i1, i2);
  }
}

Évidemment, vous pouvez faire n'importe quel type de stratégie d'itérations de la même manière.

Bien sûr, vous pourriez affirmer que faire directement la boucle for interne est plus facile que d'écrire une fonction personnalisée comme celle-ci ... et vous auriez raison, si vous ne le faites qu'une ou deux fois. Mais la bonne chose est que c'est très réutilisable. =)

wjl
la source
Il semble que vous deviez déclarer les itérateurs avant la boucle? J'ai essayé ceci: for (Container1::iterator i1 = c1.begin(), Container2::iterator i2 = c2.begin(); (i1 != end1) && (i2 != end2); ++it1, ++i2)mais le compilateur crie. Quelqu'un peut-il expliquer pourquoi cela est invalide?
David Doria le
@DavidDoria La première partie de la boucle for est une seule instruction. Vous ne pouvez pas déclarer deux variables de types différents dans la même instruction. Pensez à pourquoi for (int x = 0, y = 0; ...fonctionne, mais for (int x = 0, double y = 0; ...)ne fonctionne pas.
wjl
1
.. vous pouvez cependant avoir std :: pair <Container1 :: iterator, Container2 :: iterator> its = {c1.begin (), c2.begin ()};
lorro
1
Une autre chose à noter est que cela pourrait être facilement rendu variadique avec C ++ 14typename...
wjl
8

Dans le cas où vous devez itérer simultanément sur 2 conteneurs uniquement, il existe une version étendue de l'algorithme for_each standard dans la bibliothèque de plage de boost, par exemple:

#include <vector>
#include <boost/assign/list_of.hpp>
#include <boost/bind.hpp>
#include <boost/range/algorithm_ext/for_each.hpp>

void foo(int a, int& b)
{
    b = a + 1;
}

int main()
{
    std::vector<int> contA = boost::assign::list_of(4)(3)(5)(2);
    std::vector<int> contB(contA.size(), 0);

    boost::for_each(contA, contB, boost::bind(&foo, _1, _2));
    // contB will be now 5,4,6,3
    //...
    return 0;
}

Lorsque vous devez gérer plus de 2 conteneurs dans un seul algorithme, vous devez jouer avec zip.

tsarles
la source
Merveilleux! Comment avez-vous trouvé? On dirait qu'il n'est documenté nulle part.
Mikhail
4

une autre solution pourrait être de capturer une référence de l'itérateur de l'autre conteneur dans un lambda et d'utiliser l'opérateur de post-incrémentation sur cela. par exemple, une copie simple serait:

vector<double> a{1, 2, 3};
vector<double> b(3);

auto ita = a.begin();
for_each(b.begin(), b.end(), [&ita](auto &itb) { itb = *ita++; })

à l'intérieur de lambda, vous pouvez faire n'importe quoi itaet l'incrémenter. Cela s'étend facilement au cas des conteneurs multiples.

Vahid
la source
3

Une bibliothèque de plage fournit ceci et d'autres fonctionnalités très utiles. L'exemple suivant utilise Boost.Range . Le rangev3 d'Eric Niebler devrait être une bonne alternative.

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

int main(int, const char*[])
{
    std::vector<int> const v{0,1,2,3,4};
    std::list<char> const  l{'a', 'b', 'c', 'd', 'e'};

    for(auto const& i: boost::combine(v, l))
    {
        int ti;
        char tc;
        boost::tie(ti,tc) = i;
        std::cout << '(' << ti << ',' << tc << ')' << '\n';
    }

    return 0;
}

C ++ 17 rendra cela encore meilleur avec des liaisons structurées:

int main(int, const char*[])
{
    std::vector<int> const v{0,1,2,3,4};
    std::list<char> const  l{'a', 'b', 'c', 'd', 'e'};

    for(auto const& [ti, tc]: boost::combine(v, l))
    {
        std::cout << '(' << ti << ',' << tc << ')' << '\n';
    }

    return 0;
}
Jens
la source
Ce programme ne compile pas avec g ++ 4.8.0. delme.cxx:15:25: error: no match for 'operator=' (operand types are 'std::tuple<int&, char&>' and 'const boost::tuples::cons<const int&, boost::tuples::cons<const char&, boost::tuples::null_type> >') std::tie(ti,tc) = i; ^
syam
Après avoir changé std :: tie en boost: tie, il a été compilé.
syam
J'obtiens l'erreur de compilation suivante pour la version avec liaison structurée (en utilisant MSVC 19.13.26132.0et la version SDK Windows 10.0.16299.0): error C2679: binary '<<': no operator found which takes a right-hand operand of type 'const boost::tuples::cons<const char &,boost::fusion::detail::build_tuple_cons<boost::fusion::single_view_iterator<Sequence,boost::mpl::int_<1>>,Last,true>::type>' (or there is no acceptable conversion)
pooya13
les liaisons structurées ne semblent pas fonctionner avec boost::combine: stackoverflow.com/q/55585723/8414561
Dev Null
2

Je suis un peu en retard aussi; mais vous pouvez utiliser ceci (fonction variadique de style C):

template<typename T>
void foreach(std::function<void(T)> callback, int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        std::vector<T> v = va_arg(args, std::vector<T>);
        std::for_each(v.begin(), v.end(), callback);
    }

    va_end(args);
}

foreach<int>([](const int &i) {
    // do something here
}, 6, vecA, vecB, vecC, vecD, vecE, vecF);

ou ceci (en utilisant un pack de paramètres de fonction):

template<typename Func, typename T>
void foreach(Func callback, std::vector<T> &v) {
    std::for_each(v.begin(), v.end(), callback);
}

template<typename Func, typename T, typename... Args>
void foreach(Func callback, std::vector<T> &v, Args... args) {
    std::for_each(v.begin(), v.end(), callback);
    return foreach(callback, args...);
}

foreach([](const int &i){
    // do something here
}, vecA, vecB, vecC, vecD, vecE, vecF);

ou ceci (en utilisant une liste d'initialiseurs entre accolades):

template<typename Func, typename T>
void foreach(Func callback, std::initializer_list<std::vector<T>> list) {
    for (auto &vec : list) {
        std::for_each(vec.begin(), vec.end(), callback);
    }
}

foreach([](const int &i){
    // do something here
}, {vecA, vecB, vecC, vecD, vecE, vecF});

ou vous pouvez joindre des vecteurs comme ici: Quelle est la meilleure façon de concaténer deux vecteurs? puis itérer sur un gros vecteur.

Szymon Marczak
la source
0

Voici une variante

template<class ... Iterator>
void increment_dummy(Iterator ... i)
    {}

template<class Function,class ... Iterator>
void for_each_combined(size_t N,Function&& fun,Iterator... iter)
    {
    while(N!=0)
        {
        fun(*iter...);
        increment_dummy(++iter...);
        --N;
        }
    }

Exemple d'utilisation

void arrays_mix(size_t N,const float* x,const float* y,float* z)
    {
    for_each_combined(N,[](float x,float y,float& z){z=x+y;},x,y,z);    
    }
user877329
la source