Devo preferire puntatori o riferimenti nei dati dei membri?


138

Questo è un esempio semplificato per illustrare la domanda:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Quindi B è responsabile dell'aggiornamento di una parte di C. Ho eseguito il codice tramite lint e si è lamentato del membro di riferimento: lint # 1725 . Si tratta di occuparsi della copia predefinita e delle assegnazioni, il che è abbastanza equo, ma anche la copia predefinita e l'assegnazione sono cattive con i puntatori, quindi c'è poco vantaggio lì.

Cerco sempre di utilizzare i riferimenti dove posso poiché i puntatori nudi introducono in modo incerto su chi è responsabile dell'eliminazione di quel puntatore. Preferisco incorporare gli oggetti in base al valore, ma se ho bisogno di un puntatore, utilizzo auto_ptr nei dati del membro della classe che possiede il puntatore e passo l'oggetto come riferimento.

In genere utilizzerei un puntatore nei dati dei membri solo quando il puntatore potrebbe essere nullo o potrebbe cambiare. Ci sono altri motivi per preferire i puntatori ai riferimenti per i membri dei dati?

È vero dire che un oggetto contenente un riferimento non dovrebbe essere assegnabile, poiché un riferimento non dovrebbe essere cambiato una volta inizializzato?


"ma anche la copia e l'assegnazione predefinite sono dannose per i puntatori": non è la stessa cosa: un puntatore può essere cambiato ogni volta che vuoi se non è const. Un riferimento è normalmente sempre const! (Vedrai questo se provi a cambiare il tuo membro in "A & const a;". Il compilatore (almeno GCC) ti avvertirà comunque che il riferimento è const anche senza la parola chiave "const".
mmmmmmmm

Il problema principale è che se qualcuno fa B b (A ()), sei fregato perché stai finendo con un riferimento penzolante.
log0,

Risposte:


67

Evita i membri di riferimento, perché limitano ciò che può fare l'implementazione di una classe (incluso, come dici tu, impedendo l'implementazione di un operatore di assegnazione) e non offrono alcun beneficio a ciò che la classe può fornire.

Problemi di esempio:

  • sei costretto a inizializzare il riferimento nell'elenco di inizializzatori di ogni costruttore: non c'è modo di fattorizzare questa inizializzazione in un'altra funzione ( fino a C ++ 0x, comunque modifica: C ++ ora ha delegatori di costruttori )
  • il riferimento non può essere rimbalzato o essere nullo. Questo può essere un vantaggio, ma se il codice deve mai essere modificato per consentire il rebinding o per rendere nullo il membro, tutti gli usi del membro devono essere modificati
  • a differenza dei membri puntatore, i riferimenti non possono essere facilmente sostituiti da puntatori intelligenti o iteratori come potrebbe richiedere il refactoring
  • Ogni volta che viene utilizzato un riferimento sembra un tipo di valore ( .operatore ecc.), Ma si comporta come un puntatore (può penzolare) - quindi ad esempio la Guida di stile di Google lo scoraggia

66
Tutte le cose che hai menzionato sono buone cose da evitare, quindi se i riferimenti aiutano in questo - sono buoni e non cattivi. L'elenco di inizializzazione è il posto migliore per l'inizializzazione dei dati. Molto spesso devi nascondere l'operatore di assegnazione, con riferimenti che non devi. "Impossibile rimbalzare" - bene, il riutilizzo delle variabili è una cattiva idea.
Mykola Golubyev,

4
@Mykola: sono d'accordo con te. Preferisco che i membri vengano inizializzati negli elenchi di inizializzatori, evito i puntatori null e certamente non è buono per una variabile cambiarne il significato. Dove le nostre opinioni differiscono è che non ho bisogno o voglio che il compilatore lo imponga per me. Non rende le classi più facili da scrivere, non ho riscontrato bug in quest'area che potrebbe catturare e ogni tanto apprezzo la flessibilità che ottengo dal codice che utilizza costantemente membri del puntatore (puntatori intelligenti dove appropriato).
James Hopkin,

Penso che non dover nascondere l'assegnazione sia un argomento falso poiché non è necessario nascondere l'assegnazione se la classe contiene un puntatore che non è responsabile dell'eliminazione in ogni caso - il default lo farà. Penso che questa sia la risposta migliore perché fornisce buone ragioni per cui i puntatori dovrebbero essere preferiti ai riferimenti nei dati dei membri.
markh44,

