Was sagt uns auto &&?

168

Wenn Sie Code wie lesen

auto&& var = foo();

Wo foogibt eine Funktion nach Wert des Typs zurück T. Dann varist ein lWert vom Typ rWertreferenz auf T. Aber wofür bedeutet das var? Heißt das, wir dürfen die Ressourcen von stehlen var? Gibt es vernünftige Situationen, in denen Sie auto&&dem Leser Ihres Codes mitteilen sollten , wie Sie es tun, wenn Sie a zurückgeben, unique_ptr<>um mitzuteilen, dass Sie das ausschließliche Eigentum haben? Und was ist zum Beispiel, T&&wenn Tes sich um einen Klassentyp handelt?

Ich möchte nur verstehen, ob es andere Anwendungsfälle auto&&als die in der Vorlagenprogrammierung gibt. wie die in den Beispielen in diesem Artikel besprochenen Universal References von Scott Meyers.

MWid
quelle
1
Ich habe mich das Gleiche gefragt. Ich verstehe, wie der Typabzug funktioniert, aber was sagt mein Code , wenn ich ihn verwende auto&&? Ich habe darüber nachgedacht, warum eine bereichsbasierte for-Schleife auto&&als Beispiel erweitert wird, bin aber noch nicht dazu gekommen. Vielleicht kann jeder, der antwortet, es erklären.
Joseph Mansfield
1
Ist das überhaupt legal? Ich meine, die Instanz von T wird sofort nach der fooRückkehr zerstört, und das Speichern eines r-Wert-Refs klingt wie UB to ne.
1
@aleguna Das ist völlig legal. Ich möchte keine Referenz oder einen Zeiger auf eine lokale Variable zurückgeben, sondern einen Wert. Die Funktion fookönnte zum Beispiel so aussehen : int foo(){return 1;}.
MWid
9
@ aleguna-Verweise auf Provisorien führen wie in C ++ 98 eine Verlängerung der Lebensdauer durch.
Ecatmur
5
Die Verlängerung der Lebensdauer von @aleguna funktioniert nur mit lokalen Provisorien, nicht mit Funktionen, die Referenzen zurückgeben. Siehe stackoverflow.com/a/2784304/567292
ecatmur

Antworten:

232

Wenn auto&& var = <initializer>Sie verwenden, sagen Sie: Ich akzeptiere jeden Initialisierer, unabhängig davon, ob es sich um einen l-Wert- oder einen r-Wert-Ausdruck handelt, und ich werde seine Konstanz beibehalten . Dies wird normalerweise für die Weiterleitung verwendet (normalerweise mit T&&). Der Grund, warum dies funktioniert, ist, dass eine "universelle Referenz" auto&&oder T&&an irgendetwas gebunden ist .

Man könnte sagen, warum nicht einfach ein verwenden, const auto&weil das auch an irgendetwas gebunden ist? Das Problem bei der Verwendung einer constReferenz ist, dass es ist const! Sie können es später nicht mehr an nicht konstante Referenzen binden oder Mitgliedsfunktionen aufrufen, die nicht markiert sind const.

Stellen Sie sich als Beispiel vor, Sie möchten einen erhalten std::vector, nehmen Sie einen Iterator zu seinem ersten Element und ändern Sie den Wert, auf den dieser Iterator zeigt, auf irgendeine Weise:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

Dieser Code wird unabhängig vom Initialisierungsausdruck problemlos kompiliert. Die Alternativen, um auto&&auf folgende Weise zu scheitern:

auto         => will copy the vector, but we wanted a reference
auto&        => will only bind to modifiable lvalues
const auto&  => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

Funktioniert also auto&&perfekt! Ein Beispiel für eine solche Verwendung auto&&ist eine bereichsbasierte forSchleife. Siehe meine andere Frage für weitere Details.

Wenn Sie dann std::forwardIhre auto&&Referenz verwenden, um die Tatsache beizubehalten, dass es ursprünglich entweder ein l-Wert oder ein r-Wert war, lautet Ihr Code: Nachdem ich Ihr Objekt entweder aus einem l-Wert- oder einem r-Wert-Ausdruck erhalten habe, möchte ich die Wertigkeit beibehalten, die es ursprünglich hatte hatte, damit ich es am effizientesten nutzen kann - dies könnte es ungültig machen. Wie in:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

