Was ist "Wertreferenz für * dies"?


238

Kam auf einen Vorschlag namens "rvalue reference for * this" auf der C ++ 11- Statusseite von clang .

Ich habe ziemlich viel über rvalue-Referenzen gelesen und verstanden, aber ich glaube nicht, dass ich davon weiß. Ich konnte mit den Begriffen auch nicht viele Ressourcen im Web finden.

Es gibt einen Link zum Vorschlagspapier auf der Seite: N2439 (Erweiterung der Verschiebungssemantik auf * this), aber ich bekomme auch nicht viele Beispiele von dort.

Worum geht es in dieser Funktion?

Antworten:


293

Erstens ist "ref-qualifiers for * this" nur eine "Marketing-Aussage". Die Art von *thisändert sich nie, siehe unten in diesem Beitrag. Mit dieser Formulierung ist es jedoch viel einfacher zu verstehen.

Als nächstes wählt der folgende Code die aufzurufende Funktion basierend auf dem Ref-Qualifier des "impliziten Objektparameters" der Funktion † aus :

// 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 durchgeführt, damit Sie die Tatsache ausnutzen können, dass das Objekt, für das die Funktion aufgerufen wird, ein r-Wert ist (z. B. unbenannt temporär). Nehmen Sie den folgenden Code als weiteres Beispiel:

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);
  }
};

Dies mag ein bisschen erfunden sein, aber Sie sollten auf die Idee kommen.

Beachten Sie, dass Sie die kombinieren können cv-Qualifier ( constund volatile) und ref-Qualifikations ( &und &&).


Hinweis: Viele Standardzitate und Erklärungen zur Überlastungsauflösung finden Sie hier!

† Um zu verstehen, wie dies 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 die Antwort von @ Nicol falsch ist, befindet sich unten, wenn Sie es sind nur daran interessiert).

Welche Funktion aufgerufen werden soll, wird durch einen Prozess namens Überlastauflösung bestimmt . Dieser Prozess ist ziemlich kompliziert, daher werden wir nur das Bit berühren, das für uns wichtig ist.

Zunächst ist es wichtig zu sehen, wie die Überlastungsauflösung für Mitgliedsfunktionen funktioniert:

§13.3.1 [over.match.funcs]

p2 Der Satz von Kandidatenfunktionen kann sowohl Element- als auch Nichtmitgliedsfunktionen enthalten, die anhand derselben Argumentliste aufgelöst werden sollen. Damit Argument- und Parameterlisten innerhalb dieser heterogenen Menge vergleichbar sind, wird davon ausgegangen, dass eine Elementfunktion einen zusätzlichen Parameter hat, den impliziten Objektparameter, der das Objekt darstellt, für das die Elementfunktion aufgerufen wurde . [...]

p3 In ähnlicher Weise kann der Kontext gegebenenfalls eine Argumentliste erstellen , die ein implizites Objektargument enthält , um das zu bearbeitende Objekt zu bezeichnen.

Warum müssen wir überhaupt Funktionen von Mitgliedern und Nichtmitgliedern vergleichen? Überlastung des Bedieners, deshalb. Bedenken Sie:

struct foo{
  foo& operator<<(void*); // implementation unimportant
};

foo& operator<<(foo&, char const*); // implementation unimportant

Sie möchten sicher, dass Folgendes die freie Funktion aufruft, nicht wahr?

char const* s = "free foo!\n";
foo f;
f << s;

Aus diesem Grund sind Member- und Non-Member-Funktionen im sogenannten Overload-Set enthalten. Um die Auflösung zu vereinfachen, ist der fett gedruckte Teil des Standardzitats vorhanden. Darüber hinaus ist dies das wichtige Bit für uns (gleiche Klausel):

p4 Für nicht statische Elementfunktionen lautet der Typ des impliziten Objektparameters

  • "Wertreferenz auf Lebenslauf X " für Funktionen, die ohne Ref-Qualifier oder mit Ref-Qualifier deklariert wurden&

  • "R-Wert-Referenz auf Lebenslauf X " für Funktionen, die mit dem Ref-Qualifier deklariert wurden&&

