Possibili alternative per copiare i costruttori

3

Nel mio progetto C ++ mi sto affidando ad alcune librerie che gestiscono la memoria per me. Faccio classi di wrapper, per facilità d'uso e sicurezza della memoria, per esempio la classe qui sotto. Si noti che questo è un esempio molto semplificato, per dimostrare il mio problema.

#include <library>

class Wrapper {
private:
    lib_type* data;
public:
    Wrapper() : data(library_new()) {
    }
    Wrapper(const Wrapper& orig) = delete;
    ~Wrapper() {
        library_free(data);
    }
    const lib_type* getData() const {
        return data;
    }
    /* ... */
    /* Lots of functions for using the wrapped object */
};

Avevo bisogno di cancellare il costruttore di copie, perché altrimenti una copia che andava fuori dal campo invaliderebbe l'oggetto originale. Non avere la possibilità di avere copie rende l'oggetto molto poco pratico da usare - ovunque sia usato ora ho bisogno di tenere un riferimento, e ho bisogno di gestire l'oggetto centralmente, che in parte sconfigge lo scopo della classe wrapper.

Possibili soluzioni che ho provato / pensato di:

  • Copia dei dati. Normalmente non è un'opzione, perché non è consentita dalla libreria o i dati sono enormi.
  • Sposta i costruttori. Ho provato a risolvere la mancanza di un costruttore di copie usando un costruttore di mosse, ma a seconda dell'implementazione, questo non risolveva il problema o diventava effettivamente un costruttore di copie, reintroducendo i problemi correlati .
  • Puntatori intelligenti. Poi ho capito che potrebbe essere risolto utilizzando puntatori intelligenti al wrapper anziché ai riferimenti, in quanto ciò elimina la necessità di mantenere una copia centrale.
  • Disposizione di un puntatore intelligente. Oppure potrei racchiudere un puntatore intelligente sui dati anziché un puntatore non elaborato. Ciò rende l'implementazione del wrapper un po 'più complicata, ma rende anche più facile l'uso, dando alle classi che lo utilizzano un'interfaccia più pulita.

Il mio tentativo alla seconda soluzione di puntatore intelligente applicata all'esempio sopra:

#include <library>

class Deleter {
public:
    void operator()(lib_type* p) const {
        library_free(p);
    }
};

class Wrapper {
private:
    shared_ptr<lib_type> data;
public:
    Wrapper() : data(library_new(), Deleter()) {
    }
    Wrapper(const Wrapper& orig) : data(orig.data) {
    }
    ~Wrapper() {
    }
    const lib_type* getData() const {
        return *data;
    }
    /* ... */
    /* Lots of functions for using the wrapped object */
};

Ora per la domanda concreta: questa è una buona soluzione? O ci sono altre soluzioni, forse migliori?

Sono particolarmente preoccupato per l'implementazione di getData; se dovrei restituire una copia del puntatore condiviso o un puntatore nudo e perché.

    
posta Oebele 12.08.2015 - 12:19
fonte

3 risposte

4

Penso che queste implementazioni siano ragionevoli e una soluzione generalmente buona. L'aggiunta di un costruttore di movimento appropriato e il trasferimento del compito potrebbero contribuire a risolvere i problemi di copia : l'impostazione predefinita dovrebbe essere appropriata con il wrapper condiviso.

Alcuni potrebbero obiettare (o consigliare) che non è necessario avvolgere le strutture della Libreria standard che si usano qui; mentre questo è vero, la semantica della funzione getData() potrebbe essere abbastanza specifica per il tuo codice di destinazione: utilizzare la funzione di libreria o avvolgerlo è davvero un compromesso di progettazione .

