Trattare con una variabile che è un numero, ma ha un significato maggiore? [chiuso]

0

Ho lavorato su un codice incorporato che gestisce una radio Bluetooth Low Energy (BLE). BLE ha 40 canali, numerati da 0 a 39. Una funzione per il driver radio prende il canale e imposta il registro appropriato con l'impostazione corretta.

Il prototipo della funzione ovvia era SetChannel(uint8_t channel); . Tuttavia, uint8_t può contenere numeri da 0 a 255, ma BLE ha solo 40 canali. Qualsiasi channel su 39 non è valido.

Che fa apparire la mia domanda: come si dovrebbe gestire una variabile che è chiaramente una cifra, ma ha un intervallo inferiore al tipo di archiviazione?

Ho pensato di usare un enum, come questo:

typdef enum channels
{
   Channel0,
   Channel1,
   Channel2,
   Channel3,
   ...
   Channel39
}BleChannel;

rendendo così la funzione prototipo SetChannel(BleChannel channel); , ma ciò non sembra corretto quando il tipo è chiaramente un numero, ma con un intervallo limitato. Lanciare un'eccezione o altrimenti terminare l'esecuzione non è una grande opzione poiché si tratta di un dispositivo incorporato.

    
posta CHendrix 27.07.2016 - 16:26
fonte

6 risposte

5

I numeri con intervallo limitato sorgono abbastanza spesso che (IMO) è utile aggiungere un tipo specifico per gestirli. Ho usato qualcosa su questo ordine diverse volte:

template <class T, T lower, T upper>
class bounded { 
    T val;
    void assure_range(T v) {
        if ( v < lower || upper <= v)
            throw std::range_error("Value out of range");
    }
public:
    bounded &operator=(T v) { 
        assure_range(v);
        val = v;
        return *this;
    }

    bounded(T const &v=T()) {
        assure_range(v);
        val = v;
    }

    operator T() { return val; }
};

Quindi dovresti fare qualcosa nell'ordine di:

using channel = bounded<uint8_t, 0, 39>;

Se passi solo un int8_t (o qualsiasi altra cosa), hai buone possibilità di finire con due problemi. Uno è che c'è un percorso attraverso il codice in cui il valore viene utilizzato senza che venga controllato il suo intervallo. L'altro è che perdi tempo ripetutamente controllando l'intervallo sullo stesso valore. Peggio ancora, se la base di codice è grande, si può facilmente finire con entrambi problemi contemporaneamente.

Tutti i soliti vantaggi di DRY si applicano qui. Centralizzando il controllo dell'intervallo in un punto, è facile assicurare che ciò che abbiamo è corretto, facile da modificare in futuro, se necessario, facile essere certi 1 che gli intervalli siano controllati ovunque debbano essere , senza aggiungere controlli superflui dei valori che sono già stati controllati. Soprattutto, ovviamente, otteniamo un codice leggibile, quindi quelle che avrebbero dovuto essere funzioni a una riga non finiscono con il codice reale sepolto da qualche parte sotto uno strato di controllo di intervallo che è irrilevante per il lavoro in questione.

Per quanto riguarda l'utilizzo della gestione delle eccezioni contro non va, la formulazione della domanda ("Lanciare un'eccezione o altrimenti terminare l'esecuzione ...") indica che l'OP ha una credenza errata riguardo alle eccezioni - che portano a terminare il programma. Questo non è assolutamente il caso. Un'eccezione può terminare un programma se non viene mai rilevato, ma lanciare e catturare un'eccezione può semplicemente trasferire il flusso di esecuzione dal codice che rileva un problema al codice che sa come gestirlo (senza che nessun livello intermedio debba essere a conoscenza di a tutti, oltre ad essere neutrali). In breve, questo è esattamente il tipo di situazione per cui è stata progettata la gestione delle eccezioni e in assenza di un motivo verificabile che veramente non può essere utilizzato qui, è chiaramente lo strumento giusto per il lavoro.

È, naturalmente, possibile che tu gestisca sempre un simile errore in un modo specifico, o che tu abbia una gamma limitata di opzioni che sono tutte note al momento della compilazione. In quest'ultimo caso, puoi gestire l'errore tramite una classe di strategia:

