Come mai un riferimento non const non può legarsi a un oggetto temporaneo?


226

Perché non è consentito ottenere riferimenti non costanti a un oggetto temporaneo, quale funzione getx()restituisce? Chiaramente, questo è vietato dalla norma C ++ ma sono interessato allo scopo di tale limitazione, non a un riferimento alla norma.

struct X
{
    X& ref() { return *this; }
};

X getx() { return X();}

void g(X & x) {}    

int f()
{
    const X& x = getx(); // OK
    X& x = getx(); // error
    X& x = getx().ref(); // OK
    g(getx()); //error
    g(getx().ref()); //OK
    return 0;
}
  1. È chiaro che la durata dell'oggetto non può essere la causa, poiché il riferimento costante a un oggetto non è proibito dallo standard C ++.
  2. È chiaro che l'oggetto temporaneo non è costante nell'esempio sopra, poiché sono consentite chiamate a funzioni non costanti. Ad esempio, ref()potrebbe modificare l'oggetto temporaneo.
  3. Inoltre, ref()ti consente di ingannare il compilatore e ottenere un collegamento a questo oggetto temporaneo e questo risolve il nostro problema.

Inoltre:

Dicono che "l'assegnazione di un oggetto temporaneo al riferimento const estende la durata di questo oggetto" e "Nulla si dice sui riferimenti non costanti". La mia domanda aggiuntiva . La seguente assegnazione prolunga la durata dell'oggetto temporaneo?

X& x = getx().ref(); // OK

4
Non sono d'accordo con la parte "la durata dell'oggetto non può essere la causa", proprio perché è affermato nello standard, che l'assegnazione di un oggetto temporaneo al riferimento const estende la durata di questo oggetto alla durata del riferimento const. Nulla si dice sui riferimenti non
costanti

3
Bene, qual è la causa di questo "Nulla si dice sui riferimenti non costanti però ...". Fa parte della mia domanda. C'è qualche senso in questo? Potrebbero essere autori di Standard appena dimenticato riferimenti non costanti e presto vedremo il prossimo numero principale?
Alexey Malistov,

2
GotW # 88: un candidato per la "const più importante". herbsutter.spaces.live.com/blog/cns!2D4327CC297151BB!378.entry
fnieto - Fernando Nieto,

4
@Michael: VC associa i valori ai riferimenti non costanti. Lo chiamano una funzionalità, ma in realtà è un bug. (Nota che non è un bug perché è intrinsecamente illogico, ma perché è stato escluso esplicitamente per prevenire errori sciocchi.)
sbi

Risposte:


97

Da questo articolo del blog di Visual C ++ sui riferimenti ai valori :

... C ++ non vuole che tu modifichi accidentalmente i provvisori, ma chiamare direttamente una funzione membro non const su un valore modificabile è esplicito, quindi è permesso ...

Fondamentalmente, non dovresti provare a modificare i provvisori proprio perché sono oggetti temporanei e moriranno da un momento all'altro. Il motivo per cui ti è permesso chiamare metodi non costanti è che, beh, sei il benvenuto a fare alcune cose "stupide" fintanto che sai cosa stai facendo e ne sei esplicito (come usare reinterpret_cast). Ma se associ un temporaneo a un riferimento non const, puoi continuare a passarlo "per sempre" solo per far scomparire la tua manipolazione dell'oggetto, perché da qualche parte lungo la strada hai completamente dimenticato che si trattava di un temporaneo.

Se fossi in te, ripenserei al design delle mie funzioni. Perché g () accetta il riferimento, modifica il parametro? Se no, rendilo riferimento costante, se sì, perché provi a passargli temporaneamente, non ti importa che sia un temporaneo che stai modificando? Perché getx () torna comunque temporaneo? Se condividi con noi il tuo scenario reale e ciò che stai cercando di realizzare, potresti ricevere alcuni buoni suggerimenti su come farlo.

Andare contro il linguaggio e ingannare il compilatore raramente risolve i problemi - di solito crea problemi.