4
James, non è che rende il codice più facile da scrivere, è che rende il codice più facile da leggere. Con un riferimento come membro di dati non devi mai chiederti se può essere nullo nel punto di utilizzo. Ciò significa che puoi guardare il codice con meno contesto richiesto.
Len Holgate,

4
Rende i test unitari e le derisioni molto più difficili, impossibili quasi a seconda di quanti ne vengono utilizzati, combinati con l'esplosione del grafico degli affetti, sono motivi che convincono IMHO a non usare mai.
Chris Huang-Leaver,

156

La mia regola empirica:

  • Usa un membro di riferimento quando vuoi che la vita del tuo oggetto dipenda dalla vita di altri oggetti : è un modo esplicito per dire che non permetti che l'oggetto sia vivo senza un'istanza valida di un'altra classe - perché no assegnazione e obbligo di ottenere l'inizializzazione dei riferimenti tramite il costruttore. È un buon modo per progettare la tua classe senza supporre che la sua istanza sia membro o no di un'altra classe. Supponi solo che le loro vite siano direttamente collegate ad altri casi. Ti permette di cambiare in seguito come usi la tua istanza di classe (con new, come istanza locale, come membro di classe, generato da un pool di memoria in un gestore, ecc.)
  • Usa il puntatore in altri casi : quando vuoi che il membro venga cambiato in seguito, usa un puntatore o un puntatore const per leggere solo l'istanza puntata. Se si suppone che quel tipo sia copiabile, non è possibile utilizzare comunque i riferimenti. A volte è anche necessario inizializzare il membro dopo una chiamata di funzione speciale (init () per esempio) e quindi semplicemente non si ha altra scelta che usare un puntatore. MA: usa gli asserti in tutte le tue funzioni membro per rilevare rapidamente lo stato del puntatore sbagliato!
  • Nei casi in cui si desidera che la durata dell'oggetto dipenda dalla durata di un oggetto esterno e che sia necessario copiare quel tipo, utilizzare quindi i puntatori ma argomento di riferimento nel costruttore In questo modo si indica sulla costruzione che la durata di questo oggetto dipende nel corso della vita dell'argomento MA l'implementazione usa i puntatori per essere ancora copiabili. Fintanto che questi membri vengono modificati solo tramite copia e il tuo tipo non ha un costruttore predefinito, il tipo dovrebbe raggiungere tutti e due gli obiettivi.

1
Penso che tu e Mykola siano risposte corrette e promuovano la programmazione senza puntatore (leggi meno puntatore).
Amit

37

Gli oggetti raramente dovrebbero consentire di assegnare e altre cose come il confronto. Se si considera un modello di business con oggetti come "Reparto", "Dipendente", "Direttore", è difficile immaginare un caso in cui un dipendente verrà assegnato ad un altro.

Quindi, per gli oggetti business è molto buono descrivere relazioni uno a uno e uno a molti come riferimenti e non puntatori.

E probabilmente è OK descrivere la relazione uno o zero come un puntatore.

Quindi nessun fattore "non possiamo assegnare", quindi.
Molti programmatori si abituano ai puntatori ed è per questo che troveranno argomenti per evitare l'uso del riferimento.

Avere un puntatore come membro costringerà te o il membro del tuo team a controllare il puntatore più e più volte prima dell'uso, con il commento "solo nel caso". Se un puntatore può essere zero, probabilmente il puntatore viene utilizzato come tipo di flag, il che è negativo, poiché ogni oggetto deve svolgere il proprio ruolo.


2
+1: stesso che ho sperimentato. Solo alcune classi di archiviazione di dati generici richiedono assegnemtn e copy-c'tor. E per più framework di oggetti business di alto livello dovrebbero esserci altre tecniche di copia che gestiscono anche cose come "non copiare campi univoci" e "aggiungi a un altro genitore" ecc. Quindi usi il framework di alto livello per copiare i tuoi oggetti business e non consentire le assegnazioni di basso livello.
mmmmmmmm,

2
+1: alcuni punti dell'accordo: i membri di riferimento che impediscono la definizione di un operatore di assegnazione non sono argomenti importanti. Come si afferma correttamente in un modo diverso, non tutti gli oggetti hanno una semantica di valore. Concordo anche sul fatto che non vogliamo che "if (p)" sia sparso in tutto il codice per nessun motivo logico. Ma l'approccio corretto è attraverso gli invarianti di classe: una classe ben definita non lascia spazio a dubbi sul fatto che i membri possano essere nulli. Se qualsiasi puntatore può essere nullo nel codice, mi aspetto che sia ben commentato.
James Hopkin,

