end-Iterator decrement definiert?



  • Hallo

    Darf man folgendes machen:

    unit_test_case(test_end_iterator_decrement)
    {
        const set<int> a_set { 1, 2, 3 };
    
        auto it = a_set.end();
        --it;
    
        unit_test_point().is_true(a_set.end() != it,
                                  3 == *it);
    }
    

    Es funktioniert (lasst euch nicht von diesem unit_test_bla verunsichern, da kann ich nichts dafür ;)), sieht aber irgendwie etwas grenzwertig aus. Es geht ausschliesslich um std::set.

    Ich teste das gerade, weil ich noch einen anderen Grenzfall habe mit upper_bound/lower_bound, aber dazu komme ich wohl erst später.

    FG



  • Ohne jetzt irgendwo nachgelesen zu haben, würde ich mal sagen:
    Das Ziel von Iteratoren in C++ ist ja, so ähnlich wie möglich den normalen Zeigern zu sein.
    Von daher würde ich sagen, dass du einen end-Iterator dekrementieren darfst, solange, bis begin erreicht ist.



  • Jain. Prinzipiell geht das, aber wenn dein container leer ist dann natürlich nicht. Schließlich gilt dann begin() == end() und end()-1 wäre dasselbe wie begin()-1



  • Lauf Reference gibt std::set bidirektionale Iteratoren raus. D.h. du kannst end() dekrementieren.



  • kleiner Troll schrieb:

    Jain. Prinzipiell geht das, aber wenn dein container leer ist dann natürlich nicht. Schließlich gilt dann begin() == end() und end()-1 wäre dasselbe wie begin()-1

    Das ist ein sehr guter Punkt, den ich gleich noch getestet habe. Da stürzt einem das Dach auf den Kopf 😉



  • Übrigens gibts in C++11 die Funktionen std::prev() und std::next() . Ist manchmal ganz nützlich für temporäre Objekte...


  • Mod

    Nexus schrieb:

    Übrigens gibts in C++11 die Funktionen std::prev() und std::next() . Ist manchmal ganz nützlich für temporäre Objekte...

    Ja, aber höchstens, wenn man mehr als einmal dekrementieren will.



  • Auch wenns nur einmal ist. --end() muss nicht unbedingt funktionieren.


  • Mod

    Nexus schrieb:

    Auch wenns nur einmal ist. --end() muss nicht unbedingt funktionieren.

    Und std::prev schon, oder was?



  • Genau. Wenn Iteratoren Zeiger sind (was bei std::vector theoretisch erlaubt ist), kannst du den skalaren RValue v.end() nicht verändern, aber sehr wohl ein anderes Objekt zurückgeben.

    Davon abgesehen, dass man manchmal den ursprünglichen Iterator gar nicht verändern will, z.B. in einer verschachtelten Schleife mit allen möglichen Element-Paaren:

    for (auto first = v.begin(); first != v.end(); ++first)
    {
        for (auto second = std::next(first); second != v.end(); ++second)
        {
            doSomething(*first, *second);
        }
    }
    

  • Mod

    Davon abgesehen, dass man manchmal den ursprünglichen Iterator gar nicht verändern will, z.B. in einer verschachtelten Schleife mit allen möglichen Element-Paaren:

    Ich verstehe.

    Allerdings kann man dieses temporäre Objekt gleich selbst in die Hand nehmen

    for (auto first = begin(v); first != end(v); ++first) // Hier sollte die Abbruchbedingung wahrscheinlich eher first != --end(v) lauten
    	for( auto second = first; ++second != end(v); )
    		doSomething( *first, *second );
    

    Das ist mMn. deutlich eleganter.



  • Arcoth schrieb:

    Das ist mMn. deutlich eleganter.

    Ich weiss nicht was du unter "elegant" verstehst, aber ich brauche für Abweichungen vom Standard-Iterationsschema

    for (auto itr = begin; itr != end; ++itr)
    

    jeweils länger, um den Code zu verstehen.

    Gerade ++second != end(v); und das Verzichten auf den Update-Teil machen für mich vor allem den Eindruck, dass jemand "clever" sein will. Das entspricht nicht meinem Verständnis von gutem Code, geschweige denn Eleganz.



  • Arcoth schrieb:

    Davon abgesehen, dass man manchmal den ursprünglichen Iterator gar nicht verändern will, z.B. in einer verschachtelten Schleife mit allen möglichen Element-Paaren:

    Ich verstehe.

    Allerdings kann man dieses temporäre Objekt gleich selbst in die Hand nehmen

    for (auto first = begin(v); first != end(v); ++first) // Hier sollte die Abbruchbedingung wahrscheinlich eher first != --end(v) lauten
    	for( auto second = first; ++second != end(v); )
    		doSomething( *first, *second );
    

    Das ist mMn. deutlich eleganter.

    Aua.


  • Mod

    Jetzt sagt mir nicht, dass das einzige Argument für dein AUA die Abweichung vom Standarditerationsschema ist.

    ~Edit: Grammatikalischen Fehler korrigiert, nicht dass das Volkard auch weh tut...~



  • Sag mal, seit ihr noch nicht in C++11 angekomen?

    for (auto first : range(begin(v), end(v)))
      for (auto second : range(next(first), end(v)))
        doSomething(*first, *second);
    


  • Arcoth, wenn du schon eine einfache Lösung ablehnst und mit dem "Argument" der Eleganz auf eine unnötig komplizierte hinweist, liegt es an dir, eine Begründung dazu zu liefern. Ansonsten bleibe ich bei der "clever"-Theorie.

    c++11-coder, für den Fall, dass du deinen Post tatsächlich ernst gemeint hast: Davon abgesehen, dass du range() auch zuerst schreiben musst, funktioniert das ohnehin nicht, da first und second keine Iteratoren, sondern Kopien der Elemente sind.



  • Nexus schrieb:

    c++11-coder, für den Fall, dass du deinen Post tatsächlich ernst gemeint hast: Davon abgesehen, dass du range() auch zuerst schreiben musst, funktioniert das ohnehin nicht, da first und second keine Iteratoren, sondern Kopien der Elemente sind.

    Muss ich jetzt auch noch erklären, wie range funktioniert 🙄

    for (int i : range(0,10))
      std::cout << i << '\n';
    // gibt "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n" aus
    

    Wie kann man ohne dieses range nur leben?


  • Mod

    unnötig komplizierte hinweist

    Was genau ist daran kompliziert?

    Vergleiche folgende Methoden, über ein Intervall zu iterieren:

    for( unsigned i = 1; i != 20; ++i )
        /* ... */;
    
    unsigned i = 0;
    while( ++i != 20 )
        /* ... */;
    

    Was mir auffällt ist folgendes: Ersteres ist leichter zu lesen. Man braucht ein Sekündchen für das !=, was auch ein <= oder ein < sein könnte, aber im Prinzip erkennt man schnell, dass über [1, 20) iteriert wird.
    Das zweite ist zwar nicht komplizierter (wenn nicht einfacher) in der Syntax, aber da es dem gewöhnlichen Schema for( int-Deklarieren ; Vergleich; Inkrement )
    nicht folgt, braucht man noch zwei Sekündchen um zu kapieren, worüber iteriert wird.

    Jetzt vergleiche mal

    // (1)
    unsigned i = computeGamma( i / 2 ) * pi;
    while( ++i != 20 )
        /* ... */
    
    // oder (2)
    
    unsigned i = computeGamma( i / 2 ) * pi + 1;
    while( i != 20 )
    {
        /* ... */
        ++i;
    }
    
    // oder (3)
    unsigned i = computeGamma( i / 2 ) * pi + 1;
    for(; i != 20; ++i )
        /* ... */
    
    // oder (4)
    
    for( unsigned i = computeGamma( i / 2 ) * pi + 1; i != 20; ++i )
    

    Ich würd' sagen, (3) sieht gut aus, bei (4) kommen Erinnerungen an Werner hoch (buchstäblich), bei (2) nervt das Extra-Statement (ist eigentlich kein Kandidat) und bei (1) siehts auch ganz gut aus. Nur stört mich bei (3), dass die + 1 an den Initializer der Variable drangehängt wird, bei (1) kommt das +1 in dem Intervall deutlicher zu Tage.

    Weil std::next( second ) schon ein ganz klein wenig lang ist, tendiert es, obiger Argumentation zu folgen. Daher finde ich die while -Version nicht schlimmer als die for -Version, längere Statements in der Initialisierung nerven bei for .

    Edit: Wenn ich noch mal drüber schaue, habe ich Mist gelabert. Menno.



  • Du vergisst

    // oder (5)
    
    for(auto i : range<unsigned>(computeGamma(i/2)*pi + 1, 20))
      /* ... */;
    


  • Ihr diskutiert wirklich, wie man über ein Range iteriert?
    Ich mein, die Antwort ist doch klar! Der einzige wahre Weg ist:

    void iterate(iterator begin, iterator end)
    {
      (do_sth(*begin), begin != end) && (iterate(++begin, end), true);
    }
    

    Ich bitte euch!


Anmelden zum Antworten