2009-04-08 4 views
53

C++ riferimenti hanno due proprietà:Perché i riferimenti non reseatable in C++

  • Essi puntano allo stesso oggetto sempre.
  • Non possono essere 0.

puntatori sono l'opposto:

  • Possono puntare a oggetti differenti.
  • Possono essere 0.

Perché non c'è "non annullabile, riferimento reseatable o il puntatore" in C++? Non riesco a pensare a una buona ragione per cui i riferimenti non dovrebbero essere resettabili.

Edit: La questione si presenta spesso perché io di solito uso i riferimenti quando voglio fare in modo che un ' "associazione" (Sto evitando le parole "di riferimento" o "puntatore" qui) non è mai valido.

Non credo di aver mai pensato "fantastico che questo riferimento si riferisca sempre allo stesso oggetto". Se i riferimenti erano reseatable, si poteva ancora ottenere il comportamento attuale in questo modo:

int i = 3; 
int& const j = i;

questo è già C++ legale, ma senza senso.

ho ribadiscono la mia domanda in questo modo: "Qual è stata la logica alla base del 'un riferimento è l'oggetto' progettare Perché è stato ritenuto utile avere riferimenti sempre essere lo stesso oggetto, anziché soltanto? quando dichiarato come const? "

Cheers, Felix

+2

So che è un po 'tardi, ma la risposta di Michael Burr sembra essere la più completa. Spiega le decisioni filosofiche del design di Stroustrup. Tutti gli altri sembrano mendicare la discussione (in un giro su modo). – jww

risposta

72

La ragione per cui C++ non permette di associare nuovamente i riferimenti è dato in "Progettazione ed evoluzione di C++" di Stroustrup:

Non è possibile modificare a quale riferimento fa riferimento dopo l'inizializzazione. Cioè, una volta che un riferimento C++ è inizializzato, non può essere fatto per riferirsi a un oggetto diverso più tardi; non può essere reimpostato. In passato sono stato morso da riferimenti Algol68 in cui r1=r2 può essere assegnato tramite l'r1 all'oggetto indicato o assegnare un nuovo valore di riferimento a r1 (rilegatura r1) in base al tipo di r2. Volevo evitare tali problemi in C++.

2

C++ riferimenti possono a volte essere costretti a essere con alcuni compilatori (è solo una cattiva idea di farlo *, e viola lo standard *).

int &x = *((int*)0); // Illegal but some compilers accept it 

EDIT: secondo varie persone che conoscono lo standard molto meglio di me, il codice precedente produce "comportamento non definito". In almeno alcune versioni di GCC e Visual Studio, ho visto questo fare la cosa prevista: l'equivalente di impostare un puntatore su NULL (e provoca un'eccezione di puntatore NULL quando si accede).

+0

"è solo una cattiva idea": perché l'utilità di una tale creatura è, um, limitata. – dmckee

+0

Può accadere accidentalmente, quindi è bene sapere della possibilità. Ho insistito su questo argomento prima. –

+0

Questo codice è illegale: in codice C++ valido, questo non deve mai accadere. Quindi no, i riferimenti NON possono mai essere 0, come garantito dallo standard C++. –

31

In C++, si dice spesso che "il riferimento è l'oggetto". In un certo senso, è vero: sebbene i riferimenti siano gestiti come puntatori quando il codice sorgente viene compilato, il riferimento intende significare un oggetto che non viene copiato quando viene chiamata una funzione. Poiché i riferimenti non sono direttamente indirizzabili (ad esempio, i riferimenti non hanno indirizzo, & restituisce l'indirizzo dell'oggetto), non avrebbe semanticamente senso riassegnarli. Inoltre, C++ ha già dei puntatori, che gestiscono la semantica del re-setting.

+0

Non riesco ancora a capire perché il (a mio avviso) utile riferimento resettabile non nullo non sia in C++. I problemi sollevati non sembrano difficili da risolvere e penso che l'affidabilità del programma aumenterebbe. Accettando questa risposta a causa dei voti. Correzione – TheFogger