Potresti aver bisogno di qualche modifica sugli operatori di assegnazione ; e possibilmente essere più esplicito sulle altre funzioni dei membri speciali (io preferirei questo qui - rende l'intento del codice più chiaro). A titolo di esempio;

#include <library>

class UniqueWrapper {
private:
    lib_type* data;
public:
    UniqueWrapper() : data(library_new()) {}
    UniqueWrapper(const UniqueWrapper&) = delete;
    UniqueWrapper& operator=(const UniqueWrapper&) = delete;
    UniqueWrapper(UniqueWrapper&&) = delete;
    UniqueWrapper& operator=(UniqueWrapper&&) = delete;

    ~UniqueWrapper() {
        library_free(data);
    }
    const lib_type* getData() const {
        return data;
    }
};

E come wrapper condiviso;

#include <library>

class Deleter {
public:
    void operator()(lib_type* p) const {
        library_free(p);
    }
};

class SharedWrapper {
private:
    shared_ptr<lib_type> data;
public:
    SharedWrapper() : data(library_new(), Deleter()) {}

    SharedWrapper(const SharedWrapper& orig) = default;
    SharedWrapper& operator=(const SharedWrapper& orig) = default;
    SharedWrapper(SharedWrapper&& orig) = default;
    SharedWrapper& operator=(SharedWrapper&& orig) = default;

    ~SharedWrapper() = default;

    const lib_type* getData() const {
        return data.get(); // a reference return could also be used.
    }
    const lib_type& getDataAlt() const {
        // alternative for a reference
        return *data;
    }
};

Sulla tua ultima domanda;

I am particularly worried about the getData implementation; whether I should return a copy of the shared pointer or a naked pointer and why?

Quando esponi alcuni dei componenti interni di un oggetto, rinunci al controllo sul modo in cui i dati vengono utilizzati, in sostanza non è una cosa negativa, devi solo tenerlo a mente.

Detto questo, ci sono modi per rendere il codice facile da usare correttamente e difficile da usare in modo errato. In questo caso, sarebbe probabilmente consigliabile restituire un const , poiché il metodo è const - sta dicendo al client del tuo codice, non cambiarlo. Se le modifiche devono essere consentite, è anche richiesta una versione non percentuale% co_de.

La classe wrapper che hai è lì per gestire la risorsa, quindi consiglierei di restituire un riferimento (quindi per l'osservazione) su un puntatore; un puntatore può essere un const quindi se c'è una natura "opzionale" al valore, un puntatore nullptr può essere usato per indicare che.

Se la condivisione e la costanza del tipo restituito deve essere mantenuta, la soluzione di David Cohen di nullptr è pulita.

    
risposta data 12.08.2015 - 13:05
fonte
3

Usa solo shared_ptr direttamente, con il tuo deleter personalizzato. Forse lo digiti se preferisci.

In questo modo ottieni la mossa corretta & copia i costruttori e gli operatori di assegnazione senza digitare . Puoi anche ottenere weak_ptr gratuitamente, se lo desideri.

A meno che il tuo codice non aggiunga alcune funzionalità effettive - o almeno un'interfaccia compatibile con alcuni requisiti esterni che non hai menzionato - la cosa migliore che puoi fare è semplicemente non scriverlo in primo luogo.

Guarda il SharedWrapper di Niall per un buon esempio: a meno che tu abbia bisogno di quella getData signature, è un sacco di codice per nessun beneficio effettivo. (Questa non è una critica: è stato chiaramente scritto supponendo che tu do abbia bisogno della getData signature).

    
risposta data 12.08.2015 - 15:19
fonte
2

Nota: questa risposta affronta la parte finale della domanda. La maggior parte della domanda è già stata ben trattata nella risposta di Niall.

I am particularly worried about the getData implementation; whether I should return a copy of the shared pointer or a naked pointer and why.

Supponiamo che alcune chiamate di codice getData() e salvino quel puntatore in qualche oggetto persistente e che l'ultimo oggetto Wrapper rimanente che fa riferimento a questo puntatore vada fuori ambito prima che questo oggetto persistente lo faccia. Quell'oggetto persistente ora contiene un puntatore non valido.

Per risolvere questo problema, sarebbe molto meglio restituire il puntatore condiviso. Ciò si verifica in un altro problema, ovvero che il getData() attualmente restituisce un puntatore a const lib_type. Il ritorno di un const shared_ptr<lib_type> non farà ciò che desideri, per due motivi. Uno è che questo rende possibile per il chiamante di getData() di modificare i dati, qualcosa che non vuoi che accada. L'altro è che const non ha significato; è come restituire un const int . Il compilatore ignora const . Invece, usa

shared_ptr<const lib_type> getData() const {
    return data;
}

Quanto sopra si avvantaggia del fatto che una shared_ptr<some_type> si converte automaticamente in shared_ptr<const some_type> . Il contrario (conversione di shared_ptr<const some_type> in shared_ptr<some_type> ) è illegale (che presumibilmente è il comportamento che si desidera).


Un'ultima osservazione: quei nomi di tipo possono diventare lunghi e goffi. Presumibilmente stai utilizzando std::shared_ptr anziché shared_ptr e lib_type è presumibilmente qualcosa come some_namespace::AMoreDescriptiveNameThanLibType . Per specificare il tipo di valore ricevuto da una chiamata a getData , un utente del tuo codice dovrebbe scrivere

std::shared_ptr<const some_namespace::AMoreDescriptiveNameThanLibType> data =
    wrapper.getData();

Un modo per aggirare questo è utilizzare auto :

    auto data = wrapper.getData();

Un'altra opzione è fornire alcune definizioni di tipi (o alcuni alias di tipi) nella definizione di Wrapper :

class Wrapper {
public:
    typedef std::shared_ptr<lib_type> dataT;
    typedef std::shared_ptr<const lib_type> const_dataT;
    // Rest of code elided
};

Quindi i tuoi utenti devono solo digitare

Wrapper::const_dataT data = wrapper.getData();
    
risposta data 12.08.2015 - 15:32
fonte

Leggi altre domande sui tag