2015-09-24 20 views
11

Sto lavorando con una API C legacy in base alla quale l'acquisizione di alcune risorse è costosa e la liberazione di tale risorsa è assolutamente fondamentale. Sto usando C++ 14 e voglio creare una classe per gestire queste risorse. Ecco lo scheletro di base della cosa ...C++ Move Semantics - Wrapping Legacy C API

class Thing 
{ 
private: 
    void* _legacy; 

public: 
    void Operation1(...); 
    int Operation2(...); 
    string Operation3(...); 

private: 
    Thing(void* legacy) : 
     _legacy(legacy) 
    { 
    } 
}; 

Questo non è proprio il modello singleton. Nulla è statico e potrebbero esserci molte istanze di Thing, tutte gestendo le proprie risorse legacy. Inoltre, questo non è semplicemente un puntatore intelligente. Il puntatore impacchettato, _legacy è privato e tutte le operazioni sono esposte tramite alcune funzioni di istanza pubbliche che nascondono l'API legacy dai consumatori.

Il costruttore è privata perché le istanze di Thing saranno restituiti da una fabbrica statica o chiamato costruttore che effettivamente acquisire la risorsa. Ecco un'imitazione a basso costo di quella fabbrica, utilizzando malloc() come segnaposto per il codice che richiamare le vecchie API ...

public: 
    static Thing Acquire() 
    { 
     // Do many things to acquire the thing via the legacy API 
     void* legacy = malloc(16); 

     // Return a constructed thing 
     return Thing(legacy); 
    } 

Ecco il distruttore, che è responsabile di liberare la risorsa legacy, ancora una volta, free() è solo un segnaposto ...

~Thing() noexcept 
    { 
     if (nullptr != _legacy) 
     { 
      // Do many things to free the thing via the legacy API 
      // (BUT do not throw any exceptions!) 
      free(_legacy); 
      _legacy = nullptr; 
     } 
    } 

Ora, voglio far sì che esattamente una risorsa legacy è gestito da esattamente un'istanza di Thing. Non volevo che i consumatori della classe Thing passassero le istanze in giro a piacimento: devono essere di proprietà locale della classe o della funzione, direttamente o tramite unique_ptr o incapsulati con uno shared_ptr che può essere passato circa. A tal fine, ho cancellato l'operatore di assegnazione e copiare i costruttori ...

private: 
    Thing(Thing const&) = delete; 
    void operator=(Thing const&) = delete; 

Tuttavia, questo ha aggiunto un ulteriore problema. O ho dovuto cambiare il mio metodo di fabbrica per restituire uno unique_ptr<Thing> o uno shared_ptr<Thing> o ho dovuto implementare la semantica del movimento. Non volevo dettare il modello in base al quale Thing dovrebbe essere usato così ho scelto di aggiungere una mossa-costruttore e spostare-assegnazione-operator come segue ...

Thing(Thing&& old) noexcept : _legacy(old._legacy) 
    { 
     // Reset the old thing's state to reflect the move 
     old._legacy = nullptr; 
    } 

    Thing& operator= (Thing&& old) noexcept 
    { 
     if (&old != this) 
     { 
      swap(_legacy, old._legacy); 
     } 
     return (*this); 
    } 

Con questo tutto fatto, ho potuto utilizzare Thing come un locale e spostarla su ...

Thing one = Thing::Acquire(); 
    Thing two = move(one); 

non ho potuto rompere il modello per il tentativo di commettere auto-assegnazione:

Thing one = Thing::Acquire(); 
    one = one;      // Build error! 

I Coul d anche fare una unique_ptr a uno ...

auto three = make_unique<Thing>(Thing::Acquire()); 

O un shared_ptr ...

auto three = make_shared<Thing>(Thing::Acquire()); 

Tutto ha funzionato come mi aspettavo e il mio distruttore corse esattamente al momento giusto in tutte le mie prove. In effetti, l'unica irritazione era che make_unique e sia in realtà invocato il costruttore di movimento - non era ottimizzato come avevo sperato.

Prima domanda: Avere ho implementato il mossa -constructor e Move-assegnazione-operatore correttamente? (Sono abbastanza nuovi per me e questa sarà la prima volta che ne ho usato uno in rabbia.)

Seconda domanda: Si prega di commentare questo modello! È un buon modo per racchiudere le risorse legacy in una classe C++ 14?

Infine: Devo modificare qualcosa per rendere il codice migliore, più veloce, più semplice o più leggibile?

+0