+1

: "sebbene i riferimenti siano _often_ gestiti come puntatori" –

+9

"Il riferimento * è * l'oggetto" è SBAGLIATO. Ad eccezione del caso speciale di estensione temporanea di un temporaneo, la durata di un riferimento è distinta dalla durata dell'oggetto a cui si riferisce.Se il riferimento fosse effettivamente l'oggetto, l'oggetto sarebbe distrutto quando il riferimento usciva dall'ambito di applicazione (o, al contrario, sarebbe vissuto fino a qualsiasi riferimento ad esso), e questo non è il caso. –

0

Immagino che sia correlato all'ottimizzazione.

L'ottimizzazione statica è molto molto più facile quando si può sapere in modo univoco quale bit di memoria è una variabile. I puntatori rompono questa condizione e anche il riferimento re-impostabile.

4

Probabilmente sarebbe stato meno confuso nominare i riferimenti alias "alias"? Come altri hanno già menzionato, i riferimenti in C++ dovrebbero essere di come come variabile a cui si riferiscono, non come puntatore/ riferimento alla variabile. Come tale, non riesco a pensare a una buona ragione per cui il dovrebbe essere ripristinato da.

quando si tratta di puntatori, ha spesso senso consentire null come valore (e in caso contrario, si preferisce invece un riferimento). Se specificamente desidera disabilitare tenendo null, si può sempre codificare il proprio tipo di puntatore intelligente;)

+0

+1 per l'idea del puntatore intelligente; Avrei detto la stessa cosa Le nuove funzionalità non sono considerate per il linguaggio se è già possibile eseguire con lo standard esistente. –

0

Perché a volte le cose non devono essere ri-orientabili. (Ad esempio, il riferimento a Singleton.)

Perché è bello in una funzione sapere che il tuo argomento non può essere nullo.

Ma soprattutto, perché consente di utilizzare qualcosa che è realmente un puntatore, ma che si comporta come un oggetto valore locale. C++ ci prova duramente, per citare Stroustrup, per fare in modo che le istanze di classe "facciano come ints d". Passare un int da vaue è economico, perché un int si inserisce in un registro di macchina. Le classi sono spesso più grandi di quelle inte, e il loro superamento ha un overhead significativo.

Essere in grado di passare un puntatore (che è spesso la dimensione di un int, o forse due inte) che "assomiglia" a un oggetto valore ci permette di scrivere codice più pulito, senza i "dettagli di implementazione" delle dereferenze. E, insieme all'overloading dell'operatore, ci permette di scrivere le classi usando una sintassi simile alla sintassi usata con gli inte. In particolare, ci consente di scrivere classi di template con sintassi che possono essere applicate in ugual misura a primitive, come ints e classi (come una classe di numeri Complex).

E, soprattutto con l'overloading dell'operatore, ci sono dei punti in cui dovremmo restituire un oggetto, ma ancora una volta, è molto più economico restituire un puntatore. Oncve di nuovo, restituire un riferimento è il nostro "out.

E i puntatori sono difficili, non per te, forse, e non a nessuno che capisce che un puntatore è solo il valore di un indirizzo di memoria, ma ricordando la mia classe CS 101, hanno fatto inciampare un numero di studenti

può confondere.

Heck, dopo 40 anni di C, la gente ancora non può nemmeno d'accordo se una dichiarazione di puntatore dovrebbe essere:

char* p; 

o

char *p; 
+0

Il modo corretto è: char * p. Il disaccordo riguarda lo stile, che è soggettivo. – GManNickG

+1

@GMan: hai un errore di battitura lì. Hai detto che il modo corretto è "char * p", dove è ovviamente "char * p" poiché il tipo è pointer-to-char. Solo un utile, erm, puntatore. :) –

+0