Modifica: rispondere alle domande nel commento: 1) X& x = getx().ref(); // OK when will x die?- Non lo so e non mi interessa, perché questo è esattamente ciò che intendo per "andare contro la lingua". La lingua dice "i provvisori muoiono alla fine dell'affermazione, a meno che non siano vincolati al riferimento const, nel qual caso muoiono quando il riferimento va oltre lo scopo". Applicando quella regola, sembra che x sia già morto all'inizio dell'istruzione successiva, poiché non è legato al riferimento const (il compilatore non sa cosa restituisce ref ()). Questa è solo una supposizione però.

2) Ho dichiarato chiaramente lo scopo: non è consentito modificare i provvisori, perché non ha senso (ignorando i riferimenti al valore C ++ 0x). La domanda "allora perché mi è permesso chiamare membri non const?" è buono, ma non ho una risposta migliore di quella che ho già affermato sopra.

3) Beh, se ho ragione x X& x = getx().ref();morire alla fine della frase, i problemi sono evidenti.

Ad ogni modo, in base alla tua domanda e ai tuoi commenti, non penso che nemmeno queste risposte extra ti possano soddisfare. Ecco un ultimo tentativo / riassunto: il comitato C ++ ha deciso che non ha senso modificare i provvisori, pertanto non ha consentito il legame con riferimenti non costanti. Potrebbe essere l'implementazione di un compilatore o sono stati coinvolti anche problemi storici, non lo so. Quindi, è emerso un caso specifico ed è stato deciso che, nonostante ogni previsione, consentiranno comunque la modifica diretta tramite la chiamata del metodo non const. Ma questa è un'eccezione: in genere non ti è consentito modificare i provvisori. Sì, il C ++ è spesso così strano.


2
@sbk: 1) In realtà, la frase corretta è: "... alla fine dell'espressione completa ...". Una "espressione completa", credo, è definita come una che non è una sottoespressione di un'altra espressione. Se questa sia sempre la stessa "fine dell'affermazione", non sono sicuro.
sbi,

5
@sbk: 2) In realtà, si è consentito modificare rvalues (provvisori). E 'vietato per i tipi built-in ( intetc.), ma è consentito per i tipi definiti dall'utente: (std::string("A")+"B").append("C").
sbi,

6
@sbk: 3) Il motivo fornito da Stroustrup (in D&E) per non consentire l'associazione di valori a riferimenti non costanti è che, se Alexey g()modificasse l'oggetto (cosa che ti aspetteresti da una funzione che prende un riferimento non const), modificherebbe un oggetto che morirà, quindi nessuno potrebbe ottenere comunque il valore modificato. Dice che questo, molto probabilmente, è un errore.
sbi,

2
@sbk: mi dispiace se ti ho offeso, ma non credo che 2) sia un pignolo. I valori semplicemente non sono cost se non li crei e puoi cambiarli, a meno che non siano integrati. Mi ci è voluto un po 'di tempo per capire se, ad esempio con l'esempio di stringa, faccio qualcosa di sbagliato (JFTR: non lo faccio), quindi tendo a prendere sul serio questa distinzione.
sbi,

5
Sono d'accordo con sbi - questa questione non è affatto nitpicking. È tutta la base della semantica di movimento che i valori del tipo di classe sono meglio non costanti.
Johannes Schaub - litb

38

Nel tuo codice getx()restituisce un oggetto temporaneo, un cosiddetto "valore". È possibile copiare valori in oggetti (ovvero variabili) o associarli a riferimenti const (che prolungheranno la loro durata fino alla fine della vita del riferimento). Non è possibile associare valori a riferimenti non costanti.

Questa è stata una decisione di progettazione deliberata al fine di impedire agli utenti di modificare accidentalmente un oggetto che morirà alla fine dell'espressione:

g(getx()); // g() would modify an object without anyone being able to observe

Se vuoi farlo, dovrai prima creare una copia locale o dell'oggetto o associarlo a un riferimento const:

