Suggerimenti per l'estensione idioma RAII

6

RAII è di gran lunga l'idioma più utile in c ++. Tuttavia sembrano esserci due trappole comuni ad esso associate che ritengo debbano essere formalmente affrontate.

Mancato rilascio di una risorsa nel distruttore e invalidazione delle risorse prima della decostruzione. Quali sono le tue opinioni su estensioni di pattern solidi per incorporare queste trappole?

Esempi di errore di rilascio della risorsa:

  1. Impossibile chiudere un file
  2. Impossibile arrestare una connessione socket

Esempi di invalidazione delle risorse (riferimento risorsa ciondolante):

  1. Il socket peer ha chiuso la connessione
  2. La risorsa è stata revocata dal fornitore di terze parti

Ecco un nuovo oggetto RAII che ho progettato per gestire il problema di invalidazione delle risorse utilizzando il feedback finora (feedback sempre ben accetto):

template<typename T> class shared_weak_ptr;

template<typename T>
class revoke_ptr {
  friend class shared_weak_ptr<T>;
public:
  typedef std::function<void()> revoke_func;
  revoke_ptr(T* t) : _self(std::make_shared<model_t>(t)) {}

  void revoke() {
    if (!_self->_resource) throw 1; // Already revoked
    for (auto func : _self->_revoke_funcs) func();
    _self->_revoke_funcs.clear();
    _self->_resource.reset();
  }
private:
  struct model_t {
    model_t(T* t) : _resource(t) {}
    std::shared_ptr<T> _resource;
    std::list<revoke_func> _revoke_funcs;
  };

  std::shared_ptr<model_t> _self;
};

template<typename T>
class shared_weak_ptr {
public:
  template<typename R>
  shared_weak_ptr(revoke_ptr<T> revoke_ptr, R revoke_callback) : _self(std::make_shared<const model_t>(std::move(revoke_ptr), std::move(revoke_callback))) {}

  std::shared_ptr<T> lock() const { return _self->_revoke_ptr._self->_resource; }
private:
  using revoke_func = typename revoke_ptr<T>::revoke_func;
  using revoke_it = typename std::list<revoke_func>::iterator;
  struct model_t {
    model_t(revoke_ptr<T> revoke_ptr, revoke_func revoke_callback) : _revoke_ptr(std::move(revoke_ptr)) {
      if (!_revoke_ptr._self->_resource) throw 1; // The resource has already been revoked
      _revoke_ptr._self->_revoke_funcs.emplace_back(std::move(revoke_callback));
      _revoke_it = _revoke_ptr._self->_revoke_funcs.end();
      --_revoke_it;
    }
    ~model_t() {
      if (!_revoke_ptr._self->_resource) return; // Already revoked so our callback has already been removed
      _revoke_ptr._self->_revoke_funcs.erase(_revoke_it); // Remove our callback
    }
    revoke_ptr<T> _revoke_ptr;
    revoke_it _revoke_it;
  };

  std::shared_ptr<const model_t> _self;
};

Esempio di utilizzo:

int main() {
  revoke_ptr<int> my_revoke_ptr(new int(5));

  typedef std::shared_ptr<const uint8_t> unique_key;
  unique_key key = std::make_shared<const uint8_t>();

  std::map<unique_key, shared_weak_ptr<int>> my_collection;

  shared_weak_ptr<int> my_shared_weak_ptr(my_revoke_ptr, [key, &my_collection](){
    std::cout << "revoked" << std::endl;
    auto it = my_collection.find(key);
    if (it != my_collection.end()) {
      my_collection.erase(it);
    }
  });
  my_collection.emplace(key, my_shared_weak_ptr);

  if (auto ptr = my_shared_weak_ptr.lock()) {
    assert(true);
    std::cout << *ptr << std::endl;
  } else {
    assert(false);
    std::cout << "nope" << std::endl;
  }

  my_revoke_ptr.revoke();

  if (auto ptr = my_shared_weak_ptr.lock()) {
    assert(false);
    std::cout << *ptr << std::endl;
  } else {
    assert(true);
    std::cout << "nope" << std::endl;
  }

  assert(my_collection.find(key) == my_collection.end());
  return 0;
}
    
posta Aaron Albers 22.11.2016 - 02:03
fonte

4 risposte

4

Potresti considerare di lanciare qualche eccezione dal distruttore (ma questo è disapprovato per buone ragioni, e se lo fai, devi stare attento ...; il tuo commento cita this ). Ma la vera domanda è cosa faresti dopo?

Devi pensare a cosa fare con questo tipo di fallimenti. In molti casi, credo che fallendo un close (2) o un shutdown (2) è così insolito e serio che praticamente puoi solo emettere un messaggio (es. qualche file di registro, magari usando syslog (3) ....) e termina il programma. Certo, YMMV. In alcuni casi devi fare qualcosa di intelligente su close di errore, ma quello che farai è specifico per applicazione e dominio.