È possibile utilizzare sempre il [modello Singleton statico] (http://stackoverflow.com/questions/1008019/c-singleton-design-pattern) per evitare l'uso di puntatori intelligenti. – NathanOliver

+0

Non è davvero un singleton - solo uno pseudo-singleton. Potrebbero esserci diverse cose, tutte acquisite separatamente ma gestite da un'istanza di 'Thing' ciascuna. – Xharlie

+0

Sembra che si stia reinventando un puntatore intelligente e quindi lo si memorizzi in un altro ointer intelligente. Vorrei solo scrivere un deleter personalizzato e usare direttamente 'std :: unique_ptr' o' std :: shared_ptr'. – Galik

risposta

2
struct free_thing{ 
    void operator()(void* p)const{ 
    // stuff 
    free(p); 
    } 
}; 
using upthing=std::unique_ptr<void,free_thing>; 

upthing make_thing(stuff){ 
    void*retval; 
    // code 
    return upthing(retval); 
} 

Conservare un upthing in Thing come _legacy. Usa predefinito dtor, move ctor, move assign per Thing (=default).

Il codice di distruzione passa a free_thing.

Il tuo ctor crea il upthing.

Ora gli utenti possono trattare il tuo Thing come un tipo di valore di solo spostamento.

Non scrivere i propri gestori di puntatori a meno che non sia realmente necessario. Unico e condiviso fa molto per te: se scrivi il tuo puntatore intelligente, usali pure come budello interno.

+0

Mi piace così. Cosa succede se il codice destroy genera un'eccezione? È permesso lanciare eccezioni? – Xharlie

6

Si consiglia di avvolgere il proprio Thing in un puntatore intelligente, quindi non sarà necessario preoccuparsi della copia e spostare la semantica.

class Thing 
{ 
private: 
    void* _legacy; 

public: 
    void Operation1(...); 
    int Operation2(...); 
    string Operation3(...); 

    Thing(const Thing&) = delete; 
    Thing(Thing&&) = delete; 
    Thing& operator=(const Thing&) = delete; 
    Thing& operator=(Thing&&) = delete; 

    static std::shared_ptr<Thing> acquire() { 
     return std::make_shared<Thing>(); 
    } 

private: 
    Thing() : _legacy(malloc(16)) { 
     // ... 
    } 

    ~Thing() { 
     free(_legacy); 
    } 
}; 

Allo stesso modo, è possibile farlo con unique_ptr:

std::unique_ptr<Thing> acquire() { 
    return std::make_unique<Thing>(); 
} 

Lei sembrava implicare che si desidera avere una sola istanza di questa cosa, anche se anche nella vostra soluzione che non ha cercato di fare qualcosa del genere. Per questo hai bisogno di variabili statiche. Tuttavia, ricorda che in questo caso la tua risorsa verrà liberata solo dopo l'uscita dalla tua funzione main(). Per esempio:

static std::shared_ptr<Thing> acquire() { 
    static std::shared_ptr<Thing> instance; 
    if (!instance) { 
     instance = std::make_shared<Thing>(); 
    } 
    return instance; 
} 

o la versione unique_ptr:

static Thing& acquire() { 
    static std::unique_ptr<Thing> instance; 
    if (!instance) { 
     instance = std::make_unique<Thing>(); 
    } 
    return *instance; 
} 

In alternativa, è possibile utilizzare weak_ptr per acquisire un caso a livello di programma, che viene liberata quando nessuno lo usa. In questo caso non sarà possibile utilizzare unique_ptr per questo scopo. Questa versione ricrea l'oggetto se è stato liberato e quindi nuovamente necessario.

static std::shared_ptr<Thing> acquire() { 
    static std::weak_ptr<Thing> instance; 
    if (instance.expired()) { 
     instance = std::make_shared<Thing>(); 
    } 
    return instance.lock(); 
} 
+3

@Xharlie Ma il Il principio che è possibile fornire un deleter personalizzato al puntatore intelligente è ancora valido. – Galik

+0

Abbastanza giusto. Il deleter personalizzato è annotato. Ma, alla fine, la classe avrà altre funzioni su di essa che non sono solo allocazione e deallocazione. – Xharlie

+0

@Xharlie Quindi puoi creare la classe 'Thing' con qualsiasi operazione che fa la cosa' malloc() 'nel suo costruttore e' 'libero()' nel distruttore, quindi inseriscila nel tuo 'shared_ptr' o' unique_ptr' come ho descritto Quindi non avrai bisogno dei deletatori personalizzati, e puoi anche usare 'make_shared' o' make_unique'. – petersohn