X x1 = getx();
const X& x2 = getx(); // extend lifetime of temporary to lifetime of const reference

g(x1); // fine
g(x2); // can't bind a const reference to a non-const reference

Si noti che il prossimo standard C ++ includerà riferimenti di valore. Ciò che conosci come riferimenti sta diventando quindi chiamato "riferimenti lvalue". Ti sarà permesso di associare i valori ai riferimenti dei valori e puoi sovraccaricare le funzioni in "rvalue-ness":

void g(X&);   // #1, takes an ordinary (lvalue) reference
void g(X&&);  // #2, takes an rvalue reference

X x; 
g(x);      // calls #1
g(getx()); // calls #2
g(X());    // calls #2, too

L'idea alla base dei riferimenti rvalue è che, poiché questi oggetti moriranno comunque, puoi trarre vantaggio da quella conoscenza e implementare quella che viene chiamata "spostare la semantica", un certo tipo di ottimizzazione:

class X {
  X(X&& rhs)
    : pimpl( rhs.pimpl ) // steal rhs' data...
  {
    rhs.pimpl = NULL; // ...and leave it empty, but deconstructible
  }

  data* pimpl; // you would use a smart ptr, of course
};


X x(getx()); // x will steal the rvalue's data, leaving the temporary object empty

2
Ciao, questa è una risposta fantastica. Hai bisogno di sapere una cosa, la g(getx())non funziona perché la sua firma è g(X& x)e get(x)restituisce un oggetto temporaneo, quindi non possiamo associare un oggetto temporaneo ( valore ) a un riferimento non costante, giusto? E nel tuo primo pezzo di codice, penso che sarà const X& x2 = getx();invece di const X& x1 = getx();..
SexyBeast

1
Grazie per aver segnalato questo errore nella mia risposta 5 anni dopo averlo scritto! :-/Sì, il tuo ragionamento è corretto, anche se un po 'all'indietro: non possiamo legare i provvisori a riferimenti non const(lvalue), e quindi il temporaneo restituito da getx()(e non get(x)) non può essere associato al riferimento lvalue a cui è argomento g().
sabato

Umm, cosa intendevi con getx()(e non get(x)) ?
SexyBeast

Quando scrivo "... getx () (e non get (x)) ..." , intendo che il nome della funzione è getx()e non get(x)(come hai scritto).
sabato

Questa risposta mescola la terminologia. Un valore è una categoria di espressioni. Un oggetto temporaneo è un oggetto. Un valore può indicare o meno un oggetto temporaneo; e un oggetto temporaneo può essere o meno indicato da un valore.
MM

16

Quello che stai mostrando è che è consentito il concatenamento dell'operatore.

 X& x = getx().ref(); // OK

L'espressione è 'getx (). Ref ();' e questo viene eseguito fino al completamento prima dell'assegnazione a 'x'.

Si noti che getx () non restituisce un riferimento ma un oggetto completamente formato nel contesto locale. L'oggetto è temporaneo, ma è non è const, permettendo così di chiamare altri metodi per calcolare un valore o avere altri effetti collaterali accadere.

// It would allow things like this.
getPipeline().procInstr(1).procInstr(2).procInstr(3);

// or more commonly
std::cout << getManiplator() << 5;

Guarda la fine di questa risposta per un migliore esempio di questo

Si può non associare un temporaneo per un riferimento perché così facendo genererà un riferimento a un oggetto che verrà distrutta alla fine dell'espressione lasciando in tal modo con un riferimento penzoloni (che è disordinata e lo standard non piace disordinato).

Il valore restituito da ref () è un riferimento valido ma il metodo non presta attenzione alla durata dell'oggetto che sta restituendo (perché non può avere tali informazioni nel suo contesto). In pratica hai appena fatto l'equivalente di:

x& = const_cast<x&>(getX());

Il motivo per cui è OK eseguire ciò con un riferimento const a un oggetto temporaneo è che lo standard estende la durata del temporaneo alla durata del riferimento in modo che la durata degli oggetti temporanei venga estesa oltre la fine dell'istruzione.

