Finden Sie die Position des Elements in C ++ 11 Range-based for-Schleife?


79

Angenommen, ich habe den folgenden Code:

vector<int> list;
for(auto& elem:list) {
    int i = elem;
}

Kann ich die Position von elemim Vektor finden, ohne einen separaten Iterator zu verwalten?


17
Das ist nicht das, wofür Range-Based ist (heh, ist das ein Wortspiel?)
jrok

2
Dies ist in STL-Containern nur möglich, wenn std::findeine andere Overkill-Funktion verwendet wird. Sie können Iteratoren nicht aus enthaltenen Elementen schließen. Warum nicht einen Iterator warten?
Eitan T

2
Aus zwei Gründen. Das erste ist alles, was ich (in diesem Fall) tun möchte, um zu sehen, ob ich am letzten Element bin oder nicht :) und das zweite ist, dass der Compiler eines verwalten muss. Warum kann ich nicht darauf zugreifen? "this" ist eine Variable, deren Gültigkeitsbereich vom Compiler verwaltet wird. Warum nicht hier? Oder stellen Sie eine alternative (aber dennoch praktische) Syntax bereit, die wie Javascript eine Variable einrichtet, die sich ändert, wenn Sie die Schleife durchlaufen. für (Auto & Index: Liste)
Fred Finkle

1
@FredFinkle Sie haben tatsächlich Recht, es gibt einen Iterator , aber wenn Sie eine bereichsbasierte forSchleife verwenden, handelt es sich um einen compilerinternen Namen, der daher nicht in Ihrem Code verwendet werden kann. Wenn Sie also wirklich wissen möchten, ob Sie sich am letzten Element befinden, sollten Sie die for(;;)Schleife verwenden.
iFreilicht

Antworten:


62

Ja du kannst, es braucht nur ein bisschen Massage;)

Der Trick besteht darin, die Komposition zu verwenden: Anstatt den Container direkt zu durchlaufen, "komprimieren" Sie ihn mit einem Index auf dem Weg.

Spezialisierte Postleitzahl:

template <typename T>
struct iterator_extractor { typedef typename T::iterator type; };

template <typename T>
struct iterator_extractor<T const> { typedef typename T::const_iterator type; };


template <typename T>
class Indexer {
public:
    class iterator {
        typedef typename iterator_extractor<T>::type inner_iterator;

        typedef typename std::iterator_traits<inner_iterator>::reference inner_reference;
    public:
        typedef std::pair<size_t, inner_reference> reference;

        iterator(inner_iterator it): _pos(0), _it(it) {}

        reference operator*() const { return reference(_pos, *_it); }

        iterator& operator++() { ++_pos; ++_it; return *this; }
        iterator operator++(int) { iterator tmp(*this); ++*this; return tmp; }

        bool operator==(iterator const& it) const { return _it == it._it; }
        bool operator!=(iterator const& it) const { return !(*this == it); }

    private:
        size_t _pos;
        inner_iterator _it;
    };

    Indexer(T& t): _container(t) {}

    iterator begin() const { return iterator(_container.begin()); }
    iterator end() const { return iterator(_container.end()); }

private:
    T& _container;
}; // class Indexer

template <typename T>
Indexer<T> index(T& t) { return Indexer<T>(t); }

Und damit:

#include <iostream>
#include <iterator>
#include <limits>
#include <vector>

// Zipper code here

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (auto p: index(v)) {
        std::cout << p.first << ": " << p.second << "\n";
    }
}

Sie können es auf ideone sehen , obwohl es die Unterstützung für for-range-Schleifen fehlt, so dass es weniger hübsch ist.

BEARBEITEN:

Ich habe gerade daran gedacht, dass ich Boost.Range öfter überprüfen sollte. Leider keine zipReichweite, aber ich habe einen Perl gefunden : boost::adaptors::indexed. Es erfordert jedoch Zugriff auf den Iterator, um den Index abzurufen. Schade: x

Ansonsten mit dem counting_rangeund einem Generikum zipbin ich sicher, dass es möglich sein könnte, etwas Interessantes zu tun ...

In der idealen Welt würde ich mir vorstellen:

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (auto tuple: zip(iota(0), v)) {
        std::cout << tuple.at<0>() << ": " << tuple.at<1>() << "\n";
    }
}

Durch das zipautomatische Erstellen einer Ansicht als eine Reihe von Referenztupeln und das iota(0)einfache Erstellen eines "falschen" Bereichs, der von der 0Unendlichkeit (oder dem Maximum seines Typs ...) ausgeht und nur bis unendlich zählt.


