Perché i riferimenti non sono "const" in C ++?


86

Sappiamo che una "variabile const" indica che una volta assegnata, non è possibile modificare la variabile, in questo modo:

int const i = 1;
i = 2;

Il programma sopra non verrà compilato; gcc richiede un errore:

assignment of read-only variable 'i'

Nessun problema, posso capirlo, ma il seguente esempio va oltre la mia comprensione:

#include<iostream>
using namespace std;
int main()
{
    boolalpha(cout);
    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;
    int const &ri = i;
    cout << is_const<decltype(ri)>::value << endl;
    return 0;
}

Emette

true
false

Strano. Sappiamo che una volta che un riferimento è associato a un nome / variabile, non possiamo cambiare questo legame, cambiamo il suo oggetto associato. Quindi suppongo che il tipo di ridovrebbe essere lo stesso di i: quando iè un int const, perché rinon lo è const?


3
Inoltre, boolalpha(cout)è molto insolito. Potresti fare std::cout << boolalphainvece.
isanae

6
Tanto per riessere un "alias" indistinguibile da i.
Kaz

1
Questo perché un riferimento è sempre limitato allo stesso oggetto. iè anche un riferimento ma per ragioni storiche non lo dichiari come tale in modo esplicito. Quindi iè un riferimento che si riferisce a una memoria ed riè un riferimento che si riferisce alla stessa memoria. Ma non c'è differenza di natura tra i e ri. Poiché non è possibile modificare l'associazione di un riferimento, non è necessario qualificarlo come const. E lasciatemi affermare che il commento di @Kaz è di gran lunga migliore della risposta convalidata (non spiegare mai i riferimenti usando i puntatori, un ref è un nome, un ptr è una variabile).
Jean-Baptiste Yunès

1
Domanda fantastica. Prima di vedere questo esempio, mi sarei aspettato is_constdi tornare anche truein questo caso. A mio parere, questo è un buon esempio del perché constè fondamentalmente al contrario; un attributo "mutabile" (alla Rust mut) sembra che sarebbe più coerente.
Kyle Strand,

1
Forse il titolo dovrebbe essere cambiato in "perché è is_const<int const &>::valuefalso?" o simili; Faccio fatica a vedere un significato alla domanda se non chiedere informazioni sul comportamento dei tratti di tipo
MM

Risposte:


52

Questo può sembrare controintuitivo, ma penso che il modo per capirlo sia rendersi conto che, per certi aspetti, i riferimenti sono trattati sintatticamente come indicatori .

Questo sembra logico per un puntatore :

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Produzione:

true
false

Questo è logico perché sappiamo che non è l' oggetto puntatore che è const (può essere fatto puntare altrove), è l'oggetto a cui si punta.

Quindi vediamo correttamente la costanza del puntatore stesso restituita come false.

Se vogliamo creare il puntatore stesso constdobbiamo dire:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* const ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Produzione:

true
true

E quindi penso che vediamo un'analogia sintattica con il riferimento .

Tuttavia i riferimenti sono semanticamente diversi dai puntatori, specialmente in un aspetto cruciale, non ci è permesso di riassociare un riferimento a un altro oggetto una volta associato.

Quindi, anche se i riferimenti condividono la stessa sintassi dei puntatori, le regole sono diverse e quindi il linguaggio ci impedisce di dichiarare il riferimento stesso in constquesto modo:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const& const ri = i; // COMPILE TIME ERROR!
    cout << is_const<decltype(ri)>::value << endl;
}

Presumo che non ci sia permesso farlo perché non sembra essere necessario quando le regole del linguaggio impediscono il rimbalzo del riferimento nello stesso modo in cui potrebbe fare un puntatore (se non viene dichiarato const).

Quindi per rispondere alla domanda:

D) Perché "riferimento" non è una "const" in C ++?

Nel tuo esempio la sintassi rende la cosa a cui si fa riferimento constnello stesso modo in cui lo farebbe se si dichiarasse un puntatore .

