Che cos'è "riferimento valore per * questo"?


238

Ho trovato una proposta chiamata "riferimento al valore per * this" nella pagina di stato C ++ 11 di clang .

Ho letto un bel po 'di riferimenti a valori e li ho compresi, ma non credo di saperlo. Inoltre non sono riuscito a trovare molte risorse sul web usando i termini.

C'è un link al documento della proposta sulla pagina: N2439 (Estendere la semantica di spostamento a * questo), ma da qui non ottengo molti esempi.

Di cosa tratta questa funzionalità?

Risposte:


293

In primo luogo, "qualificazioni di ref per * this" è solo una "dichiarazione di marketing". Il tipo di *thisnon cambia mai, vedi il fondo di questo post. È molto più facile capirlo con questa formulazione però.

Successivamente, il codice seguente sceglie la funzione da chiamare in base al qualificatore di riferimento del "parametro oggetto implicito" della funzione :

// 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
}

Produzione:

$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object

Il tutto viene fatto per permetterti di sfruttare il fatto quando l'oggetto su cui viene chiamata la funzione è un valore (ad esempio temporaneo senza nome). Prendi il seguente codice come ulteriore esempio:

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

Questo potrebbe essere un po 'inventato, ma dovresti avere l'idea.

Si noti che è possibile combinare i qualificatori cv ( conste volatile) e i qualificatori ref ( &e &&).


Nota: molte citazioni standard e spiegazioni sulla risoluzione del sovraccarico dopo qui!

† Per capire come funziona e perché la risposta di @Nicol Bolas è almeno in parte sbagliata, dobbiamo scavare un po 'nello standard C ++ (la parte che spiega perché la risposta di @Nicol è sbagliata è in fondo, se sei interessato solo a quello).

La funzione che verrà chiamata è determinata da un processo chiamato risoluzione di sovraccarico . Questo processo è abbastanza complicato, quindi toccheremo solo la parte che è importante per noi.

Innanzitutto, è importante vedere come funziona la risoluzione di sovraccarico per le funzioni membro:

§13.3.1 [over.match.funcs]

p2 L'insieme di funzioni candidate può contenere sia funzioni membro che non membro da risolvere sullo stesso elenco di argomenti. In modo che gli elenchi di argomenti e parametri siano comparabili all'interno di questo insieme eterogeneo, si ritiene che una funzione membro abbia un parametro aggiuntivo, chiamato parametro oggetto implicito, che rappresenta l'oggetto per il quale è stata chiamata la funzione membro . [...]

p3 Allo stesso modo, quando appropriato, il contesto può costruire un elenco di argomenti che contiene un argomento oggetto implicito per indicare l'oggetto su cui operare.

Perché dobbiamo anche confrontare le funzioni membro e non membro? Operatore sovraccarico, ecco perché. Considera questo:

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

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

Vorresti sicuramente quanto segue per chiamare la funzione gratuita, vero?

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

Ecco perché le funzioni membro e non membro sono incluse nel cosiddetto sovraccarico. Per rendere la risoluzione meno complicata, esiste la parte in grassetto della citazione standard. Inoltre, questo è il bit importante per noi (stessa clausola):

p4 Per le funzioni membro non statiche, il tipo del parametro oggetto implicito è

  • "Lvalue reference to cv X " per le funzioni dichiarate senza un qualificatore di riferimento o con il & qualificatore di riferimento

  • “Rvalue reference to cv X ” per le funzioni dichiarate con il && qualificatore di riferimento

dove Xè la classe di cui la funzione è membro e cv è la qualifica cv sulla dichiarazione della funzione membro. [...]

p5 Durante la risoluzione di sovraccarico [...] il parametro oggetto implicito [...] mantiene la sua identità poiché le conversioni sull'argomento corrispondente devono rispettare queste regole aggiuntive:

  • nessun oggetto temporaneo può essere introdotto per contenere l'argomento per il parametro oggetto implicito; e

  • nessuna conversione definita dall'utente può essere applicata per ottenere una corrispondenza di tipo con essa

[...]

(L'ultimo bit significa solo che non è possibile imbrogliare la risoluzione di sovraccarico in base alle conversioni implicite dell'oggetto su cui viene chiamata una funzione membro (o operatore).)

Facciamo il primo esempio all'inizio di questo post. Dopo la trasformazione di cui sopra, il sovraccarico appare in questo modo:

void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'

Quindi l'elenco degli argomenti, contenente un argomento oggetto implicito , viene confrontato con l'elenco dei parametri di ogni funzione contenuta nel set di overload. Nel nostro caso, l'elenco degli argomenti conterrà solo tale argomento. Vediamo come appare:

// 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

Se, dopo aver verificato tutti i sovraccarichi nel set, ne rimane solo uno, la risoluzione del sovraccarico ha esito positivo e viene chiamata la funzione collegata a quel sovraccarico trasformato. Lo stesso vale per la seconda chiamata a '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

Si noti comunque che, se non avessimo fornito alcuna ref-qualificazione (e come tale non sovraccaricato la funzione), che f1 sarebbe corrispondere a un rvalue (ancora §13.3.1):

p5 [...] Per le funzioni membro non statiche dichiarate senza qualificatore di riferimento , si applica una regola aggiuntiva:

  • anche se il parametro oggetto implicito non è const-qualificato, un valore può essere associato al parametro fintanto che sotto tutti gli altri aspetti l'argomento può essere convertito nel tipo di parametro oggetto implicito.