Dies ermöglicht es use_it_elsewhere, die Eingeweide aus Gründen der Leistung (Vermeidung von Kopien) herauszureißen, wenn der ursprüngliche Initialisierer ein veränderbarer Wert war.

Was bedeutet dies, ob wir Ressourcen stehlen können oder wann var? Nun, da der auto&&Wille an irgendetwas vargebunden ist , können wir unmöglich versuchen, uns selbst die Eingeweide herauszureißen - es kann sehr gut ein Wert oder sogar eine Konstante sein. Wir können es jedoch std::forwardauf andere Funktionen übertragen, die sein Inneres völlig verwüsten können. Sobald wir dies tun, sollten wir uns varin einem ungültigen Zustand befinden.

Wenden wir dies nun auf den Fall an auto&& var = foo();, wie in Ihrer Frage angegeben, bei dem foo einen By- TWert zurückgibt . In diesem Fall wissen wir sicher, dass die Art von varals abgeleitet wird T&&. Da wir mit Sicherheit wissen, dass es sich um einen Wert handelt, benötigen wir keine std::forwardErlaubnis, um seine Ressourcen zu stehlen. In diesem speziellen Fall sollte der Leser in dem Wissen, dass die fooRückgabe nach Wert erfolgt , einfach Folgendes lesen: Ich nehme einen r-Wert-Verweis auf die temporäre Rückgabe von foo, damit ich mich glücklich davon entfernen kann.


Als Nachtrag denke ich, dass es erwähnenswert ist, wann ein Ausdruck wie some_expression_that_may_be_rvalue_or_lvalueauftauchen könnte, abgesehen von einer Situation, in der sich Ihr Code möglicherweise ändert. Hier ist ein erfundenes Beispiel:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
  return global_vec;
}

template <typename T>
void foo()
{
  auto&& vec = get_vector<T>();
  auto i = std::begin(vec);
  (*i)++;
  std::cout << vec[0] << std::endl;
}

Hier get_vector<T>()ist dieser schöne Ausdruck, der je nach generischem Typ entweder ein l-Wert oder ein r-Wert sein kann T. Wir ändern im Wesentlichen den Rückgabetyp von get_vectordurch den Vorlagenparameter von foo.

Wenn wir aufrufen foo<std::vector<int>>, get_vectorwird global_vecby value zurückgegeben, was einen rvalue-Ausdruck ergibt. Wenn wir aufrufen foo<std::vector<int>&>, get_vectorwird alternativ als global_vecReferenz zurückgegeben, was zu einem lvalue-Ausdruck führt.

Wenn wir es tun:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

Wir erhalten erwartungsgemäß die folgende Ausgabe:

2
1
2
2

Wenn Sie die Änderungen waren auto&&im Code zu einem auto, auto&, const auto&, oder , const auto&&dann werden wir nicht das Ergebnis bekommen wir wollen.


Eine alternative Möglichkeit, die Programmlogik basierend darauf zu ändern, ob Ihre auto&&Referenz mit einem l-Wert- oder einem r-Wert-Ausdruck initialisiert wurde, besteht in der Verwendung von Typmerkmalen:

if (std::is_lvalue_reference<decltype(var)>::value) {
  // var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
  // var was initialised with an rvalue expression
}
Joseph Mansfield
quelle
2
Können wir nicht einfach T vec = get_vector<T>();innerhalb der Funktion foo sagen ? Oder vereinfache ich es auf ein absurdes Niveau :)
Sternchen
@Asterisk Kein bcoz T vec kann nur im Fall von std :: vector <int &> lvalue zugewiesen werden. Wenn T std :: vector <int> ist, verwenden wir einen ineffizienten Aufruf nach Wert
Kapil,
1
auto & gibt mir das gleiche Ergebnis. Ich verwende MSVC 2015. Und GCC erzeugt einen Fehler.
Sergey Podobry
Hier verwende ich MSVC 2015, auto & liefert die gleichen Ergebnisse wie auto &&.
Kehe CAI
Warum ist int i; auto && j = i; erlaubt aber int i; int && j = i; ist nicht ?
SeventhSon84
14

