Wofür wird die bereichsbasierte Verwendung von C ++ 11 richtig verwendet?


211

Was ist die richtige Verwendung von C ++ 11 for?

Welche Syntax sollte verwendet werden? for (auto elem : container)oder for (auto& elem : container)oder for (const auto& elem : container)? Oder irgendein anderes?


6
Es gilt die gleiche Überlegung wie für Funktionsargumente.
Maxim Egorushkin

3
Eigentlich hat das wenig mit bereichsbasiert zu tun. Das gleiche kann von jedem gesagt werden auto (const)(&) x = <expr>;.
Matthieu M.

2
@MatthieuM: Das hat natürlich viel mit Range-Based zu tun! Stellen Sie sich einen Anfänger vor, der mehrere Syntaxen sieht und nicht auswählen kann, welches Formular verwendet werden soll. Der Sinn von "Q & A" bestand darin, zu versuchen, etwas Licht ins Dunkel zu bringen und die Unterschiede einiger Fälle zu erklären (und Fälle zu diskutieren, die gut kompiliert werden können, aber aufgrund nutzloser Tiefenkopien usw. ineffizient sind).
Mr.C64

2
@ Mr.C64: Für mich hat dies autoim Allgemeinen mehr zu tun als mit bereichsbasiert für; Sie können Range-Based perfekt ohne verwenden auto! for (int i: v) {}ist vollkommen in Ordnung. Natürlich haben die meisten Punkte, die Sie in Ihrer Antwort ansprechen, mehr mit dem Typ als mit auto... zu tun, aber aus der Frage ist nicht ersichtlich, wo der Schmerzpunkt liegt. Persönlich würde ich versuchen, autoaus der Frage zu entfernen ; oder machen Sie es vielleicht explizit, dass sich autodie Frage auf Wert / Referenz konzentriert , unabhängig davon, ob Sie den Typ verwenden oder explizit benennen.
Matthieu M.

1
@MatthieuM.: Ich bin offen dafür, den Titel zu ändern oder die Frage in einer Form zu bearbeiten, die sie möglicherweise klarer macht ... Auch hier lag mein Fokus darauf, verschiedene Optionen für bereichsbasierte Syntaxen zu diskutieren (Code zu zeigen, der kompiliert wird, aber ist Ineffizienter Code, der nicht kompiliert werden kann usw.) und der Versuch, jemandem (insbesondere Anfängern), der sich dem C ++ 11-Bereich für Schleifen nähert, eine Anleitung zu geben.
Mr.C64

Antworten:


389

Lassen Sie uns zwischen der Beobachtung der Elemente im Container und der Änderung an Ort und Stelle unterscheiden.

Die Elemente beobachten

Betrachten wir ein einfaches Beispiel:

vector<int> v = {1, 3, 5, 7, 9};

for (auto x : v)
    cout << x << ' ';

Der obige Code druckt die Elemente intin vector:

1 3 5 7 9

Betrachten Sie nun einen anderen Fall, in dem die Vektorelemente nicht nur einfache Ganzzahlen sind, sondern Instanzen einer komplexeren Klasse mit einem benutzerdefinierten Kopierkonstruktor usw.

// A sample test class, with custom copy semantics.
class X
{
public:
    X() 
        : m_data(0) 
    {}

    X(int data)
        : m_data(data)
    {}

    ~X() 
    {}

    X(const X& other) 
        : m_data(other.m_data)
    { cout << "X copy ctor.\n"; }

    X& operator=(const X& other)
    {
        m_data = other.m_data;       
        cout << "X copy assign.\n";
        return *this;
    }

    int Get() const
    {
        return m_data;
    }

private:
    int m_data;
};

ostream& operator<<(ostream& os, const X& x)
{
    os << x.Get();
    return os;
}

Wenn wir die obige for (auto x : v) {...}Syntax für diese neue Klasse verwenden:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (auto x : v)
{
    cout << x << ' ';
}

Die Ausgabe ist so etwas wie:

[... copy constructor calls for vector<X> initialization ...]

Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9

Da es aus der Ausgabe gelesen werden kann, werden Kopierkonstruktoraufrufe während bereichsbasierter Schleifeniterationen ausgeführt.
Dies liegt daran, dass wir die Elemente aus dem Container nach Wert (dem Teil in ) erfassen .auto xfor (auto x : v)