1
Dies bietet eine Fülle nützlicher Informationen. Vielen Dank. Ich werde mit dem Code herumspielen. Wie oben erwähnt, ist der "Index" -Code genau das, was die Sprache bereitstellen soll.
Fred Finkle

3
Wie wäre es mit counting_range(oder boost::counting_iterator) + boost::zip_iterator?
ildjarn

@ildjarn: Ja, Boost.Iterators hat die Bausteine ​​(wie es scheint), aber es gibt keinen entsprechenden Bereich, was ärgerlich ist.
Matthieu M.

Beachten Sie, dass Sie möglicherweise ändern Indexerkönnen, um rvalue-Argumente auch ordnungsgemäß zu akzeptieren und zu halten, indem Sie den Typ von _containerin einen Werttyp ändern, wenn das ursprüngliche Argument ein rvalue ist und std::move/ oder std::forwarddas Argument in.
Xeo

Ich habe den Code so bearbeitet, dass er hoffentlich mit rvalues ​​funktioniert. Leider kann ich den Code nicht vollständig testen, aber er sollte funktionieren. Eine Erklärung finden Sie hier . Wenn es Ihnen nicht gefällt oder Sie Fehler oder ähnliches finden, können Sie jederzeit einen Rollback durchführen. :)
Xeo

30

jrok ist richtig: bereichsbasierte for-Schleifen sind nicht für diesen Zweck ausgelegt.

In Ihrem Fall ist es jedoch möglich, es mithilfe der Zeigerarithmetik zu berechnen, da vectorseine Elemente zusammenhängend gespeichert werden (*).

vector<int> list;
for(auto& elem:list) { 
    int i = elem;
    int pos = &elem-&list[0]; // pos contains the position in the vector 

    // also a &-operator overload proof alternative (thanks to ildjarn) :
    // int pos = addressof(elem)-addressof(list[0]); 

}

Dies ist jedoch eindeutig eine schlechte Vorgehensweise, da der Code dadurch verschleiert und anfälliger wird (er kann leicht beschädigt werden, wenn jemand den Containertyp ändert, den &Operator überlastet oder "auto &" durch "auto" ersetzt. Viel Glück beim Debuggen!).

HINWEIS: Die Kontiguität ist für Vektoren in C ++ 03 und für Arrays und Zeichenfolgen in C ++ 11-Standard garantiert.


6
Ja, es ist in der Norm angegeben. Die Kontiguität ist vectorin C ++ 03 arrayund stringin C ++ 11 garantiert .
Nicol Bolas

1
Es leicht bricht , wenn jemand ... Überlastung der &Betreiber “ Das ist , was std::addressoffür ist. : -]
ildjarn

Du hast recht. Die & -Überlastungssichere Version wäre also: int pos = addressof (elem) - addressof (list [0]); .... Matthieu Ms Iterator Wrapper ist viel besser :)
Frédéric Terrazzoni

Wusste nicht, dass die Nähe garantiert ist. Ich würde es hier nicht benutzen wollen, aber gut zu wissen.
Fred Finkle

5
Warum nicht std :: distance verwenden, um die Position herauszufinden?
Michael van der Westhuizen

19

Nein, das kannst du nicht (zumindest nicht ohne Anstrengung). Wenn Sie die Position eines Elements benötigen, sollten Sie nicht bereichsbasiert für verwenden. Denken Sie daran, dass dies nur ein praktisches Tool für den häufigsten Fall ist: Führen Sie für jedes Element einen Code aus. In den selteneren Fällen, in denen Sie die Position des Elements benötigen, müssen Sie die weniger bequeme reguläre forSchleife verwenden.


15

Basierend auf der Antwort von @Matthieu gibt es eine sehr elegante Lösung mit den genannten boost :: adapters :: indexed :

std::vector<std::string> strings{10, "Hello"};
int main(){
    strings[5] = "World";
    for(auto const& el: strings| boost::adaptors::indexed(0))
      std::cout << el.index() << ": " << el.value() << std::endl;
}

Du kannst es versuchen

Dies funktioniert ziemlich ähnlich wie die erwähnte "ideale Weltlösung", hat eine hübsche Syntax und ist prägnant. Beachten Sie, dass der Typ elin diesem Fall ungefähr so ​​ist boost::foobar<const std::string&, int>, dass er die Referenz dort verarbeitet und kein Kopieren durchgeführt wird. Es ist sogar unglaublich effizient: https://godbolt.org/g/e4LMnJ (Der Code entspricht der Beibehaltung einer eigenen Zählervariablen, die so gut wie möglich ist)

Der Vollständigkeit halber die Alternativen:

