Nessuna eccezione C + + e oggetti parzialmente costruiti

3

Esaminando lo standard di codifica C ++ di Joint Strike Fighter Air Vehicle, la regola AV 73 indica qualcosa sulle linee: I costruttori c ++ di default dovrebbero essere evitati se ciò significa lasciare l'oggetto in una posizione parzialmente costruita. Questo è ovvio un buon consiglio, ma le cose si complicano quando disabiliti anche le eccezioni (come fa)

Mi chiedo se provino anche a dare qualcos'altro in cambio di renderlo pratico. Per esempio. l'esempio seguente ha bisogno di 2 costruzioni faze se OUT_OF_MEMORY deve essere gestito (ma il problema è più generale ovviamente)

class A {
    A() = default; // no way to report construction of B and C
    int Init(); // allocation failure of B/C can be reported only here
    private:
    B* b;
    C* c
}

Un modo per gestire il problema è di avere un membro statico A * Create () ma in questo caso A deve liberare lo store allocato o mantenere uno stato non completamente inizializzato in A e controllarlo all'interno di ogni accesso membro, che sembra non è niente di meglio che usare uno stile C

int Initialize(A& out) // stack allocation
int Initialize(unique_ptr<A>& out) // free store allocation

EDIT Ho postato anche un altro approccio. Ho preso in considerazione che l'allocazione dinamica della memoria è necessaria e il fatto che qualsiasi errore può accadere mentre si costruisce completamente un oggetto. Questo è uno scenario plausibile per qualsiasi lavoro di sviluppo del driver.

    
posta Ghita 04.04.2015 - 21:37
fonte

3 risposte

7

Disclaimer obbligatori

(1) Perché le persone che hanno visto il codice non possono dire nulla al riguardo e le persone che possono commentare liberamente non hanno mai visto il codice vero e proprio, tutto ciò che possiamo fare qui è di speculare, speculare e speculare. Pertanto, qui non è una risposta, solo una speculazione.

