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?
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:
13579
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;}intGet()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 forvector<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(constauto& x : v){
cout << x <<' ';}
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(constauto& 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:
13579
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):
1030507090
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(constauto& x : v)
cout << x <<' ';
Die Ausgabe ist:
HiBob!HiJeff!HiConnie!
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 statusfor(auto&& x : v)// <-- note use of "auto&&" for proxy iterators
x =!x;// Print new element values
cout << boolalpha;for(constauto& x : v)
cout << x <<' ';
und Ausgänge:
falsetruetruefalse
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(constauto& 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
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:
Verwenden Sie zur Beobachtung der Elemente:
for(constauto& elem : container)
Verwenden Sie zum Ändern der vorhandenen Elemente:
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 referenceauto& elem =*it;}
Verwenden Sie diese Option, wenn Sie beispielsweise direkt in die Elemente im Container schreiben möchten.
for(constauto& elem : container)...
Dieser ist syntaktischer Zucker für:
for(auto it = container.begin(); it != container.end();++it){// You just want to read stuff, no modificationconstauto& elem =*it;}
Wie der Kommentar sagt, nur zum Lesen. Und das war's auch schon, alles ist "richtig", wenn es richtig verwendet wird.
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).
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.
auto (const)(&) x = <expr>;
.auto
im Allgemeinen mehr zu tun als mit bereichsbasiert für; Sie können Range-Based perfekt ohne verwendenauto
!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 mitauto
... zu tun, aber aus der Frage ist nicht ersichtlich, wo der Schmerzpunkt liegt. Persönlich würde ich versuchen,auto
aus der Frage zu entfernen ; oder machen Sie es vielleicht explizit, dass sichauto
die Frage auf Wert / Referenz konzentriert , unabhängig davon, ob Sie den Typ verwenden oder explizit benennen.Antworten:
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:
Der obige Code druckt die Elemente
int
invector
: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.
Wenn wir die obige
for (auto x : v) {...}
Syntax für diese neue Klasse verwenden:Die Ausgabe ist so etwas wie:
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 , dhconst auto&
:Jetzt ist die Ausgabe:
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 .:Andernfalls ist die Erfassung per
const
Referenz im allgemeinen Fall besser , um nutzlose (und möglicherweise teure) Aufrufe von Kopierkonstruktoren zu vermeiden:Ändern der Elemente im Container
Wenn wir die Elemente in einem Container bereichsbasiert ändern möchten
for
, sind die obigenfor (auto elem : container)
und diefor (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.Die Ausgabe ist nur die Anfangssequenz:
Stattdessen kann ein Versuch der Verwendung
for (const auto& x : v)
einfach nicht kompiliert werden.g ++ gibt eine Fehlermeldung wie folgt aus:
Der richtige Ansatz in diesem Fall ist die Erfassung durch Nichtreferenz
const
:Die Ausgabe ist (wie erwartet):
Diese
for (auto& elem : container)
Syntax funktioniert auch für komplexere Typen, zB unter Berücksichtigung einvector<string>
:Die Ausgabe ist:
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:Der obige Code kann nicht kompiliert werden.
g ++ gibt eine ähnliche Fehlermeldung aus:
Das Problem ist , dass
std::vector
Vorlage spezialisiert fürbool
, mit einer Implementierung , die Packungen , diebool
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 (usingauto&&
) verwendet werden:Der folgende Code funktioniert einwandfrei:
und Ausgänge:
Beachten Sie, dass die
for (auto&& elem : container)
Syntax auch in anderen Fällen von normalen (Nicht-Proxy-) Iteratoren funktioniert (z. B. für avector<int>
oder avector<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:
Wenn die Objekte billig zu kopieren sind (wie
int
s,double
s usw.), kann ein leicht vereinfachtes Formular verwendet werden:Verwenden Sie zum Ändern der vorhandenen Elemente:
Wenn der Container "Proxy-Iteratoren" (wie
std::vector<bool>
) verwendet, verwenden Sie: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 verwendenfor (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 verwendenstd::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 verwendenstd::vector<string>
.)Im generischen Code können daher die folgenden Richtlinien bereitgestellt werden:
Verwenden Sie zur Beobachtung der Elemente:
Verwenden Sie zum Ändern der vorhandenen Elemente:
quelle
auto&&
? Gibt es eineconst auto&&
?auto&&
, da esauto&
gleich gut abdeckt .Es gibt keine richtige Art und Weise zu verwenden
for (auto elem : container)
, oderfor (auto& elem : container)
oderfor (const auto& elem : container)
. Sie drücken einfach aus, was Sie wollen.Lassen Sie mich darauf näher eingehen. Machen wir einen Spaziergang.
Dieser ist syntaktischer Zucker für:
Sie können dieses verwenden, wenn Ihr Container Elemente enthält, die billig zu kopieren sind.
Dieser ist syntaktischer Zucker für:
Verwenden Sie diese Option, wenn Sie beispielsweise direkt in die Elemente im Container schreiben möchten.
Dieser ist syntaktischer Zucker für:
Wie der Kommentar sagt, nur zum Lesen. Und das war's auch schon, alles ist "richtig", wenn es richtig verwendet wird.
quelle
Das richtige Mittel ist immer
Dies garantiert die Beibehaltung aller Semantik.
quelle
auto const &
, um meine Absicht klar zu machen?const
und niemals veränderbar. Wie auch immer, meine Antwort an Sie lautet ja, würde ich .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_expression
Unterstützungbegin()
undend()
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.
Mit der folgenden
main
Funktion:man würde die folgende Ausgabe bekommen.
quelle