Wo Xist die Klasse, zu der die Funktion gehört, und cv ist die cv-Qualifikation in der Deklaration der Mitgliedsfunktion. [...]

p5 Während der Überlastauflösung [...] [t] behält der implizite Objektparameter [...] seine Identität, da Konvertierungen für das entsprechende Argument diese zusätzlichen Regeln befolgen müssen:

  • Es kann kein temporäres Objekt eingeführt werden, das das Argument für den impliziten Objektparameter enthält. und

  • Es können keine benutzerdefinierten Konvertierungen angewendet werden, um eine Typübereinstimmung zu erzielen

[...]

(Das letzte Bit bedeutet nur, dass Sie die Überlastungsauflösung nicht aufgrund impliziter Konvertierungen des Objekts betrügen können, für das eine Mitgliedsfunktion (oder ein Operator) aufgerufen wird.)

Nehmen wir das erste Beispiel oben in diesem Beitrag. Nach der oben genannten Transformation sieht der Überlastungssatz ungefähr 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 wird die Argumentliste, die ein implizites Objektargument enthält , mit der Parameterliste jeder in der Überladungsmenge 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(t); // 't' (lvalue) can match 'test&' (lvalue reference)
       // kept in overload-set
f2(t); // 't' not an rvalue, can't match 'test&&' (rvalue reference)
       // taken out of overload-set

Wenn nach dem Testen aller Überlastungen im Satz nur noch eine übrig bleibt, war die Überlastungsauflösung erfolgreich und die mit dieser transformierten Überlast verknüpfte Funktion wird aufgerufen. Gleiches 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, wenn wir kein Ref-Qualifikationsmerkmal bereitgestellt hätten (und als solches die Funktion nicht überladen hätten), dies (noch ) mit einem r-Wert übereinstimmen f1 würde§13.3.1 :

p5 [...] Für nicht statische Elementfunktionen, die ohne Ref-Qualifier deklariert wurden , gilt eine zusätzliche Regel:

  • Selbst wenn der implizite Objektparameter nicht constqualifiziert ist, kann ein r-Wert an den Parameter gebunden werden, solange das Argument im Übrigen 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 @ Nicol's Antwort zumindest teilweise falsch ist. Er sagt:

Beachten Sie, dass diese Deklaration den Typ von ändert *this.

Das ist falsch, *thisist immer ein Wert:

§5.3.1 [expr.unary.op] p1

Der unäre *Operator führt eine Indirektion durch : Der Ausdruck, auf den er angewendet wird, muss ein Zeiger auf einen Objekttyp oder ein Zeiger auf einen Funktionstyp sein, und das Ergebnis ist ein Wert, der sich auf das Objekt oder die Funktion bezieht, auf die der Ausdruck zeigt.

§9.3.2 [class.this] p1

Im Hauptteil einer nicht statischen (9.3) Mitgliedsfunktion ist das Schlüsselwort thisein prvalue-Ausdruck, dessen Wert die Adresse des Objekts ist, für das die Funktion aufgerufen wird. Der Typ thisin einer Member-Funktion einer Klasse Xist X*. [...]


Ich glaube, die Paranetertypen direkt nach dem Abschnitt "Nach der Transformation" sollten "foo" anstelle von "test" sein.
Ryaner

@ryaner: Guter Fund, danke. Obwohl nicht der Parameter, sondern die Klassenkennung der Funktionen falsch ist. :)
Xeo

Ups, tut mir leid, ich habe die Spielzeugklasse namens Test vergessen, als ich diesen Teil gelesen habe und dachte, dass f in foo enthalten ist, also mein Kommentar.
Ryaner

Kann das mit Konstruktoren gemacht werden : MyType(int a, double b) &&?
Germán Diago

2
"Die Art von * ändert sich nie" Sie sollten vielleicht etwas klarer sein, dass sich dies nicht aufgrund der R / L-Wert-Qualifikation ändert. aber es kann zwischen const / non-const wechseln.
Xaxxon

78