Zunächst empfehle ich , meine Antwort als Nebenlesung zu lesen, um Schritt für Schritt zu erklären, wie die Ableitung von Vorlagenargumenten für universelle Referenzen funktioniert.

Heißt das, wir dürfen die Ressourcen von stehlen var?

Nicht unbedingt. Was ist, wenn foo()plötzlich eine Referenz zurückgegeben wird oder Sie den Anruf geändert haben, aber vergessen haben, die Verwendung von zu aktualisieren var? Oder wenn Sie generischen Code verwenden und sich der Rückgabetyp foo()abhängig von Ihren Parametern ändern kann?

Denken Sie auto&&daran, genau das gleiche zu sein wie T&&in template<class T> void f(T&& v);, denn es ist (fast ) genau das. Was machen Sie mit universellen Referenzen in Funktionen, wenn Sie sie weitergeben oder in irgendeiner Weise verwenden müssen? Sie verwenden std::forward<T>(v), um die ursprüngliche Wertkategorie zurückzugewinnen. Wenn es ein Wert war, bevor es an Ihre Funktion übergeben wurde, bleibt es ein Wert, nachdem es übergeben wurde std::forward. Wenn es sich um einen r-Wert handelt, wird er wieder zu einem r-Wert (denken Sie daran, dass eine benannte r-Wert-Referenz ein l-Wert ist).

Also, wie benutzt man es vargenerisch richtig? Verwenden Sie std::forward<decltype(var)>(var). Dies funktioniert genauso wie std::forward<T>(v)in der obigen Funktionsvorlage. Wenn dies der Fall varist T&&, erhalten Sie einen Wert zurück, und wenn dies der Fall ist T&, erhalten Sie einen Wert zurück.

Zurück zum Thema: Was sagen uns auto&& v = f();und std::forward<decltype(v)>(v)in einer Codebasis? Sie sagen uns, dass vdies auf die effizienteste Weise erworben und weitergegeben wird. Denken Sie jedoch daran, dass eine solche Variable nach dem Weiterleiten möglicherweise verschoben wurde. Daher ist es falsch, sie weiter zu verwenden, ohne sie zurückzusetzen.

Persönlich verwende ich auto&&generischen Code, wenn ich eine modifizierbare Variable benötige . Das perfekte Weiterleiten eines R-Werts ändert sich, da der Verschiebevorgang möglicherweise seinen Mut stiehlt. Wenn ich nur faul sein möchte (dh den Typnamen nicht buchstabieren möchte, auch wenn ich ihn kenne) und nicht ändern muss (z. B. wenn nur Elemente eines Bereichs gedruckt werden), bleibe ich dabei auto const&.


autoist in so weit anders, auto v = {1,2,3};dass vein std::initializer_list, während f({1,2,3})wird ein Abzugsfehler sein.

Xeo
quelle
Im ersten Teil Ihrer Antwort: Ich meine , dass , wenn foo()ein Wert vom Typ zurückkehrt T, dann var(dieser Ausdruck) wird ein L - Wert und seine Art sein (dieser Ausdruck) wird ein R - Wert in Bezug gesetzt T(dh T&&).
MWid
@ MWid: Sinnvoll, den ersten Teil entfernt.
Xeo
3

Stellen Sie sich einen Typ Tmit einem Verschiebungskonstruktor vor und nehmen Sie an

T t( foo() );

verwendet diesen Verschiebungskonstruktor.

Verwenden wir nun eine Zwischenreferenz, um die Rendite von zu erfassen foo:

auto const &ref = foo();

Dies schließt die Verwendung des Verschiebungskonstruktors aus, sodass der Rückgabewert kopiert anstatt verschoben werden muss (selbst wenn wir ihn std::movehier verwenden, können wir nicht durch eine const ref verschieben).

T t(std::move(ref));   // invokes T::T(T const&)

Wenn wir jedoch verwenden

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

Der Verschiebungskonstruktor ist weiterhin verfügbar.


Und um Ihre anderen Fragen zu beantworten:

