web-dev-qa-db-fra.com

Quelle est la meilleure façon de parcourir plusieurs conteneurs simultanément

C++ 11 offre plusieurs méthodes pour parcourir les 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 méthode recommandée pour effectuer une itération sur deux conteneurs (ou plus) de la même taille afin de réaliser quelque chose comme:

for(unsigned i = 0; i < containerA.size(); ++i) {
  containerA[i] = containerB[i];
}
83
memecs

Plutôt tard pour la fête. Mais: je voudrais itérer sur les indices. Mais pas avec la boucle for classique, mais avec une boucle for basée sur une plage et sur les index:

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

indices est une fonction d'encapsulation simple qui renvoie une plage (évaluée paresseuse) pour les index. Puisque la mise en œuvre - bien que simple - est un peu trop longue pour la poster ici, vous pouvez trouver une implémentation sur GitHub .

Ce code est aussi efficace que si vous utilisiez une boucle manuelle for classique.

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

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

L'implémentation de Zip reste un exercice pour le lecteur, mais elle découle facilement de l'implémentation de indices.

(Avant C++ 17, vous deviez écrire le texte suivant :)

for (auto items&& : Zip(containerA, containerB))
    get<0>(items) = get<1>(items);
41
Konrad Rudolph

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 le Zip_iterator de Boost.Iterator, avec une petite fonction pour le rendre utilisable dans les 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é complète, vous voulez probablement quelque chose de plus semblable à this , qui fonctionnera correctement pour les tableaux et les types définis par l'utilisateur qui n'ont pas de membre begin()end() mais _/do ont begin/end fonctions dans leur espace de noms. Cela permettra également à l'utilisateur d'obtenir spécifiquement l'accès const à travers les fonctions Zip_c....

Et si vous êtes un partisan des messages d'erreur Nice, comme moi, vous voudrez probablement this , qui vérifie si des conteneurs temporaires ont été passés à l'une des fonctions Zip_... et affiche un message d'erreur Nice, le cas échéant.

37
Xeo

je me demande pourquoi personne ne l'a mentionné:

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 de conteneur ne correspondent pas, vous devrez alors insérer le code dans les instructions if.

26
Joseph

Il y a beaucoup de façons de faire des choses spécifiques avec plusieurs conteneurs, comme indiqué dans l'en-tête algorithm. Par exemple, dans l'exemple que vous avez donné, vous pouvez utiliser std::copy au lieu d'une boucle for explicite.

En revanche, il n’existe pas de méthode intégrée permettant d’itérer de manière générique plusieurs conteneurs autres qu’une boucle for normale. Cela n’est pas surprenant, car il existe beaucoup de façons d’itérer. Pensez-y: vous pouvez parcourir un conteneur en une étape, un conteneur en une autre; ou à travers un conteneur jusqu'à la fin puis commencez à insérer pendant que vous allez à la fin de l'autre conteneur; ou une étape du premier conteneur à chaque fois que vous traversez complètement l'autre conteneur puis recommencez; ou un autre motif; ou plus de deux conteneurs à la fois; etc ...

Toutefois, si vous souhaitez créer votre fonction de style own "for_each" qui itère dans deux conteneurs uniquement jusqu'à la longueur du plus court, vous pouvez procéder de la manière suivante:

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

De toute évidence, vous pouvez créer n'importe quelle stratégie d'itérations de la même manière.

Bien sûr, vous pouvez faire valoir qu'il est plus facile de faire la boucle for interne directement 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 cela est très réutilisable. =) 

9
wjl

Dans le cas où vous devez itérer simultanément sur 2 conteneurs uniquement, il existe une version étendue de l'algorithme standard for_each dans la bibliothèque de plage amplifiée, 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 algorithme, vous devez jouer avec Zip.

7
czarles

Une bibliothèque de gammes fournit ceci et d'autres fonctionnalités très utiles. L'exemple suivant utilise Boost.Range . 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 le rendra encore meilleur avec les 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;
}
2
Jens

une autre solution pourrait consister à capturer une référence de l'itérateur de l'autre conteneur dans un lambda et à utiliser l'opérateur post increment sur celui-ci. 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++; })

dans lambda, vous pouvez faire n'importe quoi avec ita et l’incrémenter. Cela s’étend facilement au cas de conteneurs multiples.

2
Vahid

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

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'initialisation 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 rejoindre des vecteurs comme ici: Quel est le meilleur moyen de concaténer deux vecteurs? et ensuite itérer sur un gros vecteur.

0
Szymon Marczak