it-swarm.com.de

Was ist "rWert-Referenz für * dieses"?

Kam über einen Vorschlag namens "rvalue reference for * this" in Clangs C++ 11-Statusseite .

Ich habe viel über rvalue-Referenzen gelesen und sie verstanden, aber ich glaube nicht, dass ich darüber Bescheid 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 * dies), aber ich erhalte von dort auch nicht viele Beispiele.

Worum geht es in dieser Funktion?

229
ryaner

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

Als nächstes wählt der folgende Code die aufzurufende Funktion basierend auf dem ref-Qualifier 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 getan, um Ihnen zu ermöglichen, die Tatsache auszunutzen, dass das Objekt, auf das die Funktion aufgerufen wird, ein R-Wert ist (z. B. unbenannter temporärer Wert). 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 cv-Qualifier (const und volatile) und ref-Qualifier (& Und &&).


Hinweis: Viele Standard-Anführungszeichen und Erläuterungen zur Überlastungsauflösung folgen hier!

† Um zu verstehen, wie dies funktioniert und warum die Antwort von @Nicol Bolas zumindest teilweise falsch ist, müssen wir ein wenig im C++ - Standard graben (der Teil, der erklärt, warum die Antwort von @ Nicol falsch ist, steht unten, wenn Sie es sind nur daran interessiert).

Welche Funktion aufgerufen wird, bestimmt ein Prozess namens Überlastungsauflösung. Dieser Prozess ist ziemlich kompliziert, daher werden wir nur das berühren, was uns wichtig ist.

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

§13.3.1 [over.match.funcs]

p2 Die Menge der Kandidatenfunktionen kann sowohl Member- als auch Nicht-Member-Funktionen enthalten, die anhand derselben Argumentliste aufgelöst werden sollen. Damit Argument- und Parameterlisten innerhalb dieser heterogenen Menge vergleichbar sind, eine Mitgliedsfunktion hat einen zusätzlichen Parameter, den impliziten Objektparameter, der das Objekt darstellt, für das die Mitgliedsfunktion aufgerufen wurde. [...]

p3 Ebenso kann der Kontext gegebenenfalls eine Argumentliste erstellen, die ein implizites Objektargument enthält, um das zu bearbeitende Objekt zu kennzeichnen.

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

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

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

Sie möchten sicher, dass die folgende Funktion die kostenlose Funktion aufruft, nicht wahr?

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

Deshalb 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 der wichtige Punkt für uns (gleiche Klausel):

p4 Bei nicht statischen Elementfunktionen lautet der Typ des impliziten Objektparameters

  • "LWertverweis auf cvX" für Funktionen, die ohne ref-Qualifier oder mit dem &deklariert wurden -ref-qualifier

  • "RVerweis auf cvX" für Funktionen, die mit dem && ref-Qualifier deklariert wurden

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

p5 Während der Überladungsauflösung [...] behält der implizite Objektparameter [...] seine Identität bei, da die 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 Übereinstimmung mit dem Typ zu erzielen

[...]

(Das letzte Bit bedeutet nur, dass Sie die Überladungsauflö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 erwähnten Transformation sieht die Überladungsmenge 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 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(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 eine übrig bleibt, ist 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, hätten wir keinen ref-Qualifier angegeben (und damit die Funktion nicht überladen), f1 würde mit einem übereinstimmen rWert (noch §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 const -qualifiziert ist, kann ein Wert an den Parameter gebunden werden, solange das Argument in jeder anderen Hinsicht 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 *this Ändert.

Das ist falsch, *this Ist immer ein lWert:

§5.3.1 [expr.unary.op] p1

Der unäre * - Operator führt Indirektion aus: Der Ausdruck, auf den er angewendet wird, soll ein Zeiger auf einen Objekttyp oder ein Zeiger auf einen Funktionstyp sein nd der Ergebnis ist ein lWert bezieht sich auf das Objekt oder die Funktion, auf die der Ausdruck verweist.

§9.3.2 [class.this] p1

Im Hauptteil einer nicht statischen (9.3) Elementfunktion ist das Schlüsselwort this ein Wertausdruck, dessen Wert die Adresse des Objekts ist, für das die Funktion aufgerufen wird. Der Typ von this in einer Mitgliedsfunktion einer Klasse X ist X*. [...]

284
Xeo

Es gibt einen zusätzlichen Anwendungsfall für das lvalue ref-Qualifier-Formular. C++ 98 verfügt über eine Sprache, mit der Member-Funktionen, die nicht const sind, für Klasseninstanzen aufgerufen werden können, bei denen es sich um R-Werte handelt. Dies führt zu jeder Art von Verrücktheit, die dem Konzept der Wertigkeit zuwiderläuft und von der Funktionsweise eingebauter Typen abweicht:

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-Qualifier 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 lWerte.

77
JohannesD

Angenommen, Sie haben zwei Funktionen in einer Klasse, beide mit demselben Namen und derselben Signatur. Aber einer von ihnen wird als const deklariert:

void SomeFunc() const;
void SomeFunc();

Wenn eine Klasseninstanz nicht const ist, wählt die Überladungsauflösung vorzugsweise die Nicht-Konstanten-Version aus. Wenn die Instanz const ist, kann der Benutzer nur die Version const aufrufen. Und der Zeiger this ist ein Zeiger const, sodass die Instanz nicht geändert werden kann.

Mit "r-value reference for this" 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 richtigen r-Wert aufruft. Wenn dies also der Typ Object ist:

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 über einen r-Wert auf das Objekt zugegriffen wird oder nicht.

Beachten Sie, dass Sie nicht zwischen den r-Wert-Referenzversionen und den Nicht-Referenzversionen überladen dürfen. Wenn Sie also einen Mitgliedsfunktionsnamen haben, verwenden alle Versionen entweder die l/r-Wert-Qualifizierer für this, oder keiner 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 *this Ändert. Dies bedeutet, dass die Versionen && Alle Zugriffsmember als r-Wert-Referenzen enthalten. So wird es möglich, sich leicht aus dem Objekt heraus zu bewegen. Das Beispiel in der ersten Version des Vorschlags lautet:

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
28
Nicol Bolas