Quali sono i pro ei contro dell'utilizzo di un riferimento/puntatore rispetto a un ID
Sto scrivendo in C++, ma questo problema si applica a qualsiasi linguaggio senza GC e anche ai linguaggi con un GC.
Ho una struttura in memoria in cui creo/aggiungo oggetti. La struttura si appropria di quegli oggetti. Non dovrei mai aver bisogno di usare un oggetto dopo che è stato rimosso dalla struttura.
Quando ho implementato per la prima volta questa struttura dati, mi è sembrato naturale utilizzare un ID/chiave/nome/handle per gli oggetti memorizzati in essa. Lo sto usando così:
id1 = structure.addObj(new Square());
id2 = structure.addObj(new Square());
id3 = structure.addObj(new Circle());
obj3 = structure.getObj(id3);
obj3.addFriend( id1 );
obj3.addFriend( id2 );
idMax = structure.findObjWithMostFriends();
objMax = structure.getObj(idMax);
print(objMax.name);
Dopo averlo usato per un po', penso che sarebbe meglio dimenticarsi degli ID e usare invece sempre i riferimenti agli oggetti. In questo modo non avrei bisogno di passare ogni volta un riferimento alla struttura.
Sto pensando di refactoring di tutto per utilizzare solo i riferimenti, ma ho paura di pentirmene. Vorrei saperne di più su quali sono i pro e i contro dell'utilizzo degli ID per decidere se devo procedere.
Dettagli della memoria:
Gli oggetti vengono allocati nell'heap e il loro indirizzo non cambia mai.
La struttura dealloca quegli oggetti quando vengono rimossi (potrebbero invece essere rilasciati al chiamante, ma al momento non ne ho bisogno).
Non dovrei mai usare oggetti che non appartengono alla struttura. Se il mio programma è corretto, non dovrei mai ritrovarmi con un ID o un puntatore penzolanti. Ma potrebbe succedere se il programma ha dei bug.
Quali sono le tue esperienze nel passare dagli ID ai riferimenti per problemi simili? Quale soluzione dovrei usare?
Risposte
I pro e i contro del puntatore rispetto agli ID dipendono dal contesto in cui vengono utilizzati. Una raccomandazione generale non è quindi possibile.
In genere, gli ID hanno senso se sono associati a un "contenitore", come un repository o un oggetto che funge da aggregato proprietario. In questo caso, l'id consente di astrarre dal layout della memoria, definire interfacce più incapsulate e serializzare facilmente il contenitore. Nel contesto dei grafici, un intero grafo sarebbe un buon candidato per essere il "contenitore" dei suoi nodi e dei suoi bordi.
Tuttavia, se inizi a utilizzare gli ID a livello globale, assumendo un contenitore predefinito gloabl, stai costruendo un codice che sarà strettamente accoppiato alla struttura globale sottostante , difficile da riutilizzare e, sospetto, difficile da mantenere a lungo termine. Il primo impatto di questo problema è il tipo di API miste che stai utilizzando:
- devi usare il riferimento di un oggetto per cambiarlo, ma devi usare gli id per crearli o passarli come argomenti.
- perdi il riferimento associato a un id, in modo da essere vincolato al layout di memoria da cui vuoi astrarre, e il contesto potrebbe tenere traccia del riferimento che vieta qualsiasi spostamento dell'oggetto (e il rischio di far penzolare il puntatore in caso di bug).
- se facessi una copia temporanea di un oggetto, finirebbe nella struttura globale.
Tutto ciò mi sembra molto soggetto a errori così com'è. Soprattutto se si aggiunge il rischio di confusione tra riferimenti e ID quando si dichiarano variabili utilizzando lo auto
stile moderno.
Conclusione : riprogettare completamente la tua API per renderla completamente basata su ID e rendere esplicito il contenitore (ovvero la tua struttura attualmente globale) o passare a shared_ptr
un'API basata su coerenza anziché su ID, essendo l'ID solo un elemento che potrebbe aiutare per trovare share_ptr quando non è noto (e per serializzare i tuoi dati).
Gli ID o gli handle sono generalmente preferibili nei seguenti casi:
- le posizioni di memoria potrebbero non essere corrette, ad esempio quando si punta a contenitori di librerie standard C++ o quando si trasferiscono oggetti tra processi
- sai che avrai pochi ID rispetto allo spazio degli indirizzi del puntatore (può ridurre significativamente i requisiti di memoria)
- gli oggetti sono gestiti tramite il conteggio dei riferimenti e il grafico degli oggetti potrebbe avere dei cicli
- hai bisogno dell'indirizzamento ma il linguaggio non supporta i puntatori di prima classe (ad es. Python, Java)
- sei preoccupato per la durata degli oggetti, ad esempio la deallocazione deterministica dell'intero grafico degli oggetti o le vulnerabilità use-after-free
Il punto con le vite è importante. In C/C++ è tua responsabilità sapere se un oggetto puntato è ancora attivo in modo da poter dereferenziare il puntatore. Esistono due strategie per risolvere questo problema: utilizzare il conteggio dei riferimenti o GC per mantenere in vita l'oggetto finché si dispone di un puntatore o pensare attentamente alle vite come fa il compilatore Rust (che incidentalmente richiede l'uso di ID per grafici di oggetti complessi).
Gli ID sono una soluzione parziale al problema della durata perché un ID da solo non può essere dereferenziato, ma necessita di un contesto che contenga l'oggetto grafico effettivo. La durata di questo contesto è in genere più facile da ragionare, soprattutto quando il contesto è rappresentato da un oggetto allocato nello stack e non fa mai riferimento direttamente agli oggetti allocati nell'heap.
Ma questo non è ermetico, ad esempio potresti dereferenziare un ID nel contesto sbagliato. Ancora una volta, ci sono due approcci: puoi aspettarti che la risoluzione dell'ID possa fallire e quindi restituire un puntatore nullable dalla funzione di risoluzione, oppure provare a rilevare questo errore. Il rilevamento può essere reso più probabile assegnando un breve ID a ciascun contesto e codificandolo in ciascun ID oggetto/gestore.
Attualmente sto pensando di spostare un sistema basato su puntatori intelligenti in un sistema basato su ID perché ciò rende più fattibili query più ricche attraverso il grafico degli oggetti e può eliminare il sovraccarico del conteggio dei riferimenti. Tuttavia, il potenziale riutilizzo degli ID potrebbe portare a bug difficili da rilevare (anche una sorta di use-after-free).
Gli ID funzionano molto bene per la persistenza. Quindi memorizzi un ID nel database e hai un'API per darti l'oggetto dato un ID. Molto preferibile se è lo stesso oggetto per più chiamate. Lo usi principalmente durante la lettura di elementi dalla memoria persistente.
Una volta in memoria, l'utilizzo di un puntatore con conteggio dei riferimenti (puntatore condiviso in C++) è molto più semplice e molto più efficiente.