(Non sono sicuro di comprendere appieno cosa succede all'interno del kernel Linux quando close non funziona, e non sono sicuro che le risorse del kernel vengano rilasciate correttamente prima della conclusione del processo in quel caso strano: i dettagli sono specifici del sistema e del file system.)

Potresti anche creare il tuo oggetto "socket" con una chiusura al suo interno, o alcuni richiamata , per gestire gli errori.

C'è nessuna risposta universale ai tuoi dubbi; è specifico per una classe di applicazioni, un dominio, il tuo sistema operativo, la tua particolare configurazione di computer, sia hardware che amp; software- ecc ... BTW, la tua domanda non è specifica RAII o C ++ (problemi simili potrebbero valere per il codice Ocaml che non usa RAII, o anche per il codice C).

Guarda anche (in particolare se sei preoccupato per i sistemi embedded critici) su MISRA C , il futuro MISRA C ++ (e il vecchio Embedded C ++ ) e standard industriali come DO-178C . Trova quello pertinente al tuo dominio e alle aree di applicazione (oppure prendi ispirazione da alcuni domini o aree applicative simili alla tua). Consulta anche Common Criteria , leggi di più su Metodi formali . Ma c'è nessun proiettile d'argento !

Leggi anche le astrazioni leaky (in pratica, non puoi evitarle) e Joel legge delle astrazioni che perdono . Non aspettarti caratteristiche linguistiche o paradigmi di programmazione per risolvere o addirittura guidare tutti progettazione del software problemi.

    
risposta data 22.11.2016 - 07:05
fonte
2

Direi che il modo migliore per gestire questa situazione è lasciare che sia l'utente a gestirlo.

Considera un file, incapsulato da un oggetto RAII. Se il distruttore chiude il file, allora l'utente sta dicendo che non interessa se la chiusura del file provoca un errore.

Il modo in cui esprimono la loro preoccupazione è tramite manualmente chiudendolo, tramite una funzione di chiusura esplicita. In questo modo, possono rilevare errori e fare qualcosa per loro.

Questa distinzione è importante, in particolare nel caso di lancio di eccezioni. Diciamo che apri un file, fai qualche operazione, poi chiudilo (manualmente). Se quella parte "some operation" genera un'eccezione che viene catturata al di fuori del tuo scope, cosa esattamente intendi fare se lo stack unwinding provoca un errore sulla chiusura del file? Sei nel mezzo di lasciare quell'ambito; non sei esattamente in grado di correggere i dati che vengono manchiati a causa di questo errore. In effetti, l'eccezione che causa lo sganciamento di questo stack potrebbe essere perché di corruzione del file, quindi potresti non essere nemmeno capace per correggerlo.

Quindi, se stai avvolgendo qualche risorsa in un oggetto RAII e il rilascio di quella risorsa potrebbe provocare un errore (che non rappresenta la continua esistenza di quella risorsa), direi che il modo più ragionevole per gestirlo è quello di fornire un modo manuale per rilasciare presto quella risorsa. Uno che può fallire e può farlo in modi facilmente rilevabili / gestibili.

One example I gave was a socket being closed by the remote peer.

RAII, in definitiva, rappresenta la proprietà. Cioè, rappresenta chi ha il controllo sul fatto che tale risorsa esista ancora.

shared_ptr modelli proprietà condivisa di un oggetto C ++ assegnato in modo dinamico; continuerà ad esistere fino a quando qualcuno mantiene ancora un shared_ptr ad esso. weak_ptr modella la debole condivisione; può o non può esistere, ma se non esiste più, puoi almeno chiedere se non è lì.

Se un socket può essere "chiuso" da qualcuno che non sei tu, allora non ne hai alcuna proprietà reale. La tua relazione con il socket è più simile a weak_ptr che a shared_ptr .

Il tuo tipo di oggetto dovrebbe modellare la relazione che hai con quell'oggetto.

    
risposta data 22.11.2016 - 19:01
fonte
1

What are your thoughts on solid pattern extensions to incorporate these pitfalls?

errore di rilascio della risorsa

  • Nel caso generale, semplicistico, ignora tutti gli errori nei distruttori. (ignora i ritorni degli errori, cattura + mangia le eccezioni): I faccio lo trovo molto attraente, ma trovo che questo sia l'unico modo sensato data la lingua così com'è .
  • Se la registrazione è disponibile, DO registra questi errori.
  • Per casi specifici o classi specifiche, avere lanci di distruttori che segnalano questi errori attraverso un'eccezione è OK e assolutamente soddisfacente - vedi sotto per restrizioni e pensieri noti.

invalidazione delle risorse (riferimento risorsa ciondolante)

Penso che questo cada decisamente fuori dalla portata di ciò che possiamo gestire tramite RAII. Ben inserita altra risposta :

RAII, ultimately, represents ownership. That is, it represents who has control over whether that resource still exists.

If a socket can be "closed" by someone who isn't you, then you don't have any real ownership of it. Your relationship to the socket is more like a weak_ptr than a shared_ptr.

Il problema concettuale è che il distruttore termina la vita dell'oggetto incondizionatamente e se l'oggetto era l'unico responsabile della risorsa, in realtà, non puoi fallire, il proprietario della risorsa sarà sempre essere andato.

Citerò solo altri :

... The real reason is that an exception should be thrown if and only if a function's postconditions cannot be met. The postcondition of a destructor is that the object no longer exists. This can't not happen. Any failure-prone end-of-life operation must therefore be called as a separate method before the object goes out of scope (sensible functions normally only have one success path anyway)

Ovviamente, come altri hanno notato che possono essere spesso troppo semplicistici e gentili di sconfiggere lo scopo di un aspetto di RAII, ovvero che non puoi dimenticare di chiudere / svuotare / qualsiasi cosa.

Ho avuto le mie opinioni su questo e non è come una soluzione completa, ma penso che sia utile :

The whole problem becomes easier to think about when we split classes in two types. A class dtor can have two different responsibilities:

  • (R) release semantics (aka free that memory)
  • (C) commit semantics (aka flush file to disk)

If we view the question this way, then I think that it can be argued that (R) semantics should never cause an exception from a dtor as there is a) nothing we can do about it and b) many free-resource operations do not even provide for error checking, e.g. void free(void* p);.

