Was ist der beste Weg, um zwei oder mehr Container gleichzeitig zu durchlaufen?

113

C ++ 11 bietet mehrere Möglichkeiten zum Durchlaufen von Containern. Beispielsweise:

Bereichsbasierte Schleife

for(auto c : container) fun(c)

std :: for_each

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

Was ist jedoch die empfohlene Methode, um zwei (oder mehr) Container derselben Größe zu durchlaufen, um Folgendes zu erreichen:

for(unsigned i = 0; i < containerA.size(); ++i) {
  containerA[i] = containerB[i];
}
memecs
quelle
1
Was ist mit transformGegenwart in #include <algorithm>?
Ankit Acharya
Informationen zur Zuweisungsschleife: Wenn beide Vektoren oder ähnliche sind, verwenden Sie containerA = containerB;anstelle der Schleife.
Emlai
Eine ähnliche Frage: stackoverflow.com/questions/8511035/…
knedlsepp
1
Mögliches Duplikat der Sequence-Zip-Funktion für c ++ 11?
underscore_d

Antworten:

52

Eher zu spät zur Party. Aber: Ich würde über Indizes iterieren. Aber nicht mit der klassischen forSchleife, sondern mit einer bereichsbasierten forSchleife über den Indizes:

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

indicesist eine einfache Wrapper-Funktion, die einen (träge ausgewerteten) Bereich für die Indizes zurückgibt. Da die Implementierung - obwohl einfach - etwas zu lang ist, um sie hier zu veröffentlichen, finden Sie eine Implementierung auf GitHub .

Dieser Code ist so effizient wie die Verwendung einer manuellen, klassischen forSchleife.

Wenn dieses Muster in Ihren Daten häufig vorkommt, sollten Sie ein anderes Muster verwenden, das aus zipzwei Sequenzen besteht und eine Reihe von Tupeln erzeugt, die den gepaarten Elementen entsprechen:

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

Die Implementierung von zipbleibt als Übung für den Leser, folgt aber leicht aus der Implementierung von indices.

(Vor C ++ 17 müssten Sie stattdessen Folgendes schreiben :)

for (auto items&& : zip(containerA, containerB))
    get<0>(items) = get<1>(items);
Konrad Rudolph
quelle
2
Gibt es einen Vorteil Ihrer Indeximplementierung gegenüber Boost Counting_Range? Man könnte einfach benutzenboost::counting_range(size_t(0), containerA.size())
SebastianK
3
@SebastianK Der größte Unterschied in diesem Fall ist die Syntax: Meine ist (ich behaupte) objektiv besser in diesem Fall zu verwenden. Darüber hinaus können Sie eine Schrittgröße angeben. Beispiele finden Sie auf der verknüpften Github-Seite und insbesondere in der README-Datei.
Konrad Rudolph
Ihre Idee ist sehr schön und ich habe die Verwendung von count_range erst gefunden, nachdem ich sie gesehen habe: clear upvote :) Ich frage mich jedoch, ob sie zusätzlichen Wert bietet, um dies (erneut) umzusetzen. ZB in Bezug auf die Leistung. Natürlich stimme ich einer schöneren Syntax zu, aber es würde ausreichen, eine einfache Generatorfunktion zu schreiben, um diesen Nachteil auszugleichen.
SebastianK
@SebastianK Ich gebe zu, dass ich es beim Schreiben des Codes für einfach genug hielt, isoliert zu leben, ohne eine Bibliothek zu benutzen (und das ist es!). Jetzt würde ich es wahrscheinlich als Wrapper um Boost.Range schreiben. Trotzdem ist die Leistung meiner Bibliothek bereits optimal. Damit meine ich, dass die Verwendung meiner indicesImplementierung eine Compiler-Ausgabe liefert, die mit der Verwendung manueller forSchleifen identisch ist . Es gibt überhaupt keinen Overhead.
Konrad Rudolph
Da ich sowieso Boost benutze, wäre es in meinem Fall einfacher. Ich habe diesen Wrapper bereits um den Boost-Bereich geschrieben: Eine Funktion mit einer Codezeile ist alles, was ich brauche. Es würde mich jedoch interessieren, ob die Leistung der Boost-Bereiche ebenfalls optimal ist.
SebastianK
38