template <class T, T lower, T upper, class error> {

    // ...
    if ( v < lower || upper <= v)
        error("Value out of range");

Anche se il dispositivo rilasciato gestisce sempre questo errore in un modo specifico (ad esempio, utilizza un canale specifico) può essere utile utilizzare un parametro modello anziché codificare manualmente la gestione degli errori direttamente nel codice che rileva il problema . Ci sono almeno un paio di ovvi vantaggi:

  1. Potresti cambiare il solo ed unico modo di gestire il problema.
  2. È facile modificare il comportamento durante il test, quindi tali problemi causano un'interruzione immediata del debugger o almeno il problema.

1. Ovviamente qui sto scontando la possibilità che qualcuno faccia qualcosa del genere:

uint8_t bad_channel = 127;
channel &c = *reinterpret_cast<channel *>(&bad_channel);

Dato come viene definito C ++, è quasi impossibile impedire alle persone di spararsi ai piedi se si sforzano abbastanza. Allo stesso tempo, reinterpret_cast (o un equivalente cast di stile C) dovrebbe sempre essere esaminato con estrema cura: può essere usato per bypassare quasi qualsiasi assicurazione di tipo che provi a darti .

    
risposta data 27.07.2016 - 18:25
fonte
3

Non pensarci troppo.

  • Utilizza un numero intero.
  • Verifica il tuo codice in modo appropriato.

Passa ora alle parti del design che contano.

Con tutto il dovuto rispetto, questo è un codice molto semplice che sembra incredibilmente improbabile che porti a qualche bug. Ci sarà un codice che imposta il canale, o lo incrementa o decrementa, e questo codice deve consentire solo canali validi. È molto semplice da implementare e lo farai correttamente (dato che sei chiaramente coscienzioso di questo). E supponendo che tu abbia qualche test ragionevole in atto, questo si tradurrà in alcuni casi di test molto ovvi che potrebbero cogliere qualsiasi problema.

Sono le cose a cui non hai pensato che possano causare bug, non questo.

    
risposta data 27.07.2016 - 17:45
fonte
1

Se si tratta di un numero con un intervallo limitato, trattarlo come un numero. Rendi il tipo del parametro del canale int. Dal momento che non è possibile esprimere l'intervallo valido per tipo, non ha senso nemmeno provare. Se si passa un valore diverso da 0 a 39 è un bug di programmazione, aggiungere un asser nel codice di sviluppo in modo che venga catturato (il passaggio int ha il vantaggio che il passaggio di 256 verrà rilevato anche come errore); nel prodotto di spedizione probabilmente è meglio sostituire qualsiasi valore non valido con uno valido o riavviare; il tuo compito è determinare cosa è probabile che causi meno danni.

    
risposta data 27.07.2016 - 16:41
fonte
1

Partendo dal presupposto che il tuo codice può essere testato tramite un framework di test automatico sulla tua macchina di sviluppo e che il codice può essere strutturato in modo tale che la funzione SetChannel possa essere derisa / scambiata durante i test, prenderei il seguente approccio :

  1. Utilizza la firma SetChannel(uint8_t channel) .
  2. Non eseguire codifiche difensive in quella funzione; se ad esempio viene passato 40, allora lascia fallire il dispositivo, perché
  3. Simula il SetChannel nei tuoi test del resto del sistema con uno che causa il fallimento del test se ha superato un numero > 39 e quindi testare a fondo il resto del codice per assicurarsi che 40+ non venga mai passato alla funzione.

Assicurandosi che le chiamate a SetChannel si comportino da sole e superino solo un numero di canale valido, la necessità di preoccuparsi dei numeri fuori dall'intervallo va via.

    
risposta data 27.07.2016 - 16:53
fonte
0

Non credo che un enum sia l'ideale per due ragioni:

  • In realtà non impone la validità
  • A meno che non stiate codificando i numeri di canale nel vostro codice sorgente, non vi dà alcuna sicurezza rispetto a un numero intero. È comunque necessario analizzare l'input dell'utente in un identificatore di canale, che deve comunque gestire gli input non validi.

Considererei una classe personalizzata che

  • Ha una funzione per costruirlo dato un intero e / o una stringa. Questo dovrebbe essere un costruttore esplicito o un metodo statico. Probabilmente un metodo statico, poiché in questo modo puoi restituire un errore invece di lanciare un'eccezione.
  • Un modo per enumerare tutti i canali validi. Questo può essere usato per visualizzare i canali da selezionare in un'interfaccia utente (se ne hai uno).
  • Il più grande vantaggio di questo approccio non è che impone l'intervallo, ma fa passare accidentalmente alcuni int non collegati come identificatore di canale in un errore di compilazione. Dal momento che non è necessario eseguire operazioni aritmetiche sul canale, la classe personalizzata non ha bisogno di un codice di tipo boilerplate.
  • Quell'oggetto immutabile può avere proprietà di sola lettura per es. frequenza.

O usa solo una stringa int o even:

  • È l'approccio più semplice e le alternative non acquistano molta sicurezza nella pratica.
  • Se dispositivi diversi supportano un numero diverso di canali, puoi verificarne la validità solo quando lo associ a un dispositivo.
risposta data 27.07.2016 - 17:15
fonte
-2

Vorrei andare con questo:

void SetChannel( const int channel );

quindi controlla il valore del canale. Se è fuori portata, genera un'eccezione.

Avere un enum in questo caso è una soluzione terribile, poiché non porta nulla alla chiarezza del codice.

    
risposta data 27.07.2016 - 16:39
fonte

Leggi altre domande sui tag