c++ pass - Quando utilizzare riferimenti o puntatori




array by (17)

Comprendo la sintassi e la semantica generale dei puntatori rispetto ai riferimenti, ma come dovrei decidere quando è più o meno appropriato utilizzare riferimenti o puntatori in un'API?

Naturalmente alcune situazioni richiedono l'una o l'altra (l' operator++ bisogno di un argomento di riferimento), ma in generale sto trovando che preferisco usare puntatori (e puntatori const) poiché la sintassi è chiara che le variabili vengono passate in modo distruttivo.

Ad esempio nel seguente codice:

void add_one(int& n) { n += 1; }
void add_one(int* const n) { *n += 1; }
int main() {
  int a = 0;
  add_one(a); // Not clear that a may be modified
  add_one(&a); // 'a' is clearly being passed destructively
}

Con il puntatore, è sempre (più) ovvio cosa sta succedendo, quindi per le API e simili dove la chiarezza è una grande preoccupazione sono i puntatori non più appropriati dei riferimenti? Ciò significa che i riferimenti dovrebbero essere usati solo quando necessario (ad es. operator++ )? Ci sono problemi di prestazioni con uno o l'altro?

MODIFICA (AGGIORNATO):

Oltre a consentire i valori NULL e la gestione degli array raw, sembra che la scelta dipenda dalle preferenze personali. Ho accettato la risposta qui sotto che fa riferimento alla Guida allo stile C ++ di Google , in quanto presentano la vista secondo cui "I riferimenti possono essere fonte di confusione, poiché hanno una sintassi del valore ma la semantica del puntatore.".

A causa del lavoro aggiuntivo necessario per disinfettare gli argomenti del puntatore che non dovrebbero essere NULL (ad esempio add_one(0) chiamerà la versione del puntatore e si interromperà durante il runtime), ha senso dal punto di vista della manutenibilità usare i riferimenti dove un oggetto DEVE essere presente, sebbene è un peccato perdere la chiarezza sintattica.


Answers

La mia regola generale è:

  • Utilizzare i puntatori per i parametri di uscita o di entrata / uscita. Quindi si può vedere che il valore sarà cambiato. (Devi usare & )
  • Utilizzare i puntatori se il parametro NULL è un valore accettabile. (Assicurati che sia const se è un parametro in entrata)
  • Utilizzare i riferimenti per il parametro in entrata se non può essere NULL e non è un tipo primitivo ( const T& ).
  • Usa puntatori o puntatori intelligenti quando restituisci un oggetto appena creato.
  • Usa puntatori o puntatori intelligenti come struct o membri della classe anziché riferimenti.
  • Utilizza i riferimenti per l'aliasing (ad esempio int &current = someArray[i] )

Indipendentemente da quale utilizzi, non dimenticare di documentare le tue funzioni e il significato dei loro parametri se non sono ovvi.


Preferisco usare i puntatori. Almeno è chiaro cosa stai facendo. Ho la sensazione che i riferimenti siano usati principalmente a causa di STL e delle sue implicazioni sulla sintassi sul codice. A causa di ciò anche tante novità della libreria standard C ++ come std :: move ..... per ottenere esattamente ciò che si desidera, e non ciò a cui si sarebbe pensato in modo intuitivo.


C'è un problema con la regola " usa riferimenti ove possibile " e si verifica se si desidera mantenere un riferimento per un ulteriore utilizzo. Per illustrare questo con l'esempio, immagina di avere le seguenti lezioni.

class SimCard
{
    public:
        explicit SimCard(int id):
            m_id(id)
        {
        }

        int getId() const
        {
            return m_id;
        }

    private:
        int m_id;
};

class RefPhone
{
    public:
        explicit RefPhone(const SimCard & card):
            m_card(card)
        {
        }

        int getSimId()
        {
            return m_card.getId();
        }

    private:
        const SimCard & m_card;
};

All'inizio può sembrare una buona idea avere un parametro nel RefPhone(const SimCard & card) passato da un riferimento, poiché impedisce di passare i puntatori errati / nulli al costruttore. In qualche modo incoraggia l'allocazione delle variabili in pila e traendo beneficio da RAII.

PtrPhone nullPhone(0);  //this will not happen that easily
SimCard * cardPtr = new SimCard(666);  //evil pointer
delete cardPtr;  //muahaha
PtrPhone uninitPhone(cardPtr);  //this will not happen that easily

Ma poi i temporari vengono a distruggere il tuo mondo felice.

RefPhone tempPhone(SimCard(666));   //evil temporary
//function referring to destroyed object
tempPhone.getSimId();    //this can happen

Quindi, se ci si attiene ciecamente ai riferimenti, si scambia la possibilità di passare dei puntatori non validi per la possibilità di memorizzare riferimenti a oggetti distrutti, che ha praticamente lo stesso effetto.

modifica: nota che ho aderito alla regola "Utilizza riferimenti ovunque sia possibile, puntatori ovunque sia necessario. Evita i puntatori finché non puoi". dalla risposta più upvoted e accettata (altre risposte suggeriscono anche così). Anche se dovrebbe essere ovvio, l'esempio non è mostrare che i riferimenti in quanto tali sono cattivi. Tuttavia possono essere utilizzati in modo improprio, proprio come i puntatori e possono portare le loro minacce al codice.

Ci sono seguenti differenze tra puntatori e riferimenti.

  1. Quando si tratta di passare le variabili, passare per riferimento sembra passare per valore, ma ha una semantica del puntatore (si comporta come un puntatore).
  2. Il riferimento non può essere inizializzato direttamente su 0 (null).
  3. Riferimento (riferimento, oggetto non referenziato) non può essere modificato (equivalente al puntatore "* const").
  4. il riferimento const può accettare parametri temporanei.
  5. I riferimenti const locali prolungano la durata di oggetti temporanei

Tenendo conto di queste mie regole attuali sono le seguenti.

  • Utilizzare i riferimenti per i parametri che verranno utilizzati localmente nell'ambito di una funzione.
  • Utilizzare i puntatori quando 0 (null) è un valore di parametro accettabile o è necessario memorizzare i parametri per un ulteriore utilizzo. Se 0 (null) è accettabile, aggiungo il suffisso "_n" al parametro, uso il puntatore protetto (come QPointer in Qt) o semplicemente documentarlo. Puoi anche usare puntatori intelligenti. Devi essere ancora più attento con i puntatori condivisi rispetto ai normali puntatori (altrimenti potresti finire con perdite di memoria di progettazione e problemi di responsabilità).

Usa la referenza dovunque puoi, i puntatori ovunque tu sia.

Evita i puntatori finché non puoi.

Il motivo è che i puntatori rendono le cose più difficili da seguire / leggere, meno sicure e manipolazioni molto più pericolose di qualsiasi altro costrutto.

Quindi la regola generale è usare i puntatori solo se non c'è altra scelta.

Ad esempio, restituire un puntatore a un oggetto è un'opzione valida quando la funzione può restituire nullptr in alcuni casi e si presume che lo farà. Detto questo, un'opzione migliore sarebbe quella di utilizzare qualcosa di simile a boost::optional .

Un altro esempio è utilizzare i puntatori alla memoria grezza per manipolazioni specifiche della memoria. Questo dovrebbe essere nascosto e localizzato in parti molto strette del codice, per aiutare a limitare le parti pericolose dell'intero codice base.

Nel tuo esempio, non ha senso usare un puntatore come argomento perché:

  1. se fornisci nullptr come argomento, stai andando in undefined-behavior-land;
  2. la versione dell'attributo di riferimento non consente (senza trucchi facili da individuare) il problema con 1.
  3. la versione dell'attributo di riferimento è più semplice da comprendere per l'utente: devi fornire un oggetto valido, non qualcosa che potrebbe essere nullo.

Se il comportamento della funzione dovrebbe funzionare con o senza un dato oggetto, allora usare un puntatore come attributo suggerisce che è possibile passare nullptr come argomento e che va bene per la funzione. È una specie di contratto tra l'utente e l'implementazione.


In generale una variabile membro non dovrebbe mai essere un riferimento perché non ha senso in questo. Fa sì che la classe non sia assegnabile se non si fornisce un operatore di assegnazione. Inoltre, una volta impostato il riferimento del membro per fare riferimento ad un oggetto, non è possibile cambiarlo per fare riferimento a un altro oggetto. L'utilizzo più appropriato di un riferimento viene utilizzato come parametro di funzione che abilita il passaggio per riferimento.


Utilizzare i riferimenti come ultima risorsa. Assegna un'istanza nello stack o nell'heap, usali.

Utilizzare i riferimenti per l'ambito dei parametri per ottenere il minimo impatto. Se utilizzi il riferimento perché i puntatori sono troppo difficili per te, quindi passa a un'altra lingua.


Le seguenti sono alcune linee guida.

Una funzione utilizza i dati passati senza modificarli:

  1. Se l'oggetto dati è piccolo, ad esempio un tipo di dati incorporato o una piccola struttura, passarlo per valore.

  2. Se l'oggetto dati è un array, usa un puntatore perché è la tua unica scelta. Rendi il puntatore un puntatore a const.

  3. Se l'oggetto dati è una struttura di buone dimensioni, utilizzare un puntatore const o un riferimento const per aumentare l'efficienza del programma. Si risparmia il tempo e lo spazio necessari per copiare una struttura o un progetto di classe. Crea il puntatore o il riferimento const.

  4. Se l'oggetto dati è un oggetto di classe, usa un riferimento const. La semantica del design di classe spesso richiede l'uso di un riferimento, che è il motivo principale per cui C ++ ha aggiunto questa funzione. Quindi, il metodo standard per passare gli argomenti oggetto di classe è per riferimento.

Una funzione modifica i dati nella funzione di chiamata:

1.Se l'oggetto dati è un tipo di dati incorporato, utilizzare un puntatore. Se si individuano codice come fixit (& x), dove x è un int, è abbastanza chiaro che questa funzione intende modificare x.

2.Se l'oggetto dati è un array, utilizzare l'unica opzione: un puntatore.

3.Se l'oggetto dati è una struttura, utilizzare un riferimento o un puntatore.

4.Se l'oggetto dati è un oggetto di classe, utilizzare un riferimento.

Naturalmente, queste sono solo linee guida e potrebbero esserci motivi per fare scelte diverse. Ad esempio, cin usa i riferimenti per i tipi di base in modo da poter usare cin >> n invece di cin >> & n.


Copiato da wiki -

Una conseguenza di ciò è che in molte implementazioni, il funzionamento su una variabile con vita automatica o statica attraverso un riferimento, sebbene sintatticamente simile all'accesso diretto, può implicare operazioni di dereferenziazione nascoste che sono costose. I riferimenti sono una caratteristica sintatticamente controverso del C ++ perché oscurano il livello di indizio di un identificatore; Cioè, a differenza del codice C in cui i puntatori di solito risalgono sintatticamente, in un grande blocco di codice C ++ potrebbe non essere immediatamente ovvio se l'oggetto a cui si accede è definito come variabile locale o globale o se si tratta di un riferimento (puntatore implicito) a qualche altra posizione, specialmente se il codice mescola riferimenti e puntatori. Questo aspetto può rendere più difficile leggere ed eseguire il debug del codice C ++ scritto male (vedi Aliasing).

Sono d'accordo al 100% con questo, ed è per questo che credo che dovresti usare un riferimento solo quando hai una buona ragione per farlo.


Disclaimer: a parte il fatto che i riferimenti non possono essere NULL né "rimbalzo" (il che significa che non possono cambiare l'oggetto di cui sono l'alias), in realtà è una questione di gusti, quindi non ho intenzione di dire "questo è meglio".

Detto questo, non sono d'accordo con la tua ultima affermazione nel post, in quanto non credo che il codice perda chiarezza con i riferimenti. Nel tuo esempio,

add_one(&a);

potrebbe essere più chiaro di

add_one(a);

poiché sai che molto probabilmente il valore di a cambierà. D'altra parte, però, la firma della funzione

void add_one(int* const n);

in qualche modo non è chiaro: n sarà un singolo intero o un array? A volte hai accesso solo a intestazioni (scarsamente documentate) ea firme simili

foo(int* const a, int b);

non sono facili da interpretare a prima vista.

Imho, i riferimenti sono buoni come puntatori quando non è necessario (ri) allocare né ribattere (nel senso spiegato prima). Inoltre, se uno sviluppatore utilizza solo puntatori per gli array, le firme delle funzioni sono in qualche modo meno ambigue. Per non parlare del fatto che la sintassi degli operatori è molto più leggibile con i riferimenti.


Sto solo inserendo la mia moneta. Ho appena fatto un test. Uno sneeky a quello. Ho solo permesso a g ++ di creare i file assembly dello stesso mini-programma usando i puntatori rispetto all'utilizzo dei riferimenti. Guardando l'output sono esattamente gli stessi. Altro che il symbolnaming. Quindi guardando le prestazioni (in un semplice esempio) non ci sono problemi.

Ora sul tema dei puntatori vs riferimenti. IMHO Penso che la chiarezza sia soprattutto. Non appena ho letto un comportamento implicito le dita dei piedi iniziano a raggomitolarsi. Sono d'accordo che è un comportamento implicito bello che un riferimento non può essere NULL.

Dereferenziare un puntatore NULL non è un problema. bloccherà la tua applicazione e sarà facile eseguire il debug. Un problema più grande sono i puntatori non inizializzati contenenti valori non validi. Ciò probabilmente causerà il danneggiamento della memoria causando un comportamento indefinito senza un'origine chiara.

Questo è dove penso che i riferimenti siano molto più sicuri dei puntatori. E sono d'accordo con una precedente affermazione, che l'interfaccia (che dovrebbe essere chiaramente documentata, vedi design per contratto, Bertrand Meyer) definisce il risultato dei parametri in una funzione. Prendendo ora tutto questo in considerazione, le mie preferenze vanno ad usare riferimenti ovunque / quando possibile.


I riferimenti sono più puliti e più facili da usare e fanno un lavoro migliore per nascondere le informazioni. I riferimenti non possono essere riassegnati, tuttavia. Se è necessario puntare prima su un oggetto e poi su un altro, è necessario utilizzare un puntatore. I riferimenti non possono essere nulli, quindi se esiste qualche possibilità che l'oggetto in questione possa essere nullo, non è necessario utilizzare un riferimento. Devi usare un puntatore. Se vuoi gestire la manipolazione degli oggetti da solo, cioè se vuoi allocare spazio di memoria per un oggetto sull'Heap piuttosto sullo Stack, devi usare Puntatore

int *pInt = new int; // allocates *pInt on the Heap

Come gli altri hanno già risposto: usa sempre riferimenti, a meno che la variabile NULL / nullptr sia davvero uno stato valido.

Il punto di vista di John Carmack sull'argomento è simile:

I puntatori NULL sono il più grande problema in C / C ++, almeno nel nostro codice. Il duplice uso di un singolo valore sia come bandiera che come indirizzo provoca un numero incredibile di problemi fatali. I riferimenti al C ++ dovrebbero essere favoriti su puntatori quando possibile; mentre un riferimento è "veramente" solo un puntatore, ha il contratto implicito di non essere NULL. Esegui controlli NULL quando i puntatori vengono trasformati in riferimenti, quindi puoi ignorare il problema in seguito.

http://www.altdevblogaday.com/2011/12/24/static-code-analysis/

Modifica 2012-03-13

L'utente Bret Kuhns giustamente osserva:

Lo standard C ++ 11 è stato finalizzato. Penso che sia il momento in questo thread per dire che la maggior parte del codice dovrebbe fare perfettamente bene con una combinazione di riferimenti, shared_ptr e unique_ptr.

Abbastanza vero, ma la domanda rimane ancora, anche quando si sostituiscono puntatori grezzi con puntatori intelligenti.

Ad esempio, sia std::unique_ptr che std::shared_ptr possono essere costruiti come puntatori "vuoti" tramite il loro costruttore predefinito:

... il che significa che usarli senza verificare che non siano vuoti rischia di schiantarsi, il che è esattamente ciò che riguarda la discussione di J. Carmack.

E poi, abbiamo il divertente problema di "come si passa un puntatore intelligente come parametro di funzione?"

La answer Jon alla domanda C ++ - il passaggio dei riferimenti a boost :: shared_ptr , ei seguenti commenti mostrano che anche in quel caso, passare un puntatore intelligente per copia o per riferimento non è così netto come si vorrebbe (mi favorisco il " per riferimento "di default, ma potrei sbagliarmi).


Per i puntatori, hai bisogno che puntino a qualcosa, quindi i puntatori costano spazio nella memoria.

Ad esempio una funzione che accetta un puntatore intero non prenderà la variabile intera. Quindi dovrai prima creare un puntatore per passare alla funzione.

Per quanto riguarda un riferimento, non costerà la memoria. Hai una variabile intera e puoi passarla come variabile di riferimento. Questo è tutto. Non è necessario creare una variabile di riferimento appositamente per questo.


Le prestazioni sono esattamente le stesse, poiché i riferimenti sono implementati internamente come indicatori. Quindi non devi preoccuparti di questo.

Non esiste una convenzione generalmente accettata in merito a quando utilizzare riferimenti e puntatori. In alcuni casi devi restituire o accettare riferimenti (ad esempio, costruttore di copie), ma a parte questo sei libero di fare ciò che desideri. Una convenzione piuttosto comune che ho incontrato è quella di usare riferimenti quando il parametro deve riferirsi a un oggetto esistente e puntatori quando un valore NULL è ok.

Alcune convenzioni di codifica (come quelle di Google's ) prescrivono che si dovrebbero sempre usare puntatori o riferimenti const, perché i riferimenti hanno un po 'di sintassi poco chiara: hanno un comportamento di riferimento ma una sintassi del valore.


Qualsiasi differenza di prestazioni sarebbe così piccola da non giustificare l'utilizzo di un approccio meno chiaro.

Innanzitutto, un caso che non è stato menzionato in cui i riferimenti sono generalmente superiori sono i riferimenti const . Per i tipi non semplici, il passaggio di un const reference evita di creare un elemento temporaneo e non causa la confusione di cui si è preoccupati (poiché il valore non viene modificato). Qui, forzare una persona a passare un puntatore causa la stessa confusione di cui sei preoccupato, poiché vedere l'indirizzo preso e passato a una funzione potrebbe farti pensare che il valore sia cambiato.

In ogni caso, sostanzialmente sono d'accordo con te. Non mi piacciono le funzioni che prendono riferimenti per modificare il loro valore quando non è molto ovvio che questo è ciò che sta facendo la funzione. Anch'io preferisco usare i puntatori in quel caso.

Quando hai bisogno di restituire un valore in un tipo complesso, tendo a preferire i riferimenti. Per esempio:

bool GetFooArray(array &foo); // my preference
bool GetFooArray(array *foo); // alternative

Qui, il nome della funzione rende chiaro che stai ricevendo informazioni in un array. Quindi non c'è confusione.

I principali vantaggi dei riferimenti sono che contengono sempre un valore valido, sono più puliti dei puntatori e supportano il polimorfismo senza richiedere alcuna sintassi aggiuntiva. Se nessuno di questi vantaggi si applica, non vi è alcun motivo per preferire un riferimento su un puntatore.


Non è una questione di gusti. Ecco alcune regole definitive.

Se si desidera fare riferimento a una variabile dichiarata staticamente nell'ambito in cui è stata dichiarata, utilizzare un riferimento C ++ e sarà perfettamente sicuro. Lo stesso vale per un puntatore intelligente dichiarato staticamente. Il passaggio dei parametri per riferimento è un esempio di questo utilizzo.

Se si desidera fare riferimento a qualsiasi cosa proveniente da un ambito che è più ampio dell'ambito in cui è stato dichiarato, è necessario utilizzare un puntatore intelligente conteggiato di riferimento perché sia ​​perfettamente sicuro.

È possibile fare riferimento a un elemento di una raccolta con un riferimento per comodità sintattica, ma non è sicuro; l'elemento può essere cancellato in qualsiasi momento.

Per conservare in sicurezza un riferimento a un elemento di una raccolta, è necessario utilizzare un puntatore intelligente conteggiato di riferimento.


Indipendentemente da come siamo arrivati ​​qui, penso che sia una fortuna che questo sia un puntatore e non un riferimento in quanto ciò aiuta a "dare un senso" che è possibile eliminarlo :

void A::f () {
  delete &this;
}

Penso che questo sia un caso in cui senza necessariamente essere di progettazione, il C ++ è meglio così com'è.





c++ pointers reference