web-dev-qa-db-de.com

Was ist die richtige Art, C ++ 11 bereichsbasiert einzusetzen?

Was ist der richtige Weg, um C++ 11 bereichsbasiertes for zu verwenden?

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

174
Mr.C64

Beginnen wir mit der Unterscheidung zwischen und den Elementen im Continer und und den vorhandenen Elementen.

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 (ints) im 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 benutzerdefiniertem 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 mit dieser neuen 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

Wie aus der Ausgabe gelesen werden kann, werden Aufrufe des Kopierkonstruktors während bereichsbasierter Iterationen für Schleifen ausgeführt.
Dies liegt daran, dass wir erfassen die Elemente aus dem Container nach dem Wert (the auto x Teil von for (auto x : v)).

Dies ist ineffizient Code, z. Wenn es sich bei diesen Elementen um Instanzen von std::string handelt, können Heap-Speicherzuordnungen vorgenommen werden, mit teuren Reisen zum Speichermanager usw. Dies ist nutzlos, wenn wir nur beobachten möchten = die Elemente in einem Container.

Es gibt also eine bessere Syntax: Erfassen durch const Referenz , 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 falschen (und möglicherweise teuren) Aufruf eines Kopierkonstruktors.

Wenn Sie also Elemente in einem Container beobachten (dh für einen Nur-Lese-Zugriff), ist die folgende Syntax für einfache billig) in Ordnung -to-copy Typen wie int, double usw .:

for (auto elem : container) 

Andernfalls ist das Erfassen mit der const -Referenz im allgemeinen Fall besser, um unnötige (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 mit bereichsbasiertem for modify bearbeiten möchten, werden die obigen for (auto elem : container) und for (const auto& elem : container) Syntax ist falsch.

Im ersten Fall speichert elem ein copy des ursprünglichen Elements, sodass an ihm vorgenommene Änderungen nur verloren gehen und nicht dauerhaft im Container gespeichert werden. z.B:

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, for (const auto& x : v) zu verwenden, einfach nicht kompiliert werden.

g ++ gibt eine Fehlermeldung wie diese aus:

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

Der richtige Ansatz ist in diesem Fall das Erfassen mit der Referenz nonconst:

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, z. unter Berücksichtigung eines 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 Spezialfall von Proxy-Iteratoren

Angenommen, wir haben ein 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 Fehlermeldung ähnlich der folgenden 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 die Vorlage std::vector spezialisiert für bool ist, mit einer Implementierung, die packs die bools zur Optimierung des Speicherplatzes (jeder Boolesche Wert wird in einem Bit gespeichert, acht "Boolesche" Bits in einem Byte).

Aus diesem Grund (da es nicht möglich ist, einen Verweis auf ein einzelnes Bit zurückzugeben) verwendet vector<bool> Einen sogenannten "Proxy-Iterator" Muster. Ein "Proxy-Iterator" ist ein Iterator, der, wenn er dereferenziert wird, nicht einen gewöhnlichen bool & Ergibt, sondern (nach Wert) a ) zurückgibt + temporäres Objekt , das eine Proxy-Klasse konvertierbar in bool ist. (Siehe auch diese Frage und die zugehörigen Antworten hier auf StackOverflow.)

Um die Elemente von vector<bool> Zu ändern, muss eine neue Art von Syntax (mit 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 Syntax for (auto&& elem : container) auch in anderen Fällen von normalen (nicht-Proxy-) Iteratoren funktioniert (z. B. für ein vector<int> Oder ein vector<string>).

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

Zusammenfassung

Die obige Diskussion kann in den folgenden Richtlinien zusammengefasst werden:

  1. Verwenden Sie zum Beobachten der Elemente die folgende Syntax:

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

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

  2. Verwenden Sie zum Ändern der vorhandenen Elemente Folgendes:

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

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

Wenn Sie eine lokale Kopie des Elements innerhalb des Schleifenkörpers erstellen müssen, erfassen Sie natürlich nach Wert (for (auto elem : container)) ist eine gute Wahl.


Zusätzliche Hinweise zum generischen Code

In generischer Code können wir nicht davon ausgehen, dass der generische Typ T billig zu kopieren ist, indem wir beobachten Modus ist es sicher, immer for (const auto& elem : container) zu verwenden.
(Dies löst keine potenziell teuren, nutzlosen Kopien aus. Dies funktioniert problemlos auch für kostengünstig zu kopierende Typen wie int und für Container, die Proxy-Iteratoren wie std::vector<bool>.)

Darüber hinaus können Sie im Modus den Modus ändern, wenn generischer Code auch bei Proxy-Iteratoren funktionieren soll ist die beste Option for (auto&& elem : container).
(Dies funktioniert problemlos auch für Container, die gewöhnliche Nicht-Proxy-Iteratoren wie std::vector<int> Oder std::vector<string> Verwenden.)

Daher können in generischer Code die folgenden Richtlinien bereitgestellt werden:

  1. Verwenden Sie zum Beobachten der Elemente:

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

    for (auto&& elem : container)
    
317
Mr.C64

Es gibt keinen richtigen Weg, um for (auto elem : container) oder for (auto& elem : container) oder for (const auto& elem : container) zu verwenden. Sie drücken einfach aus, was Sie wollen.

Lassen Sie mich darauf näher eingehen. Lass uns einen Spaziergang machen.

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 diesen verwenden, wenn Ihr Container Elemente enthält, die günstig 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, bei richtiger Anwendung ist alles "richtig".

12
user2266240

Das richtige Mittel ist immer

for(auto&& elem : container)

Dies garantiert die Erhaltung aller Semantiken.

6
Puppy

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

Die syntaktische Anforderung für die for-Schleife ist, dass range_expressionbegin() und end() als eine der beiden Funktionen unterstützt - entweder als Elementfunktionen des Typs, den es auswertet, oder als Nicht-Member-Funktionen, die eine Instanz des Typs annehmen.

Als konstruiertes Beispiel kann man einen Bereich von Zahlen erzeugen und den Bereich unter Verwendung der folgenden Klasse durchlaufen.

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 Funktion main

#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 
1
R Sahu