@JamesHopkin Concordo sul fatto che le classi ben progettate possono utilizzare i puntatori in modo efficace. Oltre a proteggersi da NULL, i riferimenti garantiscono anche che il tuo riferimento sia stato inizializzato (non è necessario inizializzare un puntatore, quindi potrebbe puntare a qualsiasi altra cosa). I riferimenti indicano anche la proprietà, ovvero che l'oggetto non possiede il membro. Se usi costantemente riferimenti per membri aggregati, avrai un livello molto elevato di fiducia nel fatto che i membri puntatore sono oggetti compositi (anche se non ci sono molti casi in cui un membro composito è rappresentato da un puntatore).
weberc2,


6

In alcuni casi importanti, l'assegnabilità non è semplicemente necessaria. Si tratta spesso di wrapper di algoritmi leggeri che facilitano il calcolo senza uscire dall'ambito. Tali oggetti sono candidati principali per i membri di riferimento poiché si può essere certi che mantengano sempre un riferimento valido e non debbano mai essere copiati.

In tali casi, assicurarsi di rendere non utilizzabile l'operatore di assegnazione (e spesso anche il costruttore di copie) (ereditandoli boost::noncopyableo dichiarandoli privati).

Tuttavia, come già commentato dagli utenti, lo stesso non è vero per la maggior parte degli altri oggetti. Qui, l'utilizzo di membri di riferimento può essere un grosso problema e generalmente dovrebbe essere evitato.


6

Poiché tutti sembrano distribuire regole generali, ne offrirò due:

  • Mai e poi mai usare riferimenti come membri della classe. Non l'ho mai fatto nel mio codice (tranne per dimostrare a me stesso che avevo ragione in questa regola) e non riesco a immaginare un caso in cui lo farei. La semantica è troppo confusa e non è proprio per questo che sono stati progettati i riferimenti.

  • Utilizzare sempre, sempre, riferimenti quando si passano parametri alle funzioni, ad eccezione dei tipi di base o quando l'algoritmo richiede una copia.

Queste regole sono semplici e mi hanno aiutato. Lascio le regole sull'uso di puntatori intelligenti (ma per favore, non auto_ptr) come membri della classe per gli altri.


2
Non sei del tutto d'accordo sul primo, ma il disaccordo non è un problema per valutare la logica. +1
David Rodríguez - dribeas,

(Tuttavia, sembra che stiamo perdendo questa discussione con i buoni elettori di SO)
James Hopkin,

2
Ho votato verso il basso perché "Mai e poi mai usare i riferimenti come membri della classe" e la seguente giustificazione non è giusta per me. Come spiegato nella mia risposta (e nel commento sulla risposta in alto) e dalla mia esperienza, ci sono casi in cui è semplicemente una buona cosa. Ho pensato come te fino a quando non sono stato assunto in un'azienda in cui lo stavano usando in modo positivo e mi ha chiarito che ci sono casi in cui aiuta. In effetti, penso che il vero problema riguardi più la sintassi e le implicazioni nascoste che fanno uso dei riferimenti poiché i membri richiedono al programmatore di capire cosa sta facendo.
Klaim,

1
Neil> Sono d'accordo ma non è "l'opinione" che mi ha fatto votare in basso, è l'asserzione "mai". Dato che discutere qui non sembra essere utile, annullerò il mio voto negativo.
Klaim,