A torto oa ragione non ci è consentito fare il riferimento stesso, constma se lo fossimo sarebbe simile a questo:

int const& const ri = i; // not allowed

D) sappiamo che una volta che un riferimento è associato a un nome / variabile, non possiamo cambiare questo legame, cambiamo il suo oggetto associato. Quindi suppongo che il tipo di ridovrebbe essere lo stesso di i: quando iè a int const, perché rinon lo è const?

Perché il decltype()non viene trasferito all'oggetto a cui è vincolato il riferimento?

Suppongo che questo sia per l'equivalenza semantica con i puntatori e forse anche la funzione di decltype()(tipo dichiarato) è guardare indietro a ciò che è stato dichiarato prima che avvenisse l'associazione.


13
Sembra che tu stia dicendo "i riferimenti non possono essere costanti perché sono sempre costanti"?
ruakh,

2
Può essere vero che " semanticamente tutte le azioni su di loro dopo che sono state legate vengono trasferite all'oggetto a cui si riferiscono", ma l'OP si aspettava che si applicasse decltypeanche a, e ha scoperto che non lo era.
ruakh,

10
Ci sono molte supposizioni tortuose e analogie discutibilmente applicabili ai puntatori qui, che penso confonda la questione più del necessario, quando il controllo dello standard come ha fatto Sonyuanyao arriva direttamente al punto reale: un riferimento non è un tipo qualificabile cv, quindi non può essere const, quindi std::is_constdeve tornare false. Avrebbero invece potuto usare una formulazione che significava che doveva tornare true, ma non l'hanno fatto. Questo è tutto! Tutta questa roba sui puntatori, "presumo", "suppongo", ecc. Non fornisce alcuna reale delucidazione.
underscore_d