Verwenden Sie für Ihr spezielles Beispiel einfach

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

Für den allgemeineren Fall können Sie Boost.Iterator zip_iteratormit einer kleinen Funktion verwenden, um es in bereichsbasierten for-Schleifen verwendbar zu machen. In den meisten Fällen funktioniert dies:

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

Live-Beispiel.

Für eine ausgewachsene Großzügigkeit möchten Sie jedoch wahrscheinlich mehr davon , die für Arrays und benutzerdefinierte Typen korrekt funktionieren , die nicht Mitglied haben begin()/ end()aber sie haben begin/ endFunktionen in ihrem Namensraum. Auf diese Weise kann der Benutzer auch constüber die zip_c...Funktionen gezielt darauf zugreifen .

Und wenn Sie wie ich ein Verfechter netter Fehlermeldungen sind, dann möchten Sie wahrscheinlich diese , die prüft, ob temporäre Container an eine der zip_...Funktionen übergeben wurden, und wenn ja, eine nette Fehlermeldung ausgibt.

Xeo
quelle
1
Vielen Dank! Eine Frage: Warum verwenden Sie Auto &&, was bedeutet das &&?
Memecs
@memecs: Ich empfehle, diese Frage sowie meine Antwort durchzulesen, die irgendwie erklärt, wie der Abzug und das Zusammenbrechen von Referenzen erfolgen. Beachten Sie, dass dies autogenauso funktioniert wie ein Vorlagenparameter. T&&In einer Vorlage befindet sich eine universelle Referenz, wie im ersten Link erläutert. Sie auto&& v = 42wird also als abgeleitet int&&und auto&& w = v;dann als abgeleitet int&. Sie können sowohl l-Werte als auch r-Werte abgleichen und beide veränderbar machen, ohne eine Kopie zu erstellen.
Xeo
@Xeo: Aber was ist der Vorteil von auto && gegenüber auto & in einer foreach-Schleife?
Viktor Sehr
@ViktorSehr: Es ermöglicht Ihnen, an temporäre Elemente zu binden, wie die von zip_range.
Xeo
23
@Xeo Alle Links zu den Beispielen sind fehlerhaft.
Kynan
34

Ich frage mich, warum niemand dies erwähnt hat:

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: Wenn die Containergrößen nicht übereinstimmen, müssen Sie den Code in die if-Anweisungen einfügen.

Joseph
quelle
9

Es gibt viele Möglichkeiten, bestimmte Dinge mit mehreren Containern zu tun, wie im algorithmHeader angegeben. In dem Beispiel, das Sie gegeben haben, könnten Sie beispielsweise verwendenstd::copy anstelle einer expliziten for-Schleife verwenden.

Auf der anderen Seite gibt es keine integrierte Möglichkeit, mehrere Container generisch zu iterieren, außer einer normalen for-Schleife. Dies ist nicht überraschend, da es viele Möglichkeiten zum Iterieren gibt. Denken Sie darüber nach: Sie könnten einen Container mit einem Schritt durchlaufen, einen Container mit einem anderen Schritt; oder durch einen Behälter bis zum Ende und dann mit dem Einsetzen beginnen, während Sie bis zum Ende des anderen Behälters gehen; oder ein Schritt des ersten Containers für jedes Mal, wenn Sie den anderen Container vollständig durchlaufen und dann von vorne beginnen; oder ein anderes Muster; oder mehr als zwei Behälter gleichzeitig; etc ...