Quindi l'unica domanda rimasta è: perché lo standard non vuole consentire il riferimento ai provvisori per prolungare la vita dell'oggetto oltre la fine dell'affermazione?

Credo che sia perché renderebbe il compilatore molto difficile da correggere per gli oggetti temporanei. È stato fatto per riferimenti costanti ai provvisori poiché questo ha un uso limitato e quindi ti ha costretto a fare una copia dell'oggetto per fare qualcosa di utile ma fornisce alcune funzionalità limitate.

Pensa a questa situazione:

int getI() { return 5;}
int x& = getI();

x++; // Note x is an alias to a variable. What variable are you updating.

Estendere la durata di vita di questo oggetto temporaneo sarà molto confuso.
Mentre il seguente:

int const& y = getI();

Ti fornirà un codice intuitivo da usare e da capire.

Se si desidera modificare il valore, è necessario restituire il valore a una variabile. Se stai cercando di evitare il costo della copia dell'obejct dalla funzione (poiché sembra che l'oggetto sia una copia costruita indietro (tecnicamente lo è)). Quindi non disturbare il compilatore è molto bravo in "Return Value Optimization"


3
"Quindi l'unica domanda rimasta è perché lo standard non vuole consentire il riferimento ai provvisori per prolungare la vita dell'oggetto oltre la fine dell'affermazione?" Questo è tutto! Si capisce la mia domanda. Ma non sono d'accordo con la tua opinione. Dici "rendi il compilatore molto difficile" ma è stato fatto per riferimento const. Nel tuo esempio dici "Nota x è un alias di una variabile. Quale variabile stai aggiornando." Nessun problema. C'è una variabile unica (temporanea). Alcuni oggetti temporanei (equivalgono a 5) devono essere cambiati.
Alexey Malistov,

@Martin: i riferimenti ciondolanti non sono solo disordinati. Potrebbero portare a gravi bug quando si accederà più avanti nel metodo!
mmmmmmmm

1
@Alexey: Notare che il fatto che l'associazione a un riferimento const migliora la vita di un temporaneo è un'eccezione che è stata aggiunta deliberatamente (TTBOMK per consentire ottimizzazioni manuali). Non è stata aggiunta un'eccezione per i riferimenti non costanti, poiché l'associazione di un riferimento temporaneo a un riferimento non const è stata probabilmente considerata un errore del programmatore.
sbi,

1
@alexy: un riferimento a una variabile invisibile! Non così intuitivo.
Martin York,

const_cast<x&>(getX());non ha senso
curiousguy il

10

Perché è discusso nelle FAQ del C ++ ( grassetto mio):

In C ++, i riferimenti non costanti possono legarsi ai valori lvalori e i riferimenti costanti possono legarsi ai valori lvalori o rvalori, ma non c'è nulla che possa legarsi a un valore non const. Questo per proteggere le persone dal cambiare i valori dei provvisori che vengono distrutti prima che il loro nuovo valore possa essere usato . Per esempio:

void incr(int& a) { ++a; }
int i = 0;
incr(i);    // i becomes 1
incr(0);    // error: 0 is not an lvalue

Se a quel incr (0) fosse concesso un temporaneo che nessuno avesse mai visto sarebbe incrementato o - molto peggio - il valore di 0 diventerebbe 1. Quest'ultimo sembra sciocco, ma in realtà c'era un bug come quello nei primi compilatori Fortran che imposta a parte una posizione di memoria per contenere il valore 0.


Sarebbe stato divertente vedere il volto del programmatore che è stato morso da quel "bug zero" di Fortran! x * 0 x? Che cosa? Che cosa??
John D,

Quest'ultimo argomento è particolarmente debole. Nessun compilatore degno di nota cambierebbe mai il valore da 0 a 1, o addirittura interpreterebbe incr(0);in tal modo. Ovviamente, se ciò fosse consentito, verrebbe interpretato come la creazione di un numero intero temporaneo e il passaggio aincr()
user3204459