struct test{
  void f() { std::cout << "lvalue or rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // OK
  test().f(); // OK too
}

Ora, sul perché la risposta di @Nicol è almeno in parte sbagliata. Lui dice:

Si noti che questa dichiarazione cambia il tipo di *this.

Questo è sbagliato, *thisè sempre un valore:

§5.3.1 [expr.unary.op] p1

L' *operatore unario esegue il riferimento indiretto : l'espressione a cui viene applicata deve essere un puntatore a un tipo di oggetto o un puntatore a un tipo di funzione e il risultato è un valore che si riferisce all'oggetto o alla funzione a cui punta l'espressione.

§9.3.2 [class.this] p1

Nel corpo di una funzione membro non statica (9.3), la parola chiave thisè un'espressione di valore il cui valore è l'indirizzo dell'oggetto per cui viene chiamata la funzione. Il tipo di thisin una funzione membro di una classe Xè X*. [...]


Credo che i tipi di paraneter subito dopo la sezione "dopo la trasformazione" dovrebbero essere "pippo" anziché "test".
Ryan

@ryaner: buona scoperta, grazie. Sebbene non sia il parametro ma l'identificatore di classe delle funzioni è errato. :)
Xeo il

oops mi dispiace che mi sono dimenticato della classe di giocattoli chiamata test quando ho letto quella parte e ho pensato che f fosse contenuta in foo, quindi il mio commento ..
Ryaner,

Questo può essere fatto con i costruttori MyType(int a, double b) &&:?
Germán Diago,

2
"Il tipo di * questo non cambia mai" Forse dovresti essere un po 'più chiaro che non cambia in base alla qualificazione del valore r / l. ma può cambiare tra const / non-const.
xaxxon,

78

Esiste un caso d'uso aggiuntivo per il modulo di qualifica ref lvalue. C ++ 98 ha un linguaggio che consente constdi chiamare funzioni non membri per istanze di classe che sono valori. Questo porta a tutti i tipi di stranezze che sono contro il concetto stesso di rivalità e si discosta dal modo in cui funzionano i tipi incorporati:

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

I riqualificatori Lvalue risolvono questi problemi:

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

Ora gli operatori funzionano come quelli dei tipi predefiniti, accettando solo valori.


28

Supponiamo che tu abbia due funzioni in una classe, entrambe con lo stesso nome e la stessa firma. Ma uno di questi è dichiarato const:

void SomeFunc() const;
void SomeFunc();

In caso contrario const, la risoluzione di sovraccarico selezionerà preferibilmente la versione non const. Se l'istanza è const, l'utente può solo chiamare la constversione. E il thispuntatore è un constpuntatore, quindi l'istanza non può essere modificata.

Che cosa fa "riferimento valore r per questo" è consentire di aggiungere un'altra alternativa:

void RValueFunc() &&;

Ciò consente di avere una funzione che può essere chiamata solo se l'utente la chiama attraverso un valore r corretto. Quindi se questo è nel tipo Object:

Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.

In questo modo, è possibile specializzare il comportamento in base all'accesso o meno all'oggetto tramite un valore r.

Si noti che non è consentito sovraccaricare tra le versioni di riferimento del valore r e le versioni non di riferimento. Cioè, se si dispone di un nome di funzione membro, tutte le sue versioni utilizzano i qualificatori con valore l / r thiso nessuna delle due. Non puoi farlo:

void SomeFunc();
void SomeFunc() &&;

Devi fare questo:

void SomeFunc() &;
void SomeFunc() &&;

Si noti che questa dichiarazione cambia il tipo di *this. Ciò significa che &&tutte le versioni accedono ai membri come riferimenti di valore r. Quindi diventa possibile muoversi facilmente dall'interno dell'oggetto. L'esempio fornito nella prima versione della proposta è (nota: il seguente potrebbe non essere corretto con la versione finale di C ++ 11; è direttamente dalla proposta iniziale "valore r da questa"):

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
Penso che tu abbia bisogno std::movedella seconda versione, no? Inoltre, perché ritorna il valore di riferimento?
Xeo,

1
@Xeo: perché è quello che era l'esempio nella proposta; Non ho idea se funziona ancora con la versione corrente. E la ragione per il ritorno di riferimento del valore r è perché il movimento dovrebbe dipendere dalla persona che lo cattura. Non dovrebbe ancora accadere, nel caso in cui voglia effettivamente memorizzarlo in un && anziché in un valore.
Nicol Bolas,

3
Bene, ho pensato al motivo della mia seconda domanda. Mi chiedo, tuttavia, un riferimento di valore al membro di un temporaneo prolunga la vita di quel temporaneo o al suo membro? Potrei giurare di aver visto una domanda a riguardo su SO qualche tempo fa ...
Xeo

1
@Xeo: non è del tutto vero. La risoluzione di sovraccarico sceglierà sempre la versione non const se esiste. Dovresti fare un cast per ottenere la versione const. Ho aggiornato il post per chiarire.
Nicol Bolas,

15
Ho pensato di poter spiegare, dopo tutto quello che ho creato questa funzionalità per C ++ 11;) Xeo ha ragione nell'insistere che non cambia il tipo di *this, tuttavia posso capire da dove provenga la confusione. Questo perché ref-qualifier modifica il tipo di parametro di funzione implicita (o "nascosta") al quale l'oggetto "questo" (virgolette messe apposta qui!) È associato durante la risoluzione del sovraccarico e la chiamata di funzione. Quindi, nessun cambiamento *thisdal momento che questo è stato risolto come spiega Xeo. Invece cambia il parametro "hiddden" per renderlo lvalue- o rvalue-reference, proprio come il constqualificatore di funzione lo rende constecc.
bronekk
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.