1
@underscore_d Questa è probabilmente una domanda migliore per la chat che per la sezione commenti qui, ma hai un esempio di "confusione inutile" da questo modo di pensare ai riferimenti? Se possono essere implementati in questo modo, qual è il problema nel pensarli in questo modo? (Non penso che questa domanda conti perché decltypenon è un'operazione di runtime, quindi l'idea "in fase di esecuzione, i riferimenti si comportano come puntatori", corretta o meno, non si applica realmente.
Kyle Strand

1
I riferimenti non sono nemmeno trattati sintatticamente come puntatori. Hanno una sintassi diversa da dichiarare e da usare. Quindi non sono nemmeno sicuro di cosa intendi.
Jordan Melo

55

perché "ri" non è "const"?

std::is_const controlla se il tipo è qualificato const o meno.

Se T è un tipo qualificato const (ovvero const o const volatile), fornisce il valore della costante del membro uguale a true. Per qualsiasi altro tipo, il valore è falso.

Ma il riferimento non può essere qualificato const. Riferimenti [dcl.ref] / 1

I riferimenti qualificati per cv sono mal formati tranne quando i qualificatori cv vengono introdotti tramite l'uso di un typedef-name ([dcl.typedef], [temp.param]) o decltype-specifier ([dcl.type.simple]) , nel qual caso i qualificatori cv vengono ignorati.

Quindi is_const<decltype(ri)>::valuerestituirà falseperché ri(il riferimento) non è un tipo qualificato const. Come hai detto, non possiamo riassociare un riferimento dopo l'inizializzazione, il che implica che il riferimento è sempre "const", d'altra parte, un riferimento const-qualificato o un riferimento const-unqualified potrebbe non avere senso in realtà.


5
Dritto allo standard e quindi dritto al punto, piuttosto che tutte le supposizioni perplessi della risposta accettata.
underscore_d

5
@underscore_d Questa è una buona risposta fino a quando non ti rendi conto che l'operazione stava chiedendo anche il motivo. "Perché dice così" non è realmente utile a quell'aspetto della domanda.
simpleuser

5
@ user9999999 L'altra risposta non risponde nemmeno a questo aspetto. Se un riferimento non può essere rebound, perché non is_constritorna true? Quella risposta cerca di tracciare un'analogia con il modo in cui i puntatori sono opzionalmente richiudibili, mentre i riferimenti non lo sono e, così facendo, porta all'auto-contraddizione per lo stesso motivo. Non sono sicuro che ci sia una vera spiegazione in entrambi i casi, a parte una decisione un po 'arbitraria da parte di coloro che scrivono lo Standard, e talvolta è il meglio che possiamo sperare. Da qui questa risposta.
underscore_d

2
Un altro aspetto importante, credo, è che nondecltype è una funzione e quindi lavora direttamente sul riferimento stesso piuttosto che sull'oggetto riferito. (Questo è forse più rilevante per la risposta "i riferimenti sono fondamentalmente dei puntatori", ma penso ancora che faccia parte di ciò che rende questo esempio confuso e quindi degno di essere menzionato qui.)
Kyle Strand,

3
Per chiarire ulteriormente il commento di @KyleStrand: decltype(name)agisce in modo diverso da un generale decltype(expr). Quindi, ad esempio, decltype(i)è il tipo dichiarato di icui è const int, mentre decltype((i))sarebbe int const &.
Daniel Schepler,

18

È necessario utilizzare std::remove_referenceper ottenere il valore che stai cercando.

std::cout << std::is_const<std::remove_reference<decltype(ri)>::type>::value << std::endl;

Per ulteriori informazioni, vedere questo post .


7

Perché le macro non lo sono const? Funzioni? Letterali? I nomi dei tipi?

const le cose sono solo un sottoinsieme di cose immutabili.

Poiché i tipi di riferimento sono proprio questo - tipi - potrebbe avere senso richiedere il const-qualifier su tutti per la simmetria con altri tipi (in particolare con i tipi di puntatore), ma questo diventerebbe molto noioso molto rapidamente.

Se C ++ avesse oggetti immutabili per impostazione predefinita, richiedendo la mutableparola chiave su qualcosa che non si desiderava essere const, allora sarebbe stato facile: semplicemente non consentire ai programmatori di aggiungere mutableai tipi di riferimento.

Così com'è, sono immutabili senza qualificazione.

E, poiché non sono constqualificati, sarebbe probabilmente più confuso per is_constun tipo di riferimento restituire true.

Trovo che questo sia un ragionevole compromesso, soprattutto perché l'immutabilità è comunque imposta dal mero fatto che non esiste sintassi per mutare un riferimento.


5

Questa è una stranezza / funzionalità in C ++. Sebbene non pensiamo ai riferimenti come tipi, in realtà "siedono" nel sistema dei tipi. Sebbene questo sembri imbarazzante (dato che quando vengono utilizzati i riferimenti, la semantica di riferimento si verifica automaticamente e il riferimento "si allontana"), ci sono alcune ragioni difendibili per cui i riferimenti sono modellati nel sistema dei tipi invece che come un attributo separato al di fuori di genere.

In primo luogo, consideriamo che non tutti gli attributi di un nome dichiarato devono essere nel sistema dei tipi. Dal linguaggio C, abbiamo "classe di archiviazione" e "collegamento". Un nome può essere introdotto come extern const int ri, dove externindica la classe di memoria statica e la presenza di collegamento. Il tipo è giusto const int.

C ++ ovviamente abbraccia l'idea che le espressioni abbiano attributi che sono al di fuori del sistema di tipi. Il linguaggio ora ha un concetto di "classe di valore" che è un tentativo di organizzare il numero crescente di attributi non di tipo che un'espressione può esibire.

Eppure i riferimenti sono tipi. Perché?

Nelle esercitazioni C ++ veniva spiegato che una dichiarazione come const int &riintroduceva ricome tipo const int, ma faceva riferimento alla semantica. Quella semantica di riferimento non era un tipo; era semplicemente una specie di attributo che indicava una relazione insolita tra il nome e la posizione di archiviazione. Inoltre, il fatto che i riferimenti non siano tipi è stato utilizzato per razionalizzare il motivo per cui non è possibile costruire tipi basati sui riferimenti, anche se la sintassi di costruzione del tipo lo consente. Ad esempio, array o puntatori a riferimenti non sono possibili: const int &ari[5]e const int &*pri.

Ma in realtà i riferimenti sono tipi e quindi decltype(ri)recupera alcuni nodi del tipo di riferimento che non sono qualificati. È necessario scendere oltre questo nodo nell'albero dei tipi per raggiungere il tipo sottostante con remove_reference.

Quando si utilizza ri, il riferimento viene risolto in modo trasparente, in modo che ri"sembra e si sente come i" e può essere chiamato un "alias" per esso. Nel sistema dei tipi, però, riha in effetti un tipo che è " riferimento a const int ".

Perché sono tipi di referenze?

Considera che se i riferimenti non fossero tipi, allora queste funzioni sarebbero considerate avere lo stesso tipo:

void foo(int);
void foo(int &);

Questo semplicemente non può essere per ragioni che sono abbastanza evidenti. Se avessero lo stesso tipo, ciò significa che entrambe le dichiarazioni sarebbero adatte per entrambe le definizioni, quindi si (int)dovrebbe sospettare che ogni funzione prenda un riferimento.

Allo stesso modo, se i riferimenti non fossero tipi, queste due dichiarazioni di classe sarebbero equivalenti:

class foo {
  int m;
};

class foo {
  int &m;
};

Sarebbe corretto che un'unità di traduzione utilizzi una dichiarazione e un'altra unità di traduzione nello stesso programma utilizzi l'altra dichiarazione.

Il fatto è che un riferimento implica una differenza nell'implementazione ed è impossibile separarlo dal tipo, perché il tipo in C ++ ha a che fare con l'implementazione di un'entità: il suo "layout" in bit per così dire. Se due funzioni hanno lo stesso tipo, possono essere invocate con le stesse convenzioni di chiamata binaria: l'ABI è lo stesso. Se due strutture o classi hanno lo stesso tipo, il loro layout è lo stesso così come la semantica di accesso a tutti i membri. La presenza di riferimenti modifica questi aspetti dei tipi, quindi è una decisione progettuale semplice incorporarli nel sistema di tipi. (Tuttavia, nota un controargomentazione qui: può essere un membro della struttura / classe static, che cambia anche la rappresentazione; ma non è tipo!)

Pertanto, i riferimenti sono nel sistema dei tipi come "cittadini di seconda classe" (non diversamente dalle funzioni e dagli array in ISO C). Ci sono alcune cose che non possiamo "fare" con i riferimenti, come dichiarare puntatori a riferimenti o array di essi. Ma questo non significa che non siano tipi. Semplicemente non sono tipi in un modo che abbia senso.

Non tutte queste restrizioni di seconda classe sono essenziali. Dato che ci sono strutture di riferimenti, potrebbero esserci matrici di riferimenti! Per esempio

// fantasy syntax
int x = 0, y = 0;
int &ar[2] = { x, y };

// ar[0] is now an alias for x: could be useful!

Questo non è implementato in C ++, tutto qui. I puntatori ai riferimenti non hanno alcun senso, tuttavia, perché un puntatore sollevato da un riferimento va semplicemente all'oggetto referenziato. La probabile ragione per cui non ci sono array di riferimenti è che le persone C ++ considerano gli array come una sorta di funzionalità di basso livello ereditata da C che è rotta in molti modi che sono irreparabili e non vogliono toccare gli array come il base per qualcosa di nuovo. L'esistenza di matrici di riferimenti, tuttavia, sarebbe un chiaro esempio di come i riferimenti devono essere tipi.

constTipi non qualificabili: presenti anche in ISO C90!

Alcune risposte suggeriscono che i riferimenti non richiedono un constqualificatore. Questa è piuttosto una falsa pista , perché la dichiarazione const int &ri = inon sta nemmeno tentando di fare un constriferimento -qualified: è un riferimento a un tipo qualificato const (che di per sé non lo è const). Proprio come const in *ridichiara un puntatore a qualcosa const, ma quel puntatore di per sé non lo è const.

Detto questo, è vero che i riferimenti non possono portare il constqualificatore da soli.

Tuttavia, questo non è così bizzarro. Anche nel linguaggio ISO C 90, non tutti i tipi possono essere const. Vale a dire, gli array non possono essere.

In primo luogo, la sintassi non esiste per dichiarare un array const: int a const [42]è errata.

Tuttavia, ciò che la dichiarazione di cui sopra sta cercando di fare può essere espresso tramite un intermedio typedef:

typedef int array_t[42];
const array_t a;

Ma questo non fa quello che sembra. In questa dichiarazione, non è ciò ache viene constqualificato, ma gli elementi! Vale a dire, a[0]è un const int, ma aè solo un "array di int". Di conseguenza, questo non richiede una diagnosi:

int *p = a; /* surprise! */

Questo fa:

a[0] = 1;

Ancora una volta, questo sottolinea l'idea che i riferimenti sono in un certo senso "seconda classe" nel sistema di tipi, come gli array.

Si noti come l'analogia sia ancora più profonda, poiché gli array hanno anche un "comportamento di conversione invisibile", come i riferimenti. Senza che il programmatore debba utilizzare alcun operatore esplicito, l'identificatore asi trasforma automaticamente in un int *puntatore, come se l'espressione &a[0]fosse stata utilizzata. Questo è analogo a come un riferimento ri, quando lo usiamo come espressione primaria, denota magicamente l'oggetto ia cui è legato. È solo un altro "decadimento" come il "decadimento da matrice a puntatore".

E proprio come non dobbiamo farci confondere dal decadimento "da array a puntatore" facendoci pensare erroneamente che "gli array sono solo puntatori in C e C ++", allo stesso modo non dobbiamo pensare che i riferimenti siano solo alias che non hanno un tipo proprio.

Quando decltype(ri)sopprime la normale conversione del riferimento al suo oggetto referente, non è così diverso dal sizeof asopprimere la conversione da matrice a puntatore e dall'operare sul tipo di matrice stesso per calcolarne le dimensioni.


Hai molte informazioni interessanti e utili qui (+1), ma è più o meno limitato al fatto che "i riferimenti sono nel sistema dei tipi" - questo non risponde completamente alla domanda di OP. Tra questa risposta e la risposta di @ songyuanyao, direi che ci sono informazioni più che sufficienti per capire la situazione, ma sembra lo sfondo necessario per una risposta piuttosto che una risposta completa.
Kyle Strand,

1
Inoltre, penso che valga la pena sottolineare questa frase: "Quando usi ri, il riferimento è risolto in modo trasparente ..." Un punto chiave (che ho menzionato nei commenti ma che finora non è stato mostrato in nessuna delle risposte ) è che decltype non esegue questa risoluzione trasparente (non è una funzione, quindi rinon è "usata" nel senso che descrivi). Questo si adatta molto bene con tutta la tua attenzione al sistema dei tipi : la loro connessione chiave è che decltypeè un'operazione del sistema dei tipi .
Kyle Strand

Hmm, le cose sugli array sembrano una tangente, ma la frase finale è utile.
Kyle Strand

..... anche se a questo punto ti ho già votato e non sono l'OP, quindi non stai davvero guadagnando o perdendo nulla ascoltando le mie critiche ...
Kyle Strand

Ovviamente una risposta semplice (e corretta) ci indica semplicemente lo standard, ma mi piace il pensiero di sfondo qui anche se alcuni potrebbero sostenere che parti di esso sono supposizioni. +1.
Steve Kidd

-5

const X & x "significa x alias di un oggetto X, ma non puoi cambiare quell'oggetto X tramite x.

E vedi std :: is_const .


questo non tenta nemmeno di rispondere alla domanda.
bolov
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.