Wenn Sie jedoch Ihre eigene Funktion "for_each" erstellen möchten, die zwei Container nur bis zur Länge des kürzesten durchläuft , können Sie Folgendes tun:

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

Natürlich können Sie auf ähnliche Weise jede Art von Iterationsstrategie erstellen, die Sie möchten.

Natürlich könnten Sie argumentieren, dass es einfacher ist, die innere for-Schleife direkt auszuführen, als eine benutzerdefinierte Funktion wie diese zu schreiben ... und Sie hätten Recht, wenn Sie dies nur ein oder zwei Mal tun würden. Aber das Schöne ist, dass dies sehr wiederverwendbar ist. =)

wjl
quelle
Es scheint, als müssten Sie die Iteratoren vor der Schleife deklarieren? Ich habe es versucht: for (Container1::iterator i1 = c1.begin(), Container2::iterator i2 = c2.begin(); (i1 != end1) && (i2 != end2); ++it1, ++i2)aber der Compiler schreit. Kann jemand erklären, warum dies ungültig ist?
David Doria
@DavidDoria Der erste Teil der for-Schleife ist eine einzelne Anweisung. Sie können nicht zwei Variablen unterschiedlichen Typs in derselben Anweisung deklarieren. Überlegen Sie, warum das for (int x = 0, y = 0; ...funktioniert, aber for (int x = 0, double y = 0; ...)nicht.
wjl
1
.. Sie können jedoch std :: pair <Container1 :: iterator, Container2 :: iterator> its = {c1.begin (), c2.begin ()} haben;
Lorro
1
Eine andere Sache zu beachten ist, dass dies mit C ++ 14 leicht variadic gemacht werden könntetypename...
wjl
8

Für den Fall, dass Sie nur über 2 Container gleichzeitig iterieren müssen, gibt es eine erweiterte Version des Standardalgorithmus for_each in der Boost-Bereichsbibliothek, z.

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

Wenn Sie mehr als 2 Container in einem Algorithmus verarbeiten müssen, müssen Sie mit zip spielen.

Zaren
quelle
Wunderbar! Wie gefiel dir? Scheint, als wäre es nirgendwo dokumentiert.
Mikhail
4

Eine andere Lösung könnte darin bestehen, eine Referenz des Iterators des anderen Containers in einem Lambda zu erfassen und einen Post-Inkrement-Operator zu verwenden. Zum Beispiel wäre eine einfache Kopie:

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

In Lambda können Sie alles tun itaund es dann erhöhen. Dies erstreckt sich leicht auf den Fall mehrerer Behälter.

Vahid
quelle
3

Eine Bereichsbibliothek bietet diese und andere sehr hilfreiche Funktionen. Im folgenden Beispiel wird Boost.Range verwendet . Eric Nieblers rangev3 sollte eine gute Alternative sein.

#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 macht dies mit strukturierten Bindungen noch besser:

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
quelle
Dieses Programm wird nicht mit g ++ 4.8.0 kompiliert. 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
Nachdem std :: tie in boost: tie geändert wurde, wurde es kompiliert.
Syam
Ich erhalte den folgenden Kompilierungsfehler für die Version mit strukturierter Bindung (unter Verwendung von MSVC 19.13.26132.0und Windows SDK-Version 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
strukturierte Bindungen scheinen nicht zu funktionieren mit boost::combine: stackoverflow.com/q/55585723/8414561
Dev Null
2

Ich bin auch ein bisschen spät dran; Sie können dies jedoch verwenden (Variadic-Funktion im C-Stil):

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

oder dies (unter Verwendung eines Funktionsparameterpakets):

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

oder dies (unter Verwendung einer in Klammern eingeschlossenen Initialisierungsliste):

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

oder Sie können Vektoren wie hier verbinden: Wie können zwei Vektoren am besten verkettet werden? und dann über großen Vektor iterieren.

Szymon Marczak
quelle
0

Hier ist eine 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;
        }
    }

Anwendungsbeispiel

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
quelle