... Gibt es vernünftige Situationen, in denen Sie auto && verwenden sollten, um dem Leser Ihres Codes etwas mitzuteilen ...

Das erste, was Xeo sagt, ist, dass ich X so effizient wie möglich übergebe , egal welcher Typ X ist. Wenn Sie also Code sehen, der auto&&intern verwendet wird, sollte dies mitteilen, dass die Verschiebungssemantik gegebenenfalls intern verwendet wird.

... wie Sie es tun, wenn Sie ein unique_ptr <> zurückgeben, um anzuzeigen, dass Sie exklusives Eigentum haben ...

Wenn eine Funktionsvorlage ein Argument vom Typ annimmt T&&, bedeutet dies, dass das übergebene Objekt möglicherweise verschoben wird. Durch die Rückgabe unique_ptrerhält der Aufrufer explizit den Besitz. Annahme T&&kann entfernen Besitz des Anrufers (wenn ein Umzug Ctor existiert, etc.).

Nutzlos
quelle
2
Ich bin nicht sicher, ob Ihr zweites Beispiel gültig ist. Müssen Sie nicht perfekt weiterleiten, um den Verschiebungskonstruktor aufzurufen?
3
Das ist falsch. In beiden Fällen wird der Kopierkonstruktor aufgerufen, da refund rvrefbeide Werte sind. Wenn Sie den Verschiebungskonstruktor möchten, müssen Sie schreiben T t(std::move(rvref)).
MWid
Meinten Sie in Ihrem ersten Beispiel const ref : auto const &?
PiotrNycz
@aleguna - du und MWid haben recht, danke. Ich habe meine Antwort korrigiert.
Nutzlos
1
@ Nutzlos Du hast recht. Aber das beantwortet meine Frage nicht. Wann verwenden Sie auto&&und was sagen Sie dem Leser Ihres Codes mit auto&&?
MWid
-3

Die auto &&Syntax verwendet zwei neue Funktionen von C ++ 11:

  1. Mit diesem autoTeil kann der Compiler den Typ basierend auf dem Kontext ableiten (in diesem Fall den Rückgabewert). Dies ist ohne Referenzqualifikationen (so dass Sie angeben , ob Sie wollen T, T &oder T &&für eine davon abgeleitete Typen T).

  2. Das &&ist die neue Bewegungssemantik. Ein Typ, der die Verschiebungssemantik unterstützt, implementiert einen Konstruktor T(T && other), der den Inhalt im neuen Typ optimal verschiebt. Dadurch kann ein Objekt die interne Darstellung austauschen, anstatt eine tiefe Kopie durchzuführen.

Dies ermöglicht Ihnen so etwas wie:

std::vector<std::string> foo();

So:

auto var = foo();

führt eine Kopie des zurückgegebenen Vektors durch (teuer), aber:

auto &&var = foo();

tauscht die interne Darstellung des Vektors (der Vektor von foound der leere Vektor von var) aus, ist also schneller.

Dies wird in der neuen For-Loop-Syntax verwendet:

for (auto &item : foo())
    std::cout << item << std::endl;

Wobei die for-Schleife einen auto &&auf den Rückgabewert von hält foound itemeine Referenz auf jeden Wert in ist foo.

reece
quelle
Das ist falsch. auto&&wird nichts bewegen, es wird nur eine Referenz machen. Ob es sich um eine l-Wert- oder eine r-Wert-Referenz handelt, hängt von dem Ausdruck ab, der zum Initialisieren verwendet wird.
Joseph Mansfield
In beiden Fällen wird der Verschiebungskonstruktor aufgerufen, da std::vectorund std::stringbewegungskonstruierbar sind. Dies hat nichts mit der Art von zu tun var.
MWid
1
@MWid: Tatsächlich könnte der Aufruf des Copy / Move-Konstruktors auch mit RVO vollständig entfallen.
Matthieu M.
@MatthieuM. Du hast recht. Aber ich denke, dass im obigen Beispiel der Kopierkonstruktor niemals aufgerufen wird, da alles verschiebbar ist.
MWid
1
@ MWid: Mein Punkt war, dass sogar der Verschiebungskonstruktor entfernt werden kann. Elision Trümpfe bewegen sich (es ist billiger).
Matthieu M.