@John: * Sfortunatamente *, le regole di sintassi C e C++ interrompono le dichiarazioni in due parti. Per illustrare: "char * p, c;" dichiara 1 pointer-to-char chiamato p e un carattere (NOT pointer-to-char) chiamato c. Lo so, schifo. :/ –

15

Perché allora si avrebbe nessun tipo reseatable che non può essere 0. A meno che non includa 3 tipi di riferimenti/puntatori. Il che complicherebbe solo la lingua per un guadagno molto piccolo (E poi perché non aggiungere anche il 4 ° tipo? Riferimento non resettabile che può essere 0?)

Una domanda migliore potrebbe essere, perché vuoi che i riferimenti siano resettabili? Se lo fossero, ciò li renderebbe meno utili in molte situazioni. Renderebbe più difficile per il compilatore eseguire analisi alias.

Sembra che il principale motivo per cui i riferimenti in Java o C# sono resettabili è perché fanno il lavoro dei puntatori. Indicano oggetti. Non sono alias per un oggetto.

Quale dovrebbe essere l'effetto di quanto segue?

int i = 42; 
int& j = i; 
j = 43; 

In C++ oggi, con riferimenti non resettabili, è semplice. j è un alias per i e termina con il valore 43.

Se i riferimenti erano stati resettabili, la terza riga avrebbe vincolato il riferimento j a un valore diverso. Non sarebbe più alias io, ma invece il numero intero letterale 43 (che non è valido, ovviamente). O forse un esempio più semplice (o almeno sintatticamente valido):

int i = 42; 
int k = 43; 
int& j = i; 
j = k; 

Con riferimenti resecabili. j indicherà k dopo aver valutato questo codice. Con i riferimenti non resettabili di C++, j punta ancora a i, e i viene assegnato il valore 43.

Rendendo i riferimenti ripetibili cambia la semantica della lingua. Il riferimento non può più essere un alias per un'altra variabile. Invece diventa un tipo di valore separato, con un proprio operatore di assegnazione. E quindi uno degli usi più comuni dei riferimenti sarebbe impossibile. E nulla sarebbe guadagnato in cambio. La funzionalità appena acquisita per i riferimenti esisteva già sotto forma di puntatori. Quindi ora avremmo due modi per fare la stessa cosa, e non c'è modo di fare ciò che fa i riferimenti nell'attuale linguaggio C++.

+1

+1. La parte fondamentale è "la funzionalità appena acquisita per i riferimenti esisteva già sotto forma di puntatori". –

+6

Ci * sono * già 3 tipi. Il tipo possibilmente nullo, non resettabile è 'int * const'. Per coerenza, ci dovrebbe essere 4, in quanto possibilmente nullo e resettabile sono abbastanza ortogonali. – MSalters

+2

Sì, sono ortogonali, ma la lingua non deve fornire ogni combinazione. La domanda è: l'espressività aggiunta supererebbe la complessità aggiunta? C++ è più che abbastanza complesso così com'è, quindi hanno bisogno di un motivo migliore per aggiungere una funzionalità di "non esiste ancora". – jalf

4

Un riferimento non è un puntatore, può essere implementato come un puntatore sullo sfondo, ma il suo concetto di base non è equivalente a un puntatore. Un riferimento dovrebbe essere considerato come lo *is* l'oggetto a cui si riferisce. Pertanto non è possibile cambiarlo e non può essere NULL.

Un puntatore è semplicemente una variabile che contiene un indirizzo di memoria. Il puntatore stesso ha un proprio indirizzo di memoria e all'interno di quell'indirizzo di memoria contiene un altro indirizzo di memoria che si dice indichi. Un riferimento non è lo stesso, non ha un indirizzo proprio e, quindi, non può essere modificato in "mantenere" un altro indirizzo.

Credo che la parashift C++ FAQ on references dice che la cosa migliore:

Nota importante: anche se un riferimento è spesso implementata utilizzando un indirizzo nella sottostante assemblea lingua, si prega di non pensare a un riferimento come un puntatore dall'aspetto divertente a un oggetto. Un riferimento è l'oggetto . Non è un puntatore all'oggetto né una copia dell'oggetto. È l'oggetto.