size_t i = 0;
for(auto const& el: strings) {
  std::cout << i << ": " << el << std::endl;
  ++i;
}

Oder verwenden Sie die zusammenhängende Eigenschaft eines Vektors:

for(auto const& el: strings) {
  size_t i = &el - &strings.front();
  std::cout << i << ": " << el << std::endl;
}

Der erste generiert den gleichen Code wie die Boost-Adapter-Version (optimal) und der letzte ist 1 Anweisung länger: https://godbolt.org/g/nEG8f9

Hinweis: Wenn Sie nur wissen möchten, ob Sie das letzte Element haben, können Sie Folgendes verwenden:

for(auto const& el: strings) {
  bool isLast = &el == &strings.back();
  std::cout << isLast << ": " << el << std::endl;
}

Dies funktioniert für jeden Standard - Container aber auto&/ auto const&muss verwendet werden (wie oben) , aber das ist auf jeden Fall zu empfehlen. Abhängig von der Eingabe kann dies auch ziemlich schnell sein (insbesondere wenn der Compiler die Größe Ihres Vektors kennt).

Ersetzen Sie das &fooby std::addressof(foo), um auf der sicheren Seite für generischen Code zu sein.


Das ist in der Tat elegant!
Matthieu M.

Ich habe die 2 Alternativen mit Godbolt-Vergleich des generierten Codes der Vollständigkeit halber hinzugefügt und auch die Notwendigkeit des OP (in den Kommentaren) zur Erkennung des letzten Elements
angesprochen

10

Wenn Sie einen Compiler mit C ++ 14-Unterstützung haben, können Sie dies in einem funktionalen Stil tun:

#include <iostream>
#include <string>
#include <vector>
#include <functional>

template<typename T>
void for_enum(T& container, std::function<void(int, typename T::value_type&)> op)
{
    int idx = 0;
    for(auto& value : container)
        op(idx++, value);
}

int main()
{
    std::vector<std::string> sv {"hi", "there"};
    for_enum(sv, [](auto i, auto v) {
        std::cout << i << " " << v << std::endl;
    });
}

Funktioniert mit clang 3.4 und gcc 4.9 (nicht mit 4.8); für beide muss eingestellt werden -std=c++1y. Der Grund, warum Sie c ++ 14 benötigen, sind die autoParameter in der Lambda-Funktion.


2
std::functionverwendet Typ Löschung, die teuer ist. Warum nicht verwenden, template<typename T, typename Callable> void for_enum(T& container, Callable op)damit Sie nicht für das Löschen des Typs bezahlen müssen?
NathanOliver


4

Wenn Sie darauf bestehen, einen auf dem Index basierenden Bereich zu verwenden und den Index zu kennen, ist es ziemlich trivial, den Index wie unten gezeigt beizubehalten. Ich glaube nicht, dass es eine sauberere / einfachere Lösung für die Reichweite von Schleifen gibt. Aber warum nicht einen Standard für (;;) verwenden? Das würde wahrscheinlich Ihre Absicht und Ihren Code am klarsten machen.

vector<int> list;
int idx = 0;
for(auto& elem:list) {
    int i = elem;
    //TODO whatever made you want the idx
    ++idx;
}

1
(idx entspricht "Verwalten eines separaten Iterators")
user66081

2

Ich habe aus Ihren Kommentaren gelesen, dass ein Grund, warum Sie den Index kennen wollen, darin besteht, zu wissen, ob das Element das erste / letzte in der Sequenz ist. Wenn ja, können Sie tun

for(auto& elem:list) {
//  loop code ...
    if(&elem == &*std::begin(list)){ ... special code for first element ... }
    if(&elem == &*std::prev(std::end(list))){ ... special code for last element ... }
//  if(&elem == &*std::rbegin(list)){... (C++14 only) special code for last element ...}
//  loop code ... 
}