6

Il problema principale è quello

g(getx()); //error

è un errore logico: gsta modificando il risultato di getx()ma non hai alcuna possibilità di esaminare l'oggetto modificato. Se gnon fosse necessario modificare il suo parametro, non avrebbe richiesto un riferimento lvalue, avrebbe potuto prendere il parametro in base al valore o al riferimento const.

const X& x = getx(); // OK

è valido perché a volte è necessario riutilizzare il risultato di un'espressione ed è abbastanza chiaro che si ha a che fare con un oggetto temporaneo.

Tuttavia non è possibile effettuare

X& x = getx(); // error

valido senza renderlo g(getx())valido, che è ciò che i progettisti linguistici stavano cercando di evitare in primo luogo.

g(getx().ref()); //OK

è valido perché i metodi conoscono solo la costanza di the this, non sanno se sono chiamati su un valore o su un valore.

Come sempre in C ++, hai una soluzione alternativa per questa regola ma devi segnalare al compilatore che sai cosa stai facendo essendo esplicito:

g(const_cast<x&>(getX()));

5

Sembra che la domanda originale sul perché ciò non sia consentito abbia avuto una risposta chiara: "perché molto probabilmente è un errore".

FWIW, ho pensato di mostrare come si potesse fare, anche se non credo sia una buona tecnica.

Il motivo per cui a volte voglio passare un temporaneo a un metodo che prende un riferimento non const è di gettare via intenzionalmente un valore restituito per riferimento che non interessa al metodo chiamante. Qualcosa come questo:

// Assuming: void Person::GetNameAndAddr(std::string &name, std::string &addr);
string name;
person.GetNameAndAddr(name, string()); // don't care about addr

Come spiegato nelle risposte precedenti, ciò non viene compilato. Ma questo si compila e funziona correttamente (con il mio compilatore):

person.GetNameAndAddr(name,
    const_cast<string &>(static_cast<const string &>(string())));

Questo dimostra solo che puoi usare il casting per mentire al compilatore. Ovviamente, sarebbe molto più pulito dichiarare e passare una variabile automatica inutilizzata:

string name;
string unused;
person.GetNameAndAddr(name, unused); // don't care about addr

Questa tecnica introduce una variabile locale non necessaria nell'ambito del metodo. Se per qualche motivo si desidera impedire che venga utilizzato in seguito nel metodo, ad esempio, per evitare confusione o errore, è possibile nasconderlo in un blocco locale:

string name;
{
    string unused;
    person.GetNameAndAddr(name, unused); // don't care about addr
}

-- Chris


4

Perché mai vorresti X& x = getx();? Basta usare X x = getx();e fare affidamento su RVO.


3
Perché voglio chiamare g(getx())piuttosto cheg(getx().ref())
Alexey Malistov,

4
@Alexey, non è una vera ragione. Se lo fai, allora hai un errore logico da qualche parte, perché gsta per modificare qualcosa su cui non puoi più mettere le mani.
Johannes Schaub - litb

3
@ JohannesSchaub-litb forse non gli interessa.
curiousguy

" fare affidamento su RVO " tranne che non si chiama "RVO".
curiousguy,

2
@curiousguy: è un termine molto accettato. Non c'è assolutamente nulla di sbagliato nel riferirsi ad esso come "RVO".
Cucciolo


3

Ottima domanda, ed ecco il mio tentativo di una risposta più concisa (dal momento che molte informazioni utili sono nei commenti e difficili da scavare nel rumore.)

Qualsiasi riferimento associato direttamente a un temporaneo prolungherà la sua durata [12.2.5]. D'altra parte, un riferimento inizializzato con un altro riferimento non lo farà (anche se alla fine è lo stesso temporaneo). Questo ha senso (il compilatore non sa a cosa si riferisca alla fine quel riferimento).

