Progettazione della classe C ++ con invariante

7

Ho riflettuto su una domanda di base su in che misura imporre l'invarianza di una classe . Forse è formulato male, così come un esempio, diciamo che voglio scrivere una classe che memorizza una gamma limitata di colori. Il costruttore della classe dovrebbe prendere le dimensioni della tavolozza - l'idea è che puoi aggiungere tutti i colori alla tavolozza che vuoi, ma limita la dimensione su una politica FIFO. Quindi diremo che voglio limitare arbitrariamente questa dimensione all'intervallo da 1 a 32 ("nessuno avrà mai bisogno di più di 32 colori").

Iniziamo ...

class palette
{
public:
   palette(unsigned int max_size) : m_max_size{max_size} { }
private:
   unsigned int m_max_size;
};

Finora, tutto bene. Tranne che non sto facendo nulla per confermare che la dimensione massima rientri nel mio intervallo ristretto, cioè l'invariante può essere rotto direttamente dalla costruzione.

(Giusto per chiarire: questa classe non è intesa per l'uso in alcune librerie pubbliche - è semplicemente una classe interna a una singola applicazione)

Ti vengono in mente quattro opzioni oltre "non fare nulla":

  1. Inserisci un commento sulla classe per chiedere ai chiamanti di usare correttamente la classe.

    // Please only call with 1 <= max_size <= 32
    
  2. Asserisci che si trova nel raggio d'azione.

    assert(max_size >= 1 && max_size <= 32);
    
  3. Genera un'eccezione se è al di fuori dell'intervallo

    if (max_size < 1 || max_size > 32)
        throw std::invalid_argument();
    
  4. Verifica il compilatore convertendolo in un modello

    template <unsigned int MAX>
    class palette
    {
    public:
        palette() { // no need for the argument any more
            static_assert(MAX >= 1 && MAX <= 32, "oops");
        }
    };
    

Le opzioni n. 1 sembrano un po 'come un pio desiderio, ma forse è abbastanza buono per "solo" una classe interna? Le opzioni dalla 2 alla 4 sono sempre più esigenti riguardo alle dimensioni, al punto che la # 4 modifica la sintassi in modo che il compilatore riporti un errore se la classe non viene utilizzata correttamente.

Gradirei qualsiasi pensiero su questo. Capisco che la risposta inizierà probabilmente con "Beh tutto dipende ...", ma sto solo cercando alcune linee guida generali o alcuni suggerimenti alternativi.

    
posta WalderFrey 01.02.2016 - 12:01
fonte

2 risposte

4

Vorrei andare con un'affermazione. Senza ulteriori informazioni, probabilmente utilizzerò automaticamente non-static assert() .

Un commento sarebbe davvero un pio desiderio. Non solo può essere ignorato, ma può facilmente scomparire se (quando?) Decidi che 32 non dovrebbe più essere il limite.

Se si trattava di un'API pubblica con altri programmatori che la utilizzavano, l'eccezione sarebbe utile in quanto tende a fornire maggiori informazioni di debug, specialmente se includi un messaggio di errore significativo come "walderFrey :: la dimensione della tavolozza deve essere compresa tra 1 e 32 ". Altrimenti, potrebbero dover scavare nel codice sorgente e imparare come funziona la tua biblioteca solo per capire perché quella affermazione casuale ha fallito. Ma dal momento che questo è solo per uso personale, non c'è tanto beneficio in questo. E si potrebbe anche ottenere il beneficio (certamente piccolo) di avere il assert() scomparire nelle build non di debug.

Ho anche sentito dire che le eccezioni dovrebbero indicare condizioni imprevedibili ed eccezionali che potrebbero non essere ripetibili o potrebbero non essere risolvibili, mentre le asserzioni dovrebbero indicare errori del programmatore che possono e devono essere assolutamente fisso. Sono generalmente d'accordo con questa linea guida quando non ci sono API pubbliche di cui preoccuparsi.

La soluzione template è pulita, ma ciò solleva una domanda più grande: una tavolozza di taglia 1 e una palette di dimensioni 32 possono essere considerate dello stesso tipo? Ha senso assegnare uno all'altro? Se non ha senso, questo è un ulteriore vantaggio di implementare questa classe come modello. Ma non dovresti decidere se rendere questa classe un modello solo perché un static_assert() è leggermente più bello di un assert() . Un assert() regolare è decisamente "abbastanza buono". Normalmente, il codice generico che coinvolge i modelli è più complesso, quindi passerei automaticamente a una classe "normale" finché non sentissi la complessità giustificata, quindi raccomanderei assert() a meno che non ritieni che andare con i modelli sia giustificato per altri motivi.

    
risposta data 01.02.2016 - 12:33
fonte
1

Utilizza la più strong garanzia che sei disposto a mantenere per il prossimo futuro.

Il metodo 4 (template / static assert) assicura che l'invariante sia sempre vero in un programma compilato. Tuttavia, ciò implica che l'input deve essere noto al momento della compilazione, il che non è sempre possibile.

Il metodo 2 (asserzione) e 3 (eccezione) possono fallire in fase di esecuzione, quindi offrono una garanzia più debole del metodo 4. La scelta tra un'asserzione e un'eccezione dipende dal pubblico (chi utilizzerà la classe), il standard di codifica applicabili e le possibili ripercussioni del mancato mantenimento dell'invarianza. Per un trattamento approfondito, consulta John Lakos - Programmazione difensiva eseguita a destra .

Utilizzare un'asserzione se si considera che si tratta di un errore logico (o programmatore), è improbabile che la violazione dell'invariant causi danni (ad esempio danneggiamento dell'hardware, danneggiamento dei dati) e non si possa fare nulla per evitare l'errore senza cambiare / ricompilando il codice. Normalmente le asserzioni vengono verificate solo in una build di debug, non in build di rilascio.

Genera un'eccezione se l'errore può essere causato da "circostanze ambientali" (ad esempio errore di rete) o, in alcuni casi, da input non validi o se continuare nonostante l'errore può causare danni. Ovviamente, l'eccezione dovrebbe essere gestita da qualche parte nel codice chiamante, il che significa essenzialmente che lanciando un'eccezione rendi la semantica della tua classe più complessa.

Il metodo 1 (commento) è la documentazione del codice e questo dovrebbe sempre essere fatto in aggiunta a uno degli altri metodi. Non si vuole forzare qualcuno che usa questa classe a dover guardare il codice sorgente per capire come usarlo. Questo dovrebbe essere chiaro dalla documentazione. Se gestisci l'errore generando un'eccezione, dovresti documentarlo anche.

    
risposta data 11.02.2016 - 11:59
fonte

Leggi altre domande sui tag