e ancora in FAQ 8.5:

differenza di un puntatore, una volta riferimento è vincolato ad un oggetto, non si può "ricollocare una scheda" ad un altro oggetto. Il riferimento non è un oggetto (non ha identità , prendendo l'indirizzo di un riferimento fornisce l'indirizzo di il referente, ricordare: il riferimento è il suo referente).

5

Un riferimento resettabile sarebbe funzionalmente identico a un puntatore.

Per quanto riguarda valori Null: non è possibile garantire che un "riferimento reseatable" tale è non-NULL al momento della compilazione, in modo che qualsiasi tale prova avrebbe dovuto avvenire in fase di esecuzione. Si potrebbe ottenere questo voi stessi scrivendo un modello di classe puntatore intelligente-stile che genera un'eccezione quando inizializzato o assegnati NULL:

struct null_pointer_exception { ... }; 

template<typename T> 
struct non_null_pointer { 
    // No default ctor as it could only sensibly produce a NULL pointer 
    non_null_pointer(T* p) : _p(p) { die_if_null(); } 
    non_null_pointer(non_null_pointer const& nnp) : _p(nnp._p) {} 
    non_null_pointer& operator=(T* p) { _p = p; die_if_null(); } 
    non_null_pointer& operator=(non_null_pointer const& nnp) { _p = nnp._p; } 

    T& operator*() { return *_p; } 
    T const& operator*() const { return *_p; } 
    T* operator->() { return _p; } 

    // Allow implicit conversion to T* for convenience 
    operator T*() const { return _p; } 

    // You also need to implement operators for +, -, +=, -=, ++, -- 

private: 
    T* _p; 
    void die_if_null() const { 
     if (!_p) { throw null_pointer_exception(); } 
    } 
}; 

questo potrebbe essere utile in alcune occasioni - una funzione di prendere un parametro non_null_pointer<int> certamente comunica ulteriori informazioni al il chiamante di una funzione che richiede int*.

+4

SBAGLIATO. È banalmente garantito che un riferimento resettabile non è nullo. Quando si ripristina, si prenderà anche un lvalue, proprio come con l'inizializzazione. Per esempio. int foo [0] = {0}; int ref = foo [0]; ref = & = foo [1]/* Riposiziona ref, non assegna a foo [0] */ – MSalters

+1

buona idea farlo con lvalue. haha. –

+1

@MSalters: No, la richiesta di lvalues ​​non garantisce che il riferimento resettabile sia non nullo più di quanto non faccia l'inizializzazione di un riferimento regolare - vedere 8.3.2.4. Es .: "int & r = * static_cast (0);". C'è un mondo di differenza tra "x è proibito" e "x produce un comportamento indefinito". –

1

Non si può fare questo:

int theInt = 0; 
int& refToTheInt = theInt; 

int otherInt = 42; 
refToTheInt = otherInt; 

... per lo stesso motivo per cui secondInt e firstInt non hanno lo stesso valore qui:

int firstInt = 1; 
int secondInt = 2; 
secondInt = firstInt; 
firstInt = 3; 

assert(firstInt != secondInt); 
0

mi sono sempre chiesto il motivo per cui non ha fatto un operatore di assegnazione di riferimento (ad esempio: =) per questo.

Solo per innervosire qualcuno ho scritto del codice per cambiare l'obiettivo di un riferimento in una struttura.

No, non consiglio di ripetere il mio trucco. Si romperà se trasferito su un'architettura sufficientemente diversa.

0

Essere tra il serio: IMHO per renderli poco più diverso da puntatori;) Voi sapete che si può scrivere:

MyClass & c = *new MyClass(); 

se si potrebbe anche in seguito scrivere:

c = *new MyClass("other") 

farebbe senso di avere qualche riferimento a fianco con i puntatori?