Ma tutta questa idea è estremamente confusa. Ad esempio const X &x = X();, il temporaneo durerà quanto il xriferimento, ma const X &x = X().ref();NON (chissà cosa è ref()effettivamente tornato). In quest'ultimo caso, il distruttore per Xviene chiamato alla fine di questa linea. (Questo è osservabile con un distruttore non banale.)

Quindi sembra generalmente confuso e pericoloso (perché complicare le regole sulla durata degli oggetti?), Ma presumibilmente c'era almeno bisogno di riferimenti cost, quindi lo standard stabilisce questo comportamento per loro.

[Dal commento sbi ]: si noti che il fatto che l'associazione a un riferimento const migliora la vita di un temporaneo è un'eccezione che è stata aggiunta deliberatamente (TTBOMK per consentire ottimizzazioni manuali). Non è stata aggiunta un'eccezione per i riferimenti non costanti, poiché l'associazione di un riferimento temporaneo a un riferimento non const è stata probabilmente considerata un errore del programmatore.

Tutti i provvisori persistono fino alla fine dell'espressione completa. Per farne uso, tuttavia, è necessario un trucco come hai fatto con ref(). Questo è legale. Non sembra esserci una buona ragione per saltare il telaio extra, tranne per ricordare al programmatore che sta succedendo qualcosa di insolito (vale a dire, un parametro di riferimento le cui modifiche andranno rapidamente perse).

[Un altro commento sbi ] Il motivo fornito da Stroustrup (in D&E) per non consentire l'associazione di valori a riferimenti non costanti è che, se il g () di Alexey modificasse l'oggetto (che ci si aspetterebbe da una funzione che prende un non const riferimento), modificherebbe un oggetto che morirà, quindi nessuno potrebbe ottenere comunque il valore modificato. Dice che questo, molto probabilmente, è un errore.


2

"È chiaro che l'oggetto temporaneo non è costante nell'esempio sopra, perché sono consentite chiamate a funzioni non costanti. Ad esempio, ref () potrebbe modificare l'oggetto temporaneo."

Nel tuo esempio getX () non restituisce una const X quindi puoi chiamare ref () più o meno allo stesso modo in cui potresti chiamare X (). Ref (). Restituisci un riferimento non const e quindi puoi chiamare metodi non const, ciò che non puoi fare è assegnare il riferimento a un riferimento non const.

Insieme al commento SadSidos questo rende errati i tuoi tre punti.


1

Ho uno scenario che vorrei condividere dove vorrei poter fare quello che Alexey sta chiedendo. In un plugin Maya C ++, devo fare il seguente shenanigan per ottenere un valore in un attributo del nodo:

MFnDoubleArrayData myArrayData;
MObject myArrayObj = myArrayData.create(myArray);   
MPlug myPlug = myNode.findPlug(attributeName);
myPlug.setValue(myArrayObj);

Questo è noioso da scrivere, quindi ho scritto le seguenti funzioni di supporto:

MPlug operator | (MFnDependencyNode& node, MObject& attribute){
    MStatus status;
    MPlug returnValue = node.findPlug(attribute, &status);
    return returnValue;
}

void operator << (MPlug& plug, MDoubleArray& doubleArray){
    MStatus status;
    MFnDoubleArrayData doubleArrayData;
    MObject doubleArrayObject = doubleArrayData.create(doubleArray, &status);
    status = plug.setValue(doubleArrayObject);
}

E ora posso scrivere il codice dall'inizio del post come:

(myNode | attributeName) << myArray;

Il problema è che non viene compilato al di fuori di Visual C ++, perché sta cercando di associare la variabile temporanea restituita dal | operatore al riferimento MPlug dell'operatore <<. Vorrei che fosse un riferimento perché questo codice viene chiamato molte volte e preferirei che MPlug non venisse copiato così tanto. Ho solo bisogno che l'oggetto temporaneo viva fino alla fine della seconda funzione.

Bene, questo è il mio scenario. Ho solo pensato di mostrare un esempio in cui uno vorrebbe fare ciò che Alexey descrive. Accolgo con favore tutte le critiche e i suggerimenti!

Grazie.

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.