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:
- Potresti cambiare il solo ed unico modo di gestire il problema.
- È 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 .