Dies ist ineffizienter Code, z. B. wenn diese Elemente Instanzen von sind std::string, können Heap-Speicherzuweisungen mit teuren Fahrten zum Speichermanager usw. durchgeführt werden. Dies ist nutzlos, wenn wir nur die Elemente in einem Container beobachten möchten .

Es steht also eine bessere Syntax zur Verfügung: Erfassung durch constReferenz , dh const auto&:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (const auto& x : v)
{ 
    cout << x << ' ';
}

Jetzt ist die Ausgabe:

 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9

Ohne einen falschen (und möglicherweise teuren) Aufruf des Kopierkonstruktors.

Also, wenn die Beobachtung Elemente in einem Behälter (dh für Nur - Lese-Zugriff), die folgende Syntax ist in Ordnung für eine einfache billige-to-Copy - Typen, wie int, doubleusw .:

for (auto elem : container) 

Andernfalls ist die Erfassung per constReferenz im allgemeinen Fall besser , um nutzlose (und möglicherweise teure) Aufrufe von Kopierkonstruktoren zu vermeiden:

for (const auto& elem : container) 

Ändern der Elemente im Container

Wenn wir die Elemente in einem Container bereichsbasiert ändern möchten for, sind die obigen for (auto elem : container)und die for (const auto& elem : container)Syntax falsch.

Im ersteren Fall wird tatsächlich elemeine Kopie des ursprünglichen Elements gespeichert, sodass Änderungen daran einfach verloren gehen und nicht dauerhaft im Container gespeichert werden, z.

vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)  // <-- capture by value (copy)
    x *= 10;      // <-- a local temporary copy ("x") is modified,
                  //     *not* the original vector element.

for (auto x : v)
    cout << x << ' ';

Die Ausgabe ist nur die Anfangssequenz:

1 3 5 7 9

Stattdessen kann ein Versuch der Verwendung for (const auto& x : v)einfach nicht kompiliert werden.

g ++ gibt eine Fehlermeldung wie folgt aus:

TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
          x *= 10;
            ^

Der richtige Ansatz in diesem Fall ist die Erfassung durch Nichtreferenz const:

vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
    x *= 10;

for (auto x : v)
    cout << x << ' ';

Die Ausgabe ist (wie erwartet):

10 30 50 70 90

Diese for (auto& elem : container)Syntax funktioniert auch für komplexere Typen, zB unter Berücksichtigung ein vector<string>:

vector<string> v = {"Bob", "Jeff", "Connie"};

// Modify elements in place: use "auto &"
for (auto& x : v)
    x = "Hi " + x + "!";

// Output elements (*observing* --> use "const auto&")
for (const auto& x : v)
    cout << x << ' ';

Die Ausgabe ist:

Hi Bob! Hi Jeff! Hi Connie!

Der Sonderfall der Proxy-Iteratoren

Angenommen, wir haben a vector<bool>und möchten den logischen booleschen Zustand seiner Elemente mithilfe der obigen Syntax invertieren:

vector<bool> v = {true, false, false, true};
for (auto& x : v)
    x = !x;

Der obige Code kann nicht kompiliert werden.

g ++ gibt eine ähnliche Fehlermeldung aus:

TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
 type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'
     for (auto& x : v)
                    ^

Das Problem ist , dass std::vectorVorlage spezialisiert für bool, mit einer Implementierung , die Packungen , die bools zu optimieren Raum (jeder Booleschen Wert in einem Bit gespeichert wird, acht „boolean“ Bits in einem Byte).

Aus diesem Grund (da es nicht möglich ist, einen Verweis auf ein einzelnes Bit zurückzugeben) vector<bool>wird ein sogenanntes "Proxy-Iterator" -Muster verwendet. Ein "Proxy-Iterator" ist ein Iterator, der bei Dereferenzierung kein gewöhnliches Objektbool & liefert, sondern (nach Wert) ein temporäres Objekt zurückgibt , in das eine Proxy-Klasse konvertierbar istbool . (Siehe auch diese Frage und die zugehörigen Antworten hier auf StackOverflow.)

Um die Elemente von an Ort und Stelle zu ändern vector<bool>, muss eine neue Art von Syntax (using auto&&) verwendet werden:

for (auto&& x : v)
    x = !x;

Der folgende Code funktioniert einwandfrei:

vector<bool> v = {true, false, false, true};

// Invert boolean status
for (auto&& x : v)  // <-- note use of "auto&&" for proxy iterators
    x = !x;

