Cosa devo fare in C ++ quando si implementa una classe contenitore: memorizzare oggetti per valore o per riferimento?

2

Sono nuovo di C ++, proveniente da Java.

In Java, tutte le variabili (eccetto primitive) sono essenzialmente dei puntatori. Tengono l'indirizzo di qualunque cosa stiano "trattenendo".

Quindi qualsiasi struttura dati Java memorizza i dati per riferimento. Puoi anche archiviare in base al valore, ad esempio salvare e restituire una copia di qualsiasi elemento archiviato, ma ciò richiederebbe un lavoro supplementare e non è nativo per la lingua.

Ad esempio, le collezioni ArrayList , HashSet e un semplice array archiviano tutti gli indirizzi degli elementi che "memorizzano" e non gli articoli effettivi.

Tuttavia, in C ++, hai una scelta: quando si implementa una classe contenitore, è possibile memorizzare e restituire gli articoli utente per valore o per riferimento.

Ad esempio, ecco una semplice classe Stack che ho scritto (omessa roba irrilevante):

template <typename T> class Stack {
public:
    Stack(...) : ... { }

    void push(const T& item) {
        if(size == capacity - 1)
            enlargeArray();

        data[indexToInsert++] = &item;
        size++;
    }

    const T& pop() {
        const T& item = *data[indexToInsert - 1];
        data[indexToInsert - 1] = 0;
        indexToInsert--;
        size--;
        return item;
    }

    int getSize() const {
        return size;
    }
private:
    const T** data;
    int indexToInsert;
    int size;
    int capacity;

    void enlargeArray() {
        // omitted
    }
};

Questa struttura dati prende e restituisce i dati per riferimento. push prende un riferimento const e pop restituisce un riferimento const. L'array di supporto è un array di puntatori, non di oggetti.

Tuttavia push potrebbe anche avere il seguente aspetto:

    void push(T item) {
        if(size == capacity - 1)
            enlargeArray();

        data[indexToInsert++] = item;
        size++;
    }

E pop potrebbe restituire un T , non un const T& , ecc.

La mia domanda è: qual è l'approccio preferito in C ++? C'è c'è un approccio preferito? Quale approccio dovrei assumere normalmente quando si implementano le classi "container"?

    
posta Aviv Cohn 06.10.2014 - 01:09
fonte

3 risposte

10

In primo luogo, probabilmente non dovresti implementare una classe contenitore. Il 95% delle volte dovresti averne uno incluso nella libreria standard. Se vuoi solo imparare, o sei nel 5%, continua.

Se stai definendo un modello, lascia la decisione ai tuoi utenti. Gli utenti possono utilizzare:

Stack<Foo> se vogliono in base al valore. Stack<Foo*> se vogliono per puntatore. Stack<std::unique_ptr<Foo>> se vogliono puntatori che puliscono da soli.

Quando scegli quale usare, dovresti impostare il valore in base al valore, a meno che tu non abbia una buona ragione per fare qualcosa di diverso. All'interno della tua classe di stack, memorizza tutto in base al valore. Se l'uso del modello ha bisogno di indirezione, possono usare T = tipo di puntatore.

Guardando il tuo codice:

void push(const T& item) {
    if(size == capacity - 1)
        enlargeArray();

    data[indexToInsert++] = &item;
    size++;
}

Non puoi farlo. &item registra il puntatore a ciò che è stato passato. Ma non hai idea di quanto a lungo il puntatore sarà valido. Potrebbe diventare non valido subito dopo aver completato la pressione. In tal caso, hai memorizzato un puntatore in un posto non valido. In generale, non si può presumere che un puntatore rimanga valido. Dovresti invece copiare l'oggetto.

    
risposta data 06.10.2014 - 01:49
fonte
3

My question is: what is the preferred approach in C++? Is there a preferred approach? Which approach should I normally take when implementing 'container' classes?

In C ++ puoi conservare oggetti con:

  • Valore
  • di riferimento
  • puntatore
  • puntatore intelligente (std :: unique_ptr, std :: shared_ptr, YourPointerClass). (non hai menzionato gli ultimi due).

Ciascuno di questi è valido per diverse situazioni e impone diversi vincoli, riguardanti la proprietà degli oggetti, la gestione della vita e il comportamento polimorfico (cioè, non esiste un approccio preferenziale - ce ne sono molti :)):

  • utilizza la memorizzazione di in base al valore quando:

    • sei non memorizzare oggetti con comportamento polimorfico (cioè gli oggetti sono classi concrete senza classe base e senza funzioni virtuali, o tutti gli oggetti hanno lo stesso tipo di runtime - non stai memorizzando le specializzazioni di un classe base)
    • il tuo contenitore possiede gli oggetti (ad esempio, gli oggetti contenuti devono avere una durata pari a quella del contenitore e verranno distrutti quando il contenitore viene distrutto)
  • utilizza la memorizzazione di per riferimento quando:

    • gli oggetti memorizzati sono non di proprietà del contenitore
    • i tuoi oggetti contenuti hanno un ambito e una durata maggiori rispetto al contenitore.
    • ti interessa il comportamento polimorfico (in questo caso dovresti memorizzare i riferimenti a una classe base) Normalmente non dovresti farlo, in quanto vi è la possibilità di affettare l'assegnazione (a meno che la tua gerarchia di classi non supporti l'assegnazione polimorfica - che è un'altra discussione)
  • utilizza la memorizzazione per puntatore raw quando:

    • i tuoi oggetti memorizzati espongono il comportamento polimorfico (classe base e / o funzioni virtuali)
    • il contenitore non possiede gli oggetti memorizzati
  • utilizza la memorizzazione con puntatore intelligente quando:

    • i tuoi oggetti memorizzati espongono il comportamento polimorfico
    • i tuoi oggetti sono di proprietà del contenitore (std :: unique_ptr), la proprietà è condivisa (std :: shared_ptr) o l'osage del puntatore intelligente viene imposto dai vincoli del codice client.
    • gli oggetti memorizzati sono costosi da copiare / creare istanze (modifica cfr. @NirFriedman)

Per supportare tutto questo, vorrai probabilmente creare il template della tua classe con il tipo memorizzato e compilare secondo necessità, nel codice client.

    
risposta data 06.10.2014 - 12:50
fonte
0

Dipende se il contenitore "possiede" l'oggetto e sono una classe base.

  1. Se il contenitore non possiede l'oggetto, dovresti usare i puntatori e fare attenzione ai pericoli (assicurati che nessun contenitore tenga un puntatore all'oggetto quando viene distrutto).

  2. Se il contenitore possiede l'oggetto e non è una classe base, è necessario archiviarlo in base al valore.

  3. Altrimenti (il contenitore possiede e ci sono sottoclassi) dovresti memorizzare usando std::unique_ptr per assicurarti che non si verifichi l'affettamento in negozio e la distruzione corretta.

risposta data 06.10.2014 - 01:27
fonte

Leggi altre domande sui tag