2
Questa risposta potrebbe essere migliorata spiegando cosa sulla confusione della semantica dei membri di riferimento. Questa logica è importante perché, sulla sua faccia, i puntatori sembrano più semanticamente ambigui (potrebbero non essere inizializzati e non ti dicono nulla sulla proprietà dell'oggetto sottostante).
weberc2,

4

Sì a: è vero che un oggetto contenente un riferimento non dovrebbe essere assegnabile, poiché un riferimento non dovrebbe essere cambiato una volta inizializzato?

Le mie regole pratiche per i membri dei dati:

  • non usare mai un riferimento, perché impedisce l'assegnazione
  • se la tua classe è responsabile dell'eliminazione, usa boost's scoped_ptr (che è più sicuro di un auto_ptr)
  • in caso contrario, utilizzare un puntatore o un puntatore const

2
Non riesco nemmeno a nominare un caso in cui avrei bisogno di un'assegnazione dell'oggetto business.
Mykola Golubyev,

1
+1: Aggiungerei solo per stare attento con i membri di auto_ptr - qualsiasi classe che li abbia deve avere un compito e una costruzione della copia esplicitamente definiti (è molto improbabile che quelli generati siano ciò che è richiesto)
James Hopkin,

auto_ptr impedisce anche l'assegnazione (ragionevole).

2
@Mykola: ho anche fatto l'esperienza che il 98% dei miei oggetti business non ha bisogno di assegnazioni o copie. Lo impedisco con una piccola macro che aggiungo alle intestazioni di quelle classi che rende i c'tor di copia e operator = () privati ​​senza implementazione. Quindi nel 98% degli oggetti utilizzo anche riferimenti ed è sicuro.
mmmmmmmm,

1
I riferimenti come membri possono essere utilizzati per dichiarare esplicitamente che la durata dell'istanza della classe dipende direttamente dalla vita delle istanze di altre classi (o della stessa). "Non utilizzare mai un riferimento, perché impedisce l'assegnazione" presuppone che tutte le classi debbano consentire l'assegnazione. È come supporre che tutte le classi debbano consentire l'operazione di copia ...
Klaim,

2

In genere utilizzerei un puntatore nei dati dei membri solo quando il puntatore potrebbe essere nullo o potrebbe cambiare. Ci sono altri motivi per preferire i puntatori ai riferimenti per i membri dei dati?

Sì. Leggibilità del tuo codice. Un puntatore rende più ovvio che il membro è un riferimento (ironicamente :)) e non un oggetto contenuto, perché quando lo usi devi de-referenziarlo. So che alcune persone pensano che sia vecchio stile, ma penso ancora che prevenga semplicemente confusione ed errori.


1
Lakos anche argomenti per questo in "Progettazione software C ++ su larga scala".
Johan Kotlinski,

Credo che anche Code Complete lo sostenga e non sono contrario. Non è troppo convincente però ...
markh44

2

Sconsiglio ai membri dei dati di riferimento perché non sai mai chi deriverà dalla tua classe e cosa potrebbero voler fare. Potrebbero non voler utilizzare l'oggetto di riferimento, ma essendo un riferimento li hai costretti a fornire un oggetto valido. L'ho fatto abbastanza per smettere di usare i membri dei dati di riferimento.


0

Se capisco correttamente la domanda ...

Riferimento come parametro della funzione anziché puntatore: come hai sottolineato, un puntatore non chiarisce chi possiede la pulizia / inizializzazione del puntatore. Preferisci un punto condiviso quando vuoi un puntatore, fa parte di C ++ 11 e un debole_ptr quando la validità dei dati non è garantita per tutta la durata della classe che accetta i dati puntati .; anche una parte di C ++ 11. L'uso di un riferimento come parametro di funzione garantisce che il riferimento non sia nullo. Devi aggirare le funzionalità del linguaggio per aggirare questo, e non ci importa dei programmatori di cannoni sciolti.

Riferimento a una variabile membro: come sopra per quanto riguarda la validità dei dati. Ciò garantisce che i dati puntati, quindi referenziati, siano validi.

Spostare la responsabilità della validità variabile su un punto precedente del codice non solo pulisce il codice successivo (la classe A nel tuo esempio), ma lo chiarisce anche alla persona che lo utilizza.

Nel tuo esempio, che è un po 'confuso (cercherò davvero di trovare un'implementazione più chiara), la A utilizzata da B è garantita per tutta la durata della B, poiché B è un membro di A, quindi un riferimento rinforza questo ed è (forse) più chiaro.

E per ogni evenienza (che è un limite probabilmente basso in quanto non avrebbe alcun senso nel contesto dei tuoi codici), un'altra alternativa, usare un parametro non referenziato, non puntatore A, copierebbe A e renderebbe inutile il paradigma - I davvero non pensi che intendi questo come alternativa.

Inoltre, ciò garantisce anche che non è possibile modificare i dati a cui viene fatto riferimento, in cui è possibile modificare un puntatore. Un puntatore const funzionerebbe, solo se il riferimento / puntatore ai dati non fosse mutabile.

I puntatori sono utili se non è stato possibile impostare il parametro A per B o è possibile riassegnarlo. E a volte un puntatore debole è troppo prolisso per l'implementazione e un buon numero di persone o non sa cosa sia un debole_ptr, o semplicemente non gli piace.

Fai a pezzi questa risposta, per favore :)

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.