(2) Questo non è il modo tipico in cui scrivo C ++ perché la maggior parte dei progetti su cui lavoro consente eccezioni, almeno su base locale (cioè non oltre i limiti dell'applicazione), e lo standard di codifica garantisce che ci siano sempre catcher di eccezione appropriati nel posto giusto. Questa risposta è stata scritta come se fosse un pensiero interessante, non come una condivisione di esperienza.

La mia opinione è che per evitare il problema dell'oggetto parzialmente costruito (o, "stato"), è necessario innanzitutto modificare fondamentalmente il modo in cui viene eseguita la validazione dei parametri (precondizione) .

Il cambiamento è questo: invece di convalidare e assegnare i parametri uno per uno, si deve eseguire la convalida completa di tutti i parametri insieme, in modo da non avere effetti collaterali .

Oltre a questa modifica, viene modificato anche il ruolo del costruttore di classi. Invece di gestire sia la convalida della precondizione che l'inizializzazione dello stato, "esternalizzerà" entrambi a qualcun altro; manterrà solo la parte delle responsabilità che sono "failproof" (non in grado di fallire).

Ad esempio, l'assegnazione di un valore primitivo (ad esempio intero) a una variabile primitiva è a prova di errore, a condizione che la variabile primitiva abbia una memoria valida. Un altro esempio è il trasferimento di proprietà di un puntatore da una variabile intelligente a un'altra.

Alcuni dei più grandi progressi di C ++ 11 sono che i puntatori intelligenti (e molte altre cose) si stanno muovendo verso il rendere almeno alcune delle operazioni "a prova di errore", dando la possibilità di isolare quelle "disponibili" (avendo il potenziale di fallire) operazioni in metodi separati.

In definitiva, tuttavia, devo dire che la regola "nessuna eccezione" a volte è poco pratica per almeno alcuni tipi di sviluppo di applicazioni. In quale altro modo si impedisce std::bad_alloc senza eccezioni? Dovrebbe il sistema crash-and-burn ?

I sistemi mission-critical prevengono i problemi di memoria esaurita garantendo un determinismo a livello di sistema nell'utilizzo della memoria. Tutto è preallocato; gli oggetti sono semplicemente placement-newed sugli allocatori. Esiste un numero massimo di istanze prescritte per ciascun tipo di oggetti; il tentativo di superare il massimo verrà rifiutato o provocherà lo yank di un altro oggetto meno importante, non attivamente in uso.

Questo potrebbe essere il motivo per cui continuiamo a sentire quei meme su "questo sistema di tracciamento nemico è in grado di tracciare simultaneamente 256 oggetti diversi". Quando un 257 ° oggetto vuole essere aggiunto al sistema, deve passare uno degli oggetti meno importanti. Dato che nessuno di noi commenta qui ha visto alcun codice, questa è solo una speculazione.

    
risposta data 05.04.2015 - 02:44
fonte
3

Default c++ constructors should be avoided if that means leaving object in a partially constructed place. This is obvious a good advice but things get complicated when you also disallow exceptions (as it does)

Non necessariamente. Considera queste regole:

  • un costruttore dovrebbe ricevere argomenti già validati e non eseguire operazioni al di fuori dell'inizializzazione / spostamento di valori (la responsabilità del costruttore è di inizializzare l'oggetto, non di calcolare i valori, quindi inizializzare l'oggetto - che spezzerebbe SRP).

  • se la costruzione di un oggetto non è banale, la parte computazionale dovrebbe essere spostata in una funzione di fabbrica

Implementazione di esempio:

class A {
public:
    A() = deleted; // no way to construct instance that completely initializes A

    // int Init(); // don't use two-phased construction; it imposes on client code

    A(B* b, C* c) : b_{ b }, c_{ c } {}

private:
    B* b_;
    C* c_;
};

enum result { success, error };

// return error code
result make_A(B_and_Q_store& store, A*& instance)
{
    B * b = store.buy_a_b();
    C * c = store.get_q().make_a_c();
    if(!C)              // ERROR HANDLING
        return error;   // ERROR HANDLING
    instance = new A{ b, c };
    return success;
}

Codice cliente:

A* a = nullptr;
if(success == make_A(some_store, a)) // ERROR HANDLING
{
    // ...
}

Questa implementazione offre praticità e buon design per il codice client:

  • make_A esegue calcoli fessibili;

  • Il costruttore
  • contiene solo codice banale;

  • legge di demeter è rispettata: poiché un'istanza di A usa solo B e C, non dovrebbe sapere (o dipendere da) come B e C sono costruiti (perché non è più responsabile per la loro istanziazione) .

Simile a ciò che @rwong afferma (quando menziona gli effetti collaterali e la convalida), i valori parametro / argomento vengono convalidati come necessario, prima di creare l'istanza A (ovvero, quando si crea l'istanza, sai già tutto i valori dei parametri sono validi).

    
risposta data 07.04.2015 - 13:04
fonte
0

Un modo per gestirli è avere le funzioni di fabbrica:

expected<Missile> Missile::Create(Params...)
expected<unique_ptr<Missile>> Missile::Create(Params...)

class Missile {
  private:
   Missile() = default;
   int Initialize(int power) // second phase constructor {
     // other operations (besides out_of_memory) that may fail
     // can be reported from this function
     expected<PowerSupply> expectedPower = PowerSupply::Create(power);
     if (!expectedPower.valid()) {
       return expectedPower.get_error();
     }
     this->power = std::move(expectedPower); // from now on power.valid() true
     return SUCCESS;

  public:
  static expected<Missile> Missile::Create(int p) {
    // 1. call default constructor first, this is automatic
    // storage so no fail
    // 2. Initialize called. 
    // If failure - report expected<Missile>::from_error(Initialize_STATUS);
    Missile m;
    auto status = m.Initialize(p);
    if (status != 0) return expected<Missile>::from_error(status);
    return Missile; // implicit conversion from T to expected<T>
  }
  static expected<unique_ptr<Missile>> Missile::Create(int p) {
    // 1. use operator new to construct - may fail -> expected::from_error(OUT_OF_MEMMORY)
    // 2. call Initialize and possibly report second phase errors
  }

  private:
  int power;
  expected<PowerSupply> power;
}

Questo è più pulito dell'usare i parametri e un oggetto complesso è in grado (usando 2 fasi) di costruire le sue dipendenze, sia come variabile automatica sia come memoria libera allocata.

Il costruttore predefinito di classe previsto assegna solo l'archiviazione automatica per T. Si può creare un'istanza prevista inizializzata assegnando un'istanza T a expected<T>

    
risposta data 08.04.2015 - 21:43
fonte

Leggi altre domande sui tag