// Print new element values
cout << boolalpha;        
for (const auto& x : v)
    cout << x << ' ';

und Ausgänge:

false true true false

Beachten Sie, dass die for (auto&& elem : container)Syntax auch in anderen Fällen von normalen (Nicht-Proxy-) Iteratoren funktioniert (z. B. für a vector<int>oder a vector<string>).

(Als Randnotiz for (const auto& elem : container)funktioniert die oben erwähnte "beobachtende" Syntax von auch für den Fall des Proxy-Iterators einwandfrei.)

Zusammenfassung

Die obige Diskussion kann in den folgenden Richtlinien zusammengefasst werden:

  1. Verwenden Sie zur Beobachtung der Elemente die folgende Syntax:

    for (const auto& elem : container)    // capture by const reference
    • Wenn die Objekte billig zu kopieren sind (wie ints, doubles usw.), kann ein leicht vereinfachtes Formular verwendet werden:

      for (auto elem : container)    // capture by value
  2. Verwenden Sie zum Ändern der vorhandenen Elemente:

    for (auto& elem : container)    // capture by (non-const) reference
    • Wenn der Container "Proxy-Iteratoren" (wie std::vector<bool>) verwendet, verwenden Sie:

      for (auto&& elem : container)    // capture by &&

Wenn eine lokale Kopie des Elements innerhalb des Schleifenkörpers erstellt werden muss, ist die Erfassung mit value ( for (auto elem : container)) natürlich eine gute Wahl.


Zusätzliche Hinweise zum generischen Code

Da wir im generischen Code keine Annahmen darüber treffen können, dass der generische Typ Tbillig zu kopieren ist, ist es im Beobachtungsmodus sicher, ihn immer zu verwenden for (const auto& elem : container).
(Dies löst keine potenziell teuren nutzlosen Kopien aus, funktioniert auch gut für billig zu kopierende Typen wie intund auch für Container, die Proxy-Iteratoren wie verwenden std::vector<bool>.)

Darüber hinaus ist im Änderungsmodus die beste Option , wenn generischer Code auch bei Proxy-Iteratoren funktionieren soll for (auto&& elem : container).
(Dies funktioniert auch gut für Container, die normale Nicht-Proxy-Iteratoren wie std::vector<int>oder verwenden std::vector<string>.)

Im generischen Code können daher die folgenden Richtlinien bereitgestellt werden:

  1. Verwenden Sie zur Beobachtung der Elemente:

    for (const auto& elem : container)
  2. Verwenden Sie zum Ändern der vorhandenen Elemente:

    for (auto&& elem : container)

7
Kein Rat für generische Kontexte? :(
R. Martinho Fernandes

11
Warum nicht immer verwenden auto&&? Gibt es eine const auto&&?
Martin Ba

1
Ich denke, Sie vermissen den Fall, dass Sie tatsächlich eine Kopie innerhalb der Schleife benötigen?
Juanchopanza

6
"Wenn der Container" Proxy-Iteratoren "verwendet" - und Sie wissen, dass er "Proxy-Iteratoren" verwendet (was im generischen Code möglicherweise nicht der Fall ist). Ich denke, das Beste ist in der Tat auto&&, da es auto&gleich gut abdeckt .
Christian Rau

5
Vielen Dank, das war eine wirklich großartige "Crash-Kurs-Einführung" in die Syntax von und einige Tipps für den Bereich, der für einen C # -Programmierer basiert. +1.
AndrewJacksonZA

17

Es gibt keine richtige Art und Weise zu verwenden for (auto elem : container), oder for (auto& elem : container)oder for (const auto& elem : container). Sie drücken einfach aus, was Sie wollen.

Lassen Sie mich darauf näher eingehen. Machen wir einen Spaziergang.

for (auto elem : container) ...

Dieser ist syntaktischer Zucker für:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Observe that this is a copy by value.
    auto elem = *it;

}

Sie können dieses verwenden, wenn Ihr Container Elemente enthält, die billig zu kopieren sind.

for (auto& elem : container) ...

Dieser ist syntaktischer Zucker für:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Now you're directly modifying the elements
    // because elem is an lvalue reference
    auto& elem = *it;

}

Verwenden Sie diese Option, wenn Sie beispielsweise direkt in die Elemente im Container schreiben möchten.

for (const auto& elem : container) ...

Dieser ist syntaktischer Zucker für:

for(auto it = container.begin(); it != container.end(); ++it) {

    // You just want to read stuff, no modification
    const auto& elem = *it;

}