MyClass * a = new MyClass(); 
MyClass & b = *new MyClass(); 
a = new MyClass("other"); 
b = *new MyClass("another"); 
0

Il fatto che i riferimenti in C++ non siano annullabili è un effetto collaterale del loro essere solo un alias.

1

Questo non è in realtà una risposta, ma una soluzione per questa limitazione.

In sostanza, quando si tenta di "rebind" un riferimento si sta effettivamente tentando di utilizzare lo stesso nome per fare riferimento a un nuovo valore nel seguente contesto. In C++, questo può essere ottenuto introducendo un ambito di blocco.

Nell'esempio di JALF

int i = 42; 
int k = 43; 
int& j = i; 
//change i, or change j? 
j = k; 

se si desidera modificare i, lo scrivo come sopra. Tuttavia, se si desidera cambiare il significato di j a significare k, si può fare questo:

int i = 42; 
int k = 43; 
int& j = i; 
//change i, or change j? 
//change j! 
{ 
    int& j = k; 
    //do what ever with j's new meaning 
} 
2

Intrestingly, molte risposte qui sono un po 'sfocata o addirittura fuori luogo (ad esempio, non è perché i riferimenti non possono essere zero o simili, infatti, puoi facilmente costruire un esempio in cui un riferimento è zero).

Il motivo reale per cui non è possibile reimpostare un riferimento è piuttosto semplice.

  • puntatori consentono di fare due cose: Per modificare il valore dietro il puntatore (sia attraverso il -> o l'operatore *), e per cambiare il puntatore stesso (assegnazione = diretta). Esempio:

    int a; 
    int * p = &a;
    1. Modifica del valore richiede dereferenziazione: *p = 42;
    2. Modifica del puntatore: p = 0;
  • riferimenti permettono di cambiare solo il valore. Perché? Poiché non esiste altra sintassi per esprimere il re-set. Esempio:

    int a = 10; 
    int b = 20; 
    int & r = a; 
    r = b; // re-set r to b, or set a to 20?

In altre parole, sarebbe ambiguo se si era permesso di re-impostare un riferimento. Rende ancora più senso se il passaggio per riferimento:

void foo(int & r) 
{ 
    int b = 20; 
    r = b; // re-set r to a? or set a to 20? 
} 
void main() 
{ 
    int a = 10; 
    foo(a); 
} 

Speranza che aiuta :-)

0

C'è una soluzione se si vuole una variabile membro che è un punto di riferimento e si vuole essere in grado di rebind. Mentre lo trovo utile e affidabile, si noti che utilizza alcune assunzioni (molto deboli) sul layout di memoria. Sta a te decidere se è nei tuoi standard di codifica.

#include <iostream> 

struct Field_a_t 
{ 
    int& a_; 
    Field_a_t(int& a) 
     : a_(a) {} 
    Field_a_t& operator=(int& a) 
    { 
     // a_.~int(); // do this if you have a non-trivial destructor 
     new(this)Field_a_t(a); 
    } 
}; 

struct MyType : Field_a_t 
{ 
    char c_; 
    MyType(int& a, char c) 
     : Field_a_t(a) 
     , c_(c) {} 
}; 

int main() 
{ 
    int i = 1; 
    int j = 2; 
    MyType x(i, 'x'); 
    std::cout << x.a_; 
    x.a_ = 3; 
    std::cout << i; 
    ((Field_a_t&)x) = j; 
    std::cout << x.a_; 
    x.a_ = 4; 
    std::cout << j; 
} 

questo non è molto efficiente è necessario un tipo separato per ogni campo di riferimento riconfigurabile e classi base fate; inoltre, c'è un debole presupposto che una classe con un singolo tipo di riferimento non avrà un __vfptr o qualsiasi altro campo correlato a type_id che potrebbe potenzialmente distruggere i binding di runtime di MyType. Tutti i compilatori che conosco soddisfano questa condizione (e avrebbe poco senso non farlo).