BEARBEITEN: Hiermit wird beispielsweise ein Container gedruckt, der ein Trennzeichen im letzten Element überspringt. Funktioniert für die meisten Container, die ich mir vorstellen kann (einschließlich Arrays) (Online-Demo http://coliru.stacked-crooked.com/a/9bdce059abd87f91 ):

#include <iostream>
#include <vector>
#include <list>
#include <set>
using namespace std;

template<class Container>
void print(Container const& c){
  for(auto& x:c){
    std::cout << x; 
    if(&x != &*std::prev(std::end(c))) std::cout << ", "; // special code for last element
  }
  std::cout << std::endl;
}

int main() {
  std::vector<double> v{1.,2.,3.};
  print(v); // prints 1,2,3
  std::list<double> l{1.,2.,3.};
  print(l); // prints 1,2,3
  std::initializer_list<double> i{1.,2.,3.};
  print(i); // prints 1,2,3
  std::set<double> s{1.,2.,3.};
  print(s); // print 1,2,3
  double a[3] = {1.,2.,3.}; // works for C-arrays as well
  print(a); // print 1,2,3
}

Bitte beachten Sie (vor ungerechtfertigtem Downvoting), dass der Autor der Frage dies im Zusammenhang mit der Erkennung des letzten Elements in einer for-range-Schleife für einen Container stellt. Dafür sehe ich keinen Grund, warum zu vergleichen &elemund &*std::prev(std::end(list))wird nicht funktionieren oder praktisch sein. Ich stimme der anderen Antwort zu, dass ein iteratorbasiertes for dafür besser geeignet ist, aber immer noch.
alfC

Es scheint einfacher zu sein, int i=c.size();vor der Schleife zu deklarieren und zu testen if(--i==0).
Marc Glisse

@MarcGlisse, der int iCode war nur ein Beispiel. Ich werde es entfernen, um Verwirrung zu vermeiden. Auch wenn Sie sizevor der Schleife verwenden, benötigen Sie einen Zähler.
AlfC

2

Tobias Widlund hat einen schönen MIT-lizenzierten Python-Header geschrieben, der nur aufgezählt wird (allerdings C ++ 17):

GitHub

Blogeintrag

Wirklich schön zu bedienen:

std::vector<int> my_vector {1,3,3,7};

for(auto [i, my_element] : en::enumerate(my_vector))
{
    // do stuff
}

1

Hier ist eine makrobasierte Lösung, die wahrscheinlich die meisten anderen in Bezug auf Einfachheit, Kompilierungszeit und Qualität der Codegenerierung übertrifft:

#include <iostream>

#define fori(i, ...) if(size_t i = -1) for(__VA_ARGS__) if(i++, true)

int main() {
    fori(i, auto const & x : {"hello", "world", "!"}) {
        std::cout << i << " " << x << std::endl;
    }
}

Ergebnis:

$ g++ -o enumerate enumerate.cpp -std=c++11 && ./enumerate 
0 hello
1 world
2 !

1

Wenn Sie vermeiden möchten, dass eine Hilfsfunktion geschrieben werden muss, während sich die Indexvariable lokal in der Schleife befindet, können Sie ein Lambda mit einer veränderlichen Variablen verwenden:

int main() {
    std::vector<char> values = {'a', 'b', 'c'};
    std::for_each(begin(values), end(values), [i = size_t{}] (auto x) mutable {
        std::cout << i << ' ' << x << '\n';
        ++i;
    });
}

0

Hier ist eine sehr schöne Lösung mit c ++ 20:

#include <array>
#include <iostream>
#include <ranges>

template<typename T>
struct EnumeratedElement {
    std::size_t index;
    T& element;
};

auto enumerate(std::ranges::range auto& range) 
    -> std::ranges::view auto 
{
    return range | std::views::transform(
        [i = std::size_t{}](auto& element) mutable {
            return EnumeratedElement{i++, element};
        }
    );
}

auto main() -> int {
    auto const elements = std::array{3, 1, 4, 1, 5, 9, 2};
    for (auto const [index, element] : enumerate(elements)) {
        std::cout << "Element " << index << ": " << element << '\n';
    }
}

Die hier verwendeten Hauptfunktionen sind C ++ 20-Bereiche, C ++ 20-Konzepte, C ++ 11-veränderbare Lambdas, C ++ 14-Lambda-Capture-Initialisierer und C ++ 17-strukturierte Bindungen. Informationen zu diesen Themen finden Sie unter cppreference.com.

Beachten Sie, dass es sich elementbei der strukturierten Bindung tatsächlich um eine Referenz und nicht um eine Kopie des Elements handelt (nicht, dass es hier darauf ankommt). Dies liegt daran, dass alle Qualifizierer in der Umgebung autonur ein temporäres Objekt betreffen, aus dem die Felder extrahiert werden, und nicht die Felder selbst.

Der generierte Code ist identisch mit dem von diesem generierten Code (zumindest von gcc 10.2):

#include <array>
#include <iostream>
#include <ranges>

auto main() -> int {
    auto const elements = std::array{3, 1, 4, 1, 5, 9, 2};
    for (auto index = std::size_t{}; auto& element : elements) {
        std::cout << "Element " << index << ": " << element << '\n';
        index++;
    }
}

Beweis: https://godbolt.org/z/a5bfxz

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.