Objects with (C) semantics, like a file object that needs to successfully flush it's data or a ("scope guarded") database connection that does a commit in the dtor are of a different kind: We can do something about the error (on the application level) and we really should not continue as if nothing happened.

Leggendo su questo, ci sono diversi ostacoli tecnici per lanciare i distruttori:

  • array e contenitori - senza fortuna: gli oggetti che vivono lì dentro non devono mai lanciarsi mentre vivono lì dentro
  • deallocation dinamica - sembra che questo non funzioni ancora ( 353 non è nello standard per quanto posso vedere)
  • Lanciatore d'affari già facendo lo sbobinamento delle eccezioni (il caso classico) - questo in realtà è parzialmente risolto in C ++ 14/17, vedi std::uncaught_exception*s* e il suo rationale

Penso che la funzione std::uncaught_exceptionS consentirà finalmente di gestire più facilmente il caso (molto classico) di file-close-fail-because-flush-fail-and-now-data-is-corrupt :

Il classico consiglio è di chiamare flush (o close) esplicitamente, in questo modo puoi sempre lanciare un errore, che sfortunatamente sta tornando alla chiusura manuale delle risorse, che vorremmo provare ad evitare!

Con il macchinario uncaught_exceptionS , possiamo finalmente scrivere classi che escono dal loro dominio solo quando sono "permessi" di farlo, cioè durante l'uscita dallo scope regolare:

Penso che potrebbe essere molto utile:

  • Nel percorso regolare del codice, l'errore di chiudere (svuotare) il file dei dati importanti causerà un'eccezione, annullando in tal modo l'operazione che ha scritto sul file, consentendo quindi di segnalare (forse gestire) il caso di errore direttamente senza contorsioni manuali call-flush-and-close.
  • Nell'eccezione-unwinding-code-path, l'eventuale close-failure sarà semplicemente ignorato (forse registrato), poiché l'operazione è già "cancellata" tramite un'eccezione comunque, quindi probabilmente (si spera) l'operazione che ha richiesto il il file da scrivere / chiudere è considerato comunque un errore, a causa di un altro errore.

So che c'è un po 'di "se" è qui, ma comunque penso che valga la pena dare un'occhiata se cerchi "pensieri su estensioni di pattern solidi per incorporare queste trappole".

    
risposta data 22.11.2016 - 22:56
fonte
0

Failure to release a resource in the destructor and resource invalidation prior to deconstruction. What are your thoughts on solid pattern extensions to incorporate these pitfalls?

Penso che tu fraintenda cosa sia RAII; Per dirla in breve (troppo è già stato scritto su RAII) RAII significa rendere i tuoi atti di acquisizione di responsabilità combinare con la costruzione di un'istanza di oggetti C ++ e rilasciare la risorsa in distruzione).

Questo non ha nulla a che fare con la mancata attuazione dell'atto di rilascio delle risorse.

Se guardi al concetto di precondizioni e postcondizioni, puoi vedere che c'è un intero host di stati hardware e software che semplicemente non possono essere testati a livello di software ("il connettore di rete difettoso funziona solo quando il bordo della porta spinge il cavo verso la parete e il connettore è tenuto in un angolo ").

L'esistenza di queste condizioni assicura che tu abbia delle astrazioni che perdono (vedi la risposta di Basile ): condizioni di errore che non puoi controlla per, e le chiamate API che "non può fallire", ma a volte, lo fanno ancora.

In questi casi si hanno alcune soluzioni: specificare a livello di documentazione dell'API che ci sono condizioni che non si verificano / errori che non si gestiscono, o ignorare quei casi (come situazioni che il software non può gestire), o aggiungere un ulteriore livello di verifiche / astrazioni, ad esempio affidando la responsabilità di rilascio a una coda che eseguirà il rilascio con i propri controlli e saldi in posto e che potrebbero non riuscire).

Nessuna delle azioni proposte nel paragrafo precedente fa parte di RAII, né dovrebbero esserlo, perché RAII non tratta di quanto siano assolute le astrazioni dietro la tua API.

    
risposta data 25.11.2016 - 10:48
fonte

Leggi altre domande sui tag