Wie der Kommentar sagt, nur zum Lesen. Und das war's auch schon, alles ist "richtig", wenn es richtig verwendet wird.


2
Ich wollte eine Anleitung geben, mit Beispielcodes, die kompiliert werden (aber ineffizient sind) oder die nicht kompiliert werden konnten, und erklären, warum und versuchen, einige Lösungen vorzuschlagen.
Mr.C64

2
@ Mr.C64 Oh, tut mir leid - ich habe gerade bemerkt, dass dies eine dieser FAQ-Fragen ist. Ich bin neu auf dieser Seite. Entschuldigung! Ihre Antwort ist großartig, ich habe sie positiv bewertet - wollte aber auch eine präzisere Version für diejenigen bereitstellen, die das Wesentliche wissen wollen . Hoffentlich dringe ich nicht ein.

1
@ Mr.C64 Was ist das Problem mit OP, das auch die Frage beantwortet? Es ist nur eine andere, gültige Antwort.
Mfontanini

1
@mfontanini: Es ist absolut kein Problem, wenn jemand eine Antwort veröffentlicht, sogar besser als meine. Der letzte Zweck besteht darin, der Community einen qualitativ hochwertigen Beitrag zu leisten (insbesondere für Anfänger, die sich vor unterschiedlichen Syntaxen und Optionen, die C ++ bietet, verloren fühlen).
Mr.C64

4

Das richtige Mittel ist immer

for(auto&& elem : container)

Dies garantiert die Beibehaltung aller Semantik.


6
Aber was ist, wenn der Container nur veränderbare Referenzen zurückgibt und ich klarstellen möchte, dass ich sie nicht in der Schleife ändern möchte? Sollte ich dann nicht verwenden auto const &, um meine Absicht klar zu machen?
RedX

@ RedX: Was ist eine "modifizierbare Referenz"?
Leichtigkeitsrennen im Orbit

2
@ RedX: Referenzen sind niemals constund niemals veränderbar. Wie auch immer, meine Antwort an Sie lautet ja, würde ich .
Leichtigkeitsrennen im Orbit

4
Obwohl dies funktionieren mag, halte ich dies für einen schlechten Rat im Vergleich zu dem differenzierteren und überlegteren Ansatz, den Mr.C64 in seiner hervorragenden und umfassenden Antwort oben gegeben hat. Das Reduzieren auf den kleinsten gemeinsamen Nenner ist nicht das, wofür C ++ gedacht ist.
Jack Aidley

6
Dieser Vorschlag zur Sprachentwicklung stimmt mit dieser "schlechten" Antwort überein
Luc Hermitte

1

Während die anfängliche Motivation der Range-for-Schleife darin bestand, die Elemente eines Containers leicht zu durchlaufen, ist die Syntax generisch genug, um auch für Objekte nützlich zu sein, die keine reinen Container sind.

Die syntaktische Anforderung für die for-Schleife ist diese range_expressionUnterstützung begin()und end()als eine der beiden Funktionen - entweder als Elementfunktionen des Typs, für den sie ausgewertet wird, oder als Nichtmitgliedsfunktionen, die eine Instanz des Typs benötigen.

Als ein erfundenes Beispiel kann man einen Bereich von Zahlen erzeugen und mit der folgenden Klasse über den Bereich iterieren.

struct Range
{
   struct Iterator
   {
      Iterator(int v, int s) : val(v), step(s) {}

      int operator*() const
      {
         return val;
      }

      Iterator& operator++()
      {
         val += step;
         return *this;
      }

      bool operator!=(Iterator const& rhs) const
      {
         return (this->val < rhs.val);
      }

      int val;
      int step;
   };

   Range(int l, int h, int s=1) : low(l), high(h), step(s) {}

   Iterator begin() const
   {
      return Iterator(low, step);
   }

   Iterator end() const
   {
      return Iterator(high, 1);
   }

   int low, high, step;
}; 

Mit der folgenden mainFunktion:

#include <iostream>

int main()
{
   Range r1(1, 10);
   for ( auto item : r1 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r2(1, 20, 2);
   for ( auto item : r2 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r3(1, 20, 3);
   for ( auto item : r3 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;
}

man würde die folgende Ausgabe bekommen.

1 2 3 4 5 6 7 8 9 
1 3 5 7 9 11 13 15 17 19 
1 4 7 10 13 16 19 
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.