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 int
in 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 x
for (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 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 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
, double
usw .:
for (auto elem : container)
Andernfalls ist die Erfassung per const
Referenz 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 elem
eine 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::vector
Vorlage spezialisiert für bool
, mit einer Implementierung , die Packungen , die bool
s 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:
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 int
s, double
s usw.), kann ein leicht vereinfachtes Formular verwendet werden:
for (auto elem : container) // capture by value
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 T
billig 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 int
und 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:
Verwenden Sie zur Beobachtung der Elemente:
for (const auto& elem : container)
Verwenden Sie zum Ändern der vorhandenen Elemente:
for (auto&& elem : container)