Qual è il modo migliore per iterare su due o più contenitori contemporaneamente


114

C ++ 11 offre diversi modi per eseguire l'iterazione sui contenitori. Per esempio:

Loop basato su intervallo

for(auto c : container) fun(c)

std :: for_each

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

Tuttavia, qual è il modo consigliato per iterare su due (o più) contenitori della stessa dimensione per ottenere qualcosa come:

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

1
che dire del transformpresente in #include <algorithm>?
Ankit Acharya

Informazioni sul ciclo di assegnazione: se entrambi sono vettori o simili, utilizzare al containerA = containerB;posto del ciclo.
emlai


1
Possibile duplicato della funzione Sequence-zip per c ++ 11?
underscore_d

Risposte:


53

Piuttosto tardi per la festa. Ma: itererei sugli indici. Ma non con il forciclo classico ma invece con un forciclo basato su intervallo sugli indici:

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

indicesè una semplice funzione wrapper che restituisce un intervallo (valutato pigramente) per gli indici. Poiché l'implementazione, sebbene semplice, è un po 'troppo lunga per pubblicarla qui, puoi trovarne un'implementazione su GitHub .

Questo codice è efficiente quanto l'utilizzo di un classico forciclo manuale .

Se questo modello si verifica spesso nei dati, considera l'utilizzo di un altro modello che zipsia due sequenze e produca un intervallo di tuple, corrispondenti agli elementi accoppiati:

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

L'implementazione di zipè lasciata come esercizio per il lettore, ma segue facilmente dall'implementazione di indices.

(Prima di C ++ 17 dovresti invece scrivere quanto segue :)

for (auto items&& : zip(containerA, containerB))
    get<0>(items) = get<1>(items);

2
C'è qualche vantaggio nell'implementazione degli indici rispetto all'aumento di counting_range? Si potrebbe semplicemente usareboost::counting_range(size_t(0), containerA.size())
SebastianK

3
@SebastianK La più grande differenza in questo caso è la sintassi: la mia è (sostengo) oggettivamente migliore da usare in questo caso. Inoltre, puoi specificare una dimensione del passo. Vedere la pagina Github collegata, e in particolare il file README, per esempi.
Konrad Rudolph

La tua idea è molto bella e ho pensato di utilizzare counting_range solo dopo averlo visto: clear upvote :) Tuttavia, mi chiedo se fornisce un valore aggiuntivo per (ri) implementarlo. Ad esempio, per quanto riguarda le prestazioni. Sintassi più bella, d'accordo, ovviamente, ma basterebbe scrivere una semplice funzione di generatore per compensare questo inconveniente.
SebastianK

@SebastianK Ammetto che io quando ho scritto il codice l'ho ritenuto abbastanza semplice da vivere in isolamento senza usare una libreria (e lo è!). Ora probabilmente lo scriverei come un involucro attorno a Boost.Range. Detto questo, le prestazioni della mia libreria sono già ottimali. Ciò che intendo con questo è che l'utilizzo della mia indicesimplementazione produce un output del compilatore identico all'utilizzo dei forcicli manuali . Non ci sono spese generali di sorta.
Konrad Rudolph

Dal momento che utilizzo comunque il boost, nel mio caso sarebbe più semplice. Ho già scritto questo wrapper attorno all'intervallo boost: una funzione con una riga di codice è tutto ciò di cui ho bisogno. Tuttavia, sarei interessato se anche le prestazioni delle gamme di boost fossero ottimali.
SebastianK

38

Per il tuo esempio specifico, usa solo

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

Per il caso più generale, puoi usare Boost.Iterator's zip_iterator, con una piccola funzione per renderlo utilizzabile in loop per range-based. Nella maggior parte dei casi, questo funzionerà:

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

Esempio dal vivo.

Tuttavia, per genericità in piena regola, probabilmente si desidera qualcosa di più simile a questo , che funzionerà correttamente per array e tipi definiti dall'utente che non hanno membro begin()/ end()ma non hanno begin/ endfunzioni nel loro spazio dei nomi. Inoltre, ciò consentirà all'utente di ottenere l' constaccesso specifico tramite le zip_c...funzioni.

E se sei un sostenitore di messaggi di errore piacevoli, come me, allora probabilmente vuole questo , che controlla se sono stati passati ad uno qualsiasi degli eventuali contenitori temporanei zip_...funzioni, e stampa un messaggio di errore bello se così.


1
Grazie! Una domanda però, perché usi auto &&, cosa significa &&?
memecs

@memecs: Consiglio di leggere questa domanda , così come questa mia risposta che spiega come vengono eseguite le deduzioni e il collasso dei riferimenti. Si noti che autofunziona esattamente come un parametro di modello e T&&in un modello è un riferimento universale come spiegato nel primo collegamento, quindi auto&& v = 42verrà dedotto come int&&e auto&& w = v;verrà quindi dedotto come int&. Ti permette di abbinare lvalues ​​e rvalues ​​e lasciare che entrambi siano mutabili, senza fare una copia.
Xeo

