Stieß auf einen Vorschlag namens “rvalue reference for *this” in clang’s C++11-Statusseite.
Ich habe ziemlich viel über rvalue-Referenzen gelesen und sie verstanden, aber ich glaube nicht, dass ich darüber Bescheid weiß. Ich konnte auch nicht viele Ressourcen im Internet finden, die die Begriffe verwenden.
Es gibt einen Link zum Vorschlagspapier auf der Seite: N2439 (Erweitern der Move-Semantik auf *this), aber ich bekomme auch nicht viele Beispiele von dort.
Worum geht es bei dieser Funktion?
Erstens ist „ref-qualifiers for *this“ nur eine „Marketingaussage“. Die Art von *this
ändert sich nie, siehe unten in diesem Beitrag. Es ist jedoch viel einfacher, es mit dieser Formulierung zu verstehen.
Als Nächstes wählt der folgende Code die aufzurufende Funktion basierend auf aus Ref-Qualifizierer des “impliziten Objektparameters” der Funktion†:
// t.cpp
#include <iostream>
struct test{
void f() &{ std::cout << "lvalue object\n"; }
void f() &&{ std::cout << "rvalue object\n"; }
};
int main(){
test t;
t.f(); // lvalue
test().f(); // rvalue
}
Ausgabe:
$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object
Das Ganze wird gemacht, damit Sie die Tatsache ausnutzen können, wenn das Objekt, für das die Funktion aufgerufen wird, ein Rvalue ist (z. B. ein unbenanntes temporäres Objekt). Nehmen Sie als weiteres Beispiel folgenden Code:
struct test2{
std::unique_ptr<int[]> heavy_resource;
test2()
: heavy_resource(new int[500]) {}
operator std::unique_ptr<int[]>() const&{
// lvalue object, deep copy
std::unique_ptr<int[]> p(new int[500]);
for(int i=0; i < 500; ++i)
p[i] = heavy_resource[i];
return p;
}
operator std::unique_ptr<int[]>() &&{
// rvalue object
// we are garbage anyways, just move resource
return std::move(heavy_resource);
}
};
Das mag ein bisschen erfunden sein, aber Sie sollten die Idee bekommen.
Beachten Sie, dass Sie die kombinieren können Lebenslauf-Qualifikanten (const
und volatile
) und Ref-Qualifikanten (&
und &&
).
Hinweis: Viele Standardzitate und Erklärungen zur Auflösung der Überlastung nach hier!
† Um zu verstehen, wie das funktioniert und warum die Antwort von @Nicol Bolas zumindest teilweise falsch ist, müssen wir uns ein wenig mit dem C++-Standard befassen (der Teil, der erklärt, warum @Nicols Antwort falsch ist, befindet sich ganz unten, wenn Sie es sind nur daran interessiert).
Welche Funktion aufgerufen wird, wird durch einen aufgerufenen Prozess bestimmt Überlastungsauflösung. Dieser Prozess ist ziemlich kompliziert, also berühren wir nur das Bit, das für uns wichtig ist.
Zunächst ist es wichtig zu sehen, wie die Überladungsauflösung für Memberfunktionen funktioniert:
§13.3.1 [over.match.funcs]
p2 Der Satz von Kandidatenfunktionen kann sowohl Mitglieds- als auch Nichtmitgliedsfunktionen enthalten, die gegen dieselbe Argumentliste aufgelöst werden sollen. Damit Argument- und Parameterlisten innerhalb dieser heterogenen Menge vergleichbar sind, Es wird davon ausgegangen, dass eine Memberfunktion einen zusätzlichen Parameter hat, der als impliziter Objektparameter bezeichnet wird und das Objekt darstellt, für das die Memberfunktion aufgerufen wurde. […]
p3 In ähnlicher Weise kann der Kontext gegebenenfalls eine Argumentliste erstellen, die ein enthält implizites Objektargument um das zu operierende Objekt zu bezeichnen.
Warum müssen wir überhaupt Member- und Nicht-Member-Funktionen vergleichen? Operatorüberladung, deshalb. Bedenken Sie:
struct foo{
foo& operator<<(void*); // implementation unimportant
};
foo& operator<<(foo&, char const*); // implementation unimportant
Sie möchten sicherlich, dass Folgendes die kostenlose Funktion aufruft, nicht wahr?
char const* s = "free foo!\n";
foo f;
f << s;
Deshalb sind Member- und Nicht-Member-Funktionen im sogenannten Overload-Set enthalten. Um die Auflösung weniger kompliziert zu machen, existiert der fettgedruckte Teil des Standardzitats. Außerdem ist dies der wichtige Teil für uns (gleiche Klausel):
p4 Bei nicht statischen Elementfunktionen ist der Typ des impliziten Objektparameters
-
„lvalue Verweis auf Lebenslauf X
” für ohne a deklarierte Funktionen Ref-Qualifizierer oder mit dem &
Ref-Qualifizierer
-
„rvalue Verweis auf Lebenslauf X
” für mit deklarierte Funktionen &&
Ref-Qualifizierer
wo X
ist die Klasse, in der die Funktion Mitglied ist, und Lebenslauf ist die CV-Qualifikation in der Member-Funktionsdeklaration. […]
p5 Während Überlastauflösung […]
-
es kann kein temporäres Objekt eingeführt werden, um das Argument für den impliziten Objektparameter zu halten; und
-
Es können keine benutzerdefinierten Konvertierungen angewendet werden, um eine Typübereinstimmung damit zu erzielen
[…]
(Das letzte Bit bedeutet nur, dass Sie die Überladungsauflösung nicht basierend auf impliziten Konvertierungen des Objekts, für das eine Memberfunktion (oder ein Operator) aufgerufen wird, betrügen können.)
Nehmen wir das erste Beispiel oben in diesem Beitrag. Nach der oben genannten Transformation sieht das Überladungsset etwa so aus:
void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'
Dann die Argumentliste, die eine enthält implizites Objektargument, wird mit der Parameterliste jeder im Überladungssatz enthaltenen Funktion abgeglichen. In unserem Fall enthält die Argumentliste nur dieses Objektargument. Mal sehen, wie das aussieht:
// first call to 'f' in 'main'
test t;
f1
// kept in overload-set
f2
// taken out of overload-set
Wenn nach dem Testen aller Überladungen im Satz nur noch eine verbleibt, war die Überladungsauflösung erfolgreich und die mit dieser transformierten Überladung verknüpfte Funktion wird aufgerufen. Dasselbe gilt für den zweiten Aufruf von ‘f’:
// second call to 'f' in 'main'
f1(test()); // 'test()' not an lvalue, can't match 'test&' (lvalue reference)
// taken out of overload-set
f2(test()); // 'test()' (rvalue) can match 'test&&' (rvalue reference)
// kept in overload-set
Beachten Sie jedoch, dass wir keine bereitgestellt hatten Ref-Qualifizierer (und damit die Funktion nicht überladen), dass f1
möchten Übereinstimmung mit einem rvalue (still §13.3.1
):
p5 […] Für nicht statische Elementfunktionen, die ohne a deklariert sind Ref-Qualifizierergilt eine zusätzliche Regel:
- auch wenn der implizite Objektparameter dies nicht ist
const
-qualifiziert, kann ein rvalue an den Parameter gebunden werden, solange das Argument ansonsten in den Typ des impliziten Objektparameters konvertiert werden kann.
struct test{
void f() { std::cout << "lvalue or rvalue object\n"; }
};
int main(){
test t;
t.f(); // OK
test().f(); // OK too
}
Nun, warum @Nicols Antwort zumindest teilweise falsch ist. Er sagt:
Beachten Sie, dass diese Deklaration den Typ von ändert *this
.
Das ist falsch, *this
ist immer ein lvalue:
§5.3.1 [expr.unary.op] p1
Das Unäre *
Betreiber durchführt indirekt: Der Ausdruck, auf den es angewendet wird, soll ein Zeiger auf einen Objekttyp oder ein Zeiger auf einen Funktionstyp sein und das Ergebnis ist ein lvalue bezieht sich auf das Objekt oder die Funktion, auf die der Ausdruck zeigt.
§9.3.2 [class.this] p1
Im Hauptteil einer nicht statischen (9.3) Elementfunktion wird das Schlüsselwort this
ist ein Prvalue-Ausdruck, dessen Wert die Adresse des Objekts ist, für das die Funktion aufgerufen wird. Die Art von this
in einer Member-Funktion einer Klasse X
ist X*
. […]
Es gibt einen zusätzlichen Anwendungsfall für das Lvalue-Ref-Qualifikator-Formular. C++98 hat eine Sprache, die nicht-const
Mitgliedsfunktionen, die für Klasseninstanzen aufgerufen werden sollen, die Rvalues sind. Dies führt zu allerlei Seltsamkeiten, die gegen das eigentliche Konzept der Rvalueness verstoßen und von der Funktionsweise eingebauter Typen abweichen:
struct S {
S& operator ++();
S* operator &();
};
S() = S(); // rvalue as a left-hand-side of assignment!
S& foo = ++S(); // oops, dangling reference
&S(); // taking address of rvalue...
Lvalue-Ref-Qualifizierer lösen diese Probleme:
struct S {
S& operator ++() &;
S* operator &() &;
const S& operator =(const S&) &;
};
Jetzt funktionieren die Operatoren wie die der eingebauten Typen und akzeptieren nur lvalues.
Angenommen, Sie haben zwei Funktionen in einer Klasse, beide mit demselben Namen und derselben Signatur. Aber einer von ihnen ist deklariert const
:
void SomeFunc() const;
void SomeFunc();
Wenn eine Klasseninstanz nicht const
, wird die Überladungsauflösung vorzugsweise die nicht-konstante Version auswählen. Wenn die Instanz ist const
der Benutzer kann nur die aufrufen const
Ausführung. Und das this
Zeiger ist ein const
Zeiger, sodass die Instanz nicht geändert werden kann.
Was “r-value reference for this” bewirkt, ist, dass Sie eine weitere Alternative hinzufügen können:
void RValueFunc() &&;
Dies ermöglicht Ihnen, eine Funktion zu haben, die kann nur aufgerufen werden, wenn der Benutzer sie über einen geeigneten R-Wert aufruft. Also wenn das im Typ ist Object
:
Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.
Auf diese Weise können Sie das Verhalten darauf spezialisieren, ob auf das Objekt über einen R-Wert zugegriffen wird oder nicht.
Beachten Sie, dass Sie zwischen den R-Wert-Referenzversionen und den Nicht-Referenzversionen nicht überladen dürfen. Das heißt, wenn Sie einen Mitgliedsfunktionsnamen haben, verwenden alle seine Versionen entweder die l/r-Wert-Qualifizierer on this
, oder keiner von ihnen tut. Das geht nicht:
void SomeFunc();
void SomeFunc() &&;
Du musst das tun:
void SomeFunc() &;
void SomeFunc() &&;
Beachten Sie, dass diese Deklaration den Typ von ändert *this
. Dies bedeutet, dass die &&
versioniert alle Zugriffsmitglieder als R-Wert-Referenzen. So wird es möglich, sich einfach innerhalb des Objekts zu bewegen. Das in der ersten Version des Vorschlags angegebene Beispiel lautet (Hinweis: Folgendes ist möglicherweise mit der endgültigen Version von C++ 11 nicht korrekt; es stammt direkt aus dem ursprünglichen „r-value from this“-Vorschlag):
class X {
std::vector<char> data_;
public:
// ...
std::vector<char> const & data() const & { return data_; }
std::vector<char> && data() && { return data_; }
};
X f();
// ...
X x;
std::vector<char> a = x.data(); // copy
std::vector<char> b = f().data(); // move