Es gibt einen zusätzlichen Anwendungsfall für das lvalue ref-qualifier-Formular. C ++ 98 verfügt über eine Sprache, mit der constNichtmitgliedsfunktionen für Klasseninstanzen aufgerufen werden können, bei denen es sich um r-Werte handelt. Dies führt zu allen Arten von Verrücktheit, die gegen das Konzept der Wertigkeit 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-Qualifikanten lösen diese Probleme:

struct S {
  S& operator ++() &;
  S* operator &() &;
  const S& operator =(const S&) &;
};

Jetzt arbeiten die Operatoren wie die der eingebauten Typen und akzeptieren nur l-Werte.


28

Angenommen, Sie haben zwei Funktionen für eine Klasse, beide mit demselben Namen und derselben Signatur. Aber einer von ihnen wird erklärt const:

void SomeFunc() const;
void SomeFunc();

Wenn dies keine Klasseninstanz ist const, wählt die Überlastungsauflösung vorzugsweise die nicht konstante Version aus. Wenn dies der Fall ist const, kann der Benutzer nur die constVersion aufrufen . Und der thisZeiger ist ein constZeiger, daher kann die Instanz nicht geändert werden.

Mit "r-Wert-Referenz dafür" können Sie eine weitere Alternative hinzufügen:

void RValueFunc() &&;

Auf diese Weise können Sie eine Funktion haben, die nur aufgerufen werden kann, wenn der Benutzer sie über einen geeigneten r-Wert aufruft. Also, wenn dies in der Art 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 nicht zwischen den r-Wert-Referenzversionen und den Nicht-Referenzversionen überladen dürfen. Das heißt, wenn Sie einen Mitgliedsfunktionsnamen haben, verwenden alle Versionen entweder die l / r-Wert-Qualifikationsmerkmale für thisoder keine von ihnen. Das kannst du nicht machen:

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 &&alle Versionen auf Mitglieder als R-Wert-Referenzen zugreifen. So wird es möglich, sich leicht aus dem Objekt heraus 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 ergibt sich direkt aus dem ursprünglichen "r-Wert aus diesem" 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

2
Ich denke du brauchst std::movedie zweite Version, nicht? Warum gibt die r-Wert-Referenz auch zurück?
Xeo

1
@Xeo: Weil das das Beispiel im Vorschlag war; Ich habe keine Ahnung, ob es noch mit der aktuellen Version funktioniert. Der Grund für die Rückgabe des r-Wert-Referenzwerts liegt darin, dass die Bewegung der Person überlassen bleiben sollte, die sie erfasst. Es sollte noch nicht passieren, nur für den Fall, dass er es tatsächlich in einem && anstelle eines Wertes speichern möchte.
Nicol Bolas

3
Richtig, ich habe mir den Grund für meine zweite Frage überlegt. Ich frage mich jedoch, ob ein r-Wert-Verweis auf das Mitglied eines Provisoriums die Lebensdauer dieses Provisoriums oder dessen Mitglied verlängert. Ich könnte schwören, dass ich vor einiger Zeit eine Frage dazu auf SO gesehen habe ...
Xeo

1
@Xeo: Es ist nicht ganz wahr. Bei der Überlastungsauflösung wird immer die nicht konstante Version ausgewählt, falls vorhanden. Sie müssten eine Besetzung machen, um die const-Version zu erhalten. Ich habe den Beitrag aktualisiert, um dies zu verdeutlichen.
Nicol Bolas

15
Ich dachte, ich könnte es erklären, schließlich habe ich diese Funktion für C ++ 11 erstellt;) Xeo besteht zu Recht darauf, dass sie den Typ nicht ändert *this, aber ich kann verstehen, woher die Verwirrung kommt. Dies liegt daran, dass ref-qualifier den Typ des impliziten (oder "versteckten") Funktionsparameters ändert, an den das Objekt "this" (hier absichtlich angegeben!) Während der Überlastungsauflösung und des Funktionsaufrufs gebunden ist. Also keine Änderung, *thisda dies behoben ist, wie Xeo erklärt. Ändern Sie stattdessen den Parameter "hiddden", um ihn auf lvalue- oder rvalue-reference zu setzen, genau wie es der constFunktionsqualifizierer constusw. macht .
bronekk
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.