@Xeo: Ma qual è il vantaggio di auto && rispetto a auto & in un ciclo foreach?
Viktor Sehr

@ViktorSehr: ti permette di legarti a elementi temporanei, come quelli prodotti da zip_range.
Xeo

23
@Xeo Tutti i collegamenti agli esempi sono interrotti.
kynan

34

mi chiedo perché nessuno lo abbia menzionato:

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: se le dimensioni del contenitore non corrispondono, dovrai inserire il codice all'interno delle istruzioni if.


9

Esistono molti modi per eseguire operazioni specifiche con più contenitori, come indicato algorithmnell'intestazione. Ad esempio, nell'esempio che hai fornito, potresti usare std::copyinvece di un ciclo for esplicito.

D'altra parte, non esiste alcun modo integrato per iterare genericamente più contenitori oltre a un normale ciclo for. Questo non è sorprendente perché ci sono molti modi per iterare. Pensaci: potresti scorrere un contenitore con un passaggio, un contenitore con un altro passaggio; oppure attraverso un contenitore fino a quando non arriva alla fine, quindi inizia a inserire mentre si passa alla fine dell'altro contenitore; o un passaggio del primo contenitore per ogni volta che si passa completamente attraverso l'altro contenitore, quindi si ricomincia da capo; o qualche altro modello; o più di due contenitori alla volta; eccetera ...

Tuttavia, se si voleva fare il vostro proprio funzione di stile "for_each" che consente di scorrere due contenitori solo fino alla lunghezza del più breve, si potrebbe fare qualcosa di simile a questo:

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

Ovviamente puoi fare qualsiasi tipo di strategia di iterazione che desideri in un modo simile.

Naturalmente, potresti sostenere che eseguire direttamente il ciclo interno for è più facile che scrivere una funzione personalizzata come questa ... e avresti ragione, se lo farai solo una o due volte. Ma la cosa bella è che questo è molto riutilizzabile. =)


Sembra che tu debba dichiarare gli iteratori prima del ciclo? Ho provato questo: for (Container1::iterator i1 = c1.begin(), Container2::iterator i2 = c2.begin(); (i1 != end1) && (i2 != end2); ++it1, ++i2)ma il compilatore urla. Qualcuno può spiegare perché questo non è valido?
David Doria

@DavidDoria La prima parte del ciclo for è una singola istruzione. Non è possibile dichiarare due variabili di tipi diversi nella stessa istruzione. Pensa al motivo per cui for (int x = 0, y = 0; ...funziona, ma for (int x = 0, double y = 0; ...)non lo fa.
wjl

1
.. puoi, tuttavia, avere std :: pair <Container1 :: iterator, Container2 :: iterator> its = {c1.begin (), c2.begin ()};
lorro

1
Un'altra cosa da notare è che questo potrebbe essere facilmente reso variadico con C ++ 14typename...
wjl

8

Nel caso in cui sia necessario iterare simultaneamente solo su 2 contenitori, esiste una versione estesa dell'algoritmo for_each standard nella libreria della gamma boost, ad esempio:

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

Quando devi gestire più di 2 contenitori in un algoritmo, devi giocare con zip.


Meraviglioso! Come l'hai trovato? Sembra che non sia documentato da nessuna parte.
Mikhail

4

un'altra soluzione potrebbe essere l'acquisizione di un riferimento dell'iteratore dell'altro contenitore in un lambda e l'utilizzo dell'operatore di incremento post su quello. per esempio una copia semplice sarebbe:

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

all'interno di lambda puoi fare qualsiasi cosa con itae poi incrementarlo. Questo si estende facilmente al caso di più contenitori.


3

Una libreria di intervalli fornisce questa e altre funzionalità molto utili. L'esempio seguente utilizza Boost.Range . Il rangev3 di Eric Niebler dovrebbe essere una buona alternativa.

#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 lo renderà ancora migliore con i collegamenti strutturati:

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

Questo programma non si compila con 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

Dopo aver cambiato std :: tie in boost: tie, è stato compilato.
Syam

Ottengo il seguente errore di compilazione per la versione con binding strutturato (utilizzando MSVC 19.13.26132.0e la versione Windows SDK 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

i collegamenti strutturati non sembrano funzionare con boost::combine: stackoverflow.com/q/55585723/8414561
Dev Null

2

Anch'io sono un po 'in ritardo; ma puoi usare questo (funzione variadica in stile 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);

o questo (usando un pacchetto di parametri di funzione):

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

o questo (usando un elenco di inizializzatori racchiuso tra parentesi graffe):

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

oppure puoi unire i vettori come qui: Qual è il modo migliore per concatenare due vettori? e quindi iterare su un vettore grande.


0

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

Utilizzo di esempio

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