2009-07-21 14 views
23

Si prega di notare che si tratta di una domanda sui costruttori, non sulle classi che gestiscono il tempo.Come gestire valori errati in un costruttore?

Supponiamo che io ho una classe come questa:

class Time 
{ 
protected: 
    unsigned int m_hour; 
    unsigned int m_minute; 
    unsigned int m_second; 
public: 
    Time(unsigned int hour, unsigned int minute, unsigned int second); 
}; 

Mentre io vorrei una da costruire con successo, vorrei il costruttore di b per sicuro.

Time a = Time(12,34,56); 
Time b = Time(12,34,65); // second is larger than 60 

Tuttavia, questo non è possibile, perché i costruttori non restituiscono alcun valore e sarà sempre successo.

Come potrebbe il costruttore dire al programma che non è felice? Ho pensato ad alcuni metodi:

  1. fare in modo che il costruttore lanci un'eccezione e abbia gestori nella funzione di chiamata per gestirlo.
  2. avere un flag nella classe e impostarlo su true solo se i valori sono accettabili dal costruttore e fare in modo che il programma controlli il flag immediatamente dopo la costruzione.
  3. hanno una funzione separata (probabilmente statica) da chiamare per verificare i parametri di input immediatamente prima di chiamare il costruttore.
  4. ridisegnare la classe in modo che possa essere costruita da qualsiasi parametro di input.

Quale di questi metodi è più comune nell'industria? O c'è qualcosa che potrei aver perso?

+0

possibili duplicati di [Come gestire l'errore nel costruttore in C++?] (Http://stackoverflow.com/questions/4989807/how-to-handle-failure-in-constructor-in-c) –

risposta

38

La soluzione tipica consiste nel generare un'eccezione.

La logica alla base di ciò è la seguente: il costruttore è un metodo che trasforma un blocco di memoria in un oggetto valido. O ha successo (finisce normalmente) e hai un oggetto valido o hai bisogno di un indicatore non ignorabile di un problema. Le eccezioni sono l'unico modo per rendere il problema non ignorabile in C++.

+0

Grazie per il vostro chiarimento . – Andy

+0

la risposta qui sotto per rendere gli argomenti del ctor ora, minuto e secondo tipo è molto meglio che consentire gli ints. Lanciare eccezioni per questo è ridicolo. Puoi prenderlo al momento della compilazione. – Tim

+2

@tim: Ovviamente non è per questo che usiamo le eccezioni. Ammetto che questo esempio è un po 'forzato (e ci sono altre soluzioni per questo problema). Ma nel caso generale se un costruttore ha degli input sbagliati DEVI lanciare un'eccezione. L'altra scelta è di avere un oggetto non valido. Un oggetto non valido non obbedisce a nessun vincolo applicato ai suoi metodi. Quindi la vera soluzione qui: gli input dell'utente vengono convalidati (i dati cattivi generano un messaggio di errore) quindi utilizzati nel costruttore (che generano se la validazione fallisce (che è buona)). –

10

C'è un altro modo possibile. Non sto dicendo che questo sia in qualche modo preferito, solo aggiungendolo per completezza:

Creare una funzione factory che crea un'istanza della classe sull'heap e restituisce un puntatore nullo se la creazione fallisce.

Questo non è proprio appropriato con oggetti tipo valore come date, ma potrebbero esserci applicazioni utili.

+0

Grazie per la tua idea. Sarà utile in alcune circostanze, come hai detto tu. – Andy

+0

In C++ l'implementazione più ovvia di questa funzione di fabbrica è la nuova forma di operatore new. Ad esempio, esegui solo l'allocazione dell'heap. Questo è uno dei tanti motivi per cui praticamente ogni altra lingua alloca gli oggetti solo sull'heap. (Non di advocacy, solo spunti di riflessione.) – quark

1

Il primo è il migliore, le eccezioni sono il modo migliore per informare gli utenti della classe sugli errori.

non è consigliabile in un altro modo, perché se il costruttore restituisce senza erros, significa che hai costruito un oggetto correttamente e si può usare ovunque

"Eccezione generata da un C'tor"
5

non è un quattro lettera Se un oggetto non può essere creato correttamente, il lettore dovrebbe fallire, perché preferiresti fallire nella costruzione piuttosto che avere un oggetto non valido.

+1

Sono completamente d'accordo, molti sviluppatori di C++ pensano erroneamente che i risultati delle eccezioni nei costruttori siano indefiniti e adottino una costruzione a due fasi con un metodo di inizializzazione. – iain

12

Normalmente direi (1). Ma se scopri che i chiamanti stanno tutti circondando la costruzione con try/catch, allora puoi fornire anche la funzione di helper statico in (3), poiché con un po 'di preparazione l'eccezione può essere resa impossibile.

C'è un'altra opzione, anche se ha conseguenze significative per la codifica stile in modo da non dovrebbe essere adottato con leggerezza,

5) Non passare i parametri al costruttore:

class Time 
{ 
protected: 
    unsigned int m_hour; 
    unsigned int m_minute; 
    unsigned int m_second; 
public: 
    Time() : m_hour(0), m_minute(0), m_second(0) {} 
    // either return success/failure, or return void but throw on error, 
    // depending on why the exception in constructor was undesirable. 
    bool Set(unsigned int hour, unsigned int minute, unsigned int second); 
}; 

Si chiama due fase-costruzione, ed è usato con precisione in situazioni in cui non è auspicabile o impossibile per i costruttori generare eccezioni. Il codice che utilizza nothrow new, da compilare con -fno-exceptions, è probabilmente il caso classico. Una volta che ci si abitua, è leggermente meno fastidioso di quanto si possa pensare a prima vista.

+4

La costruzione a due fasi non è buona, poiché l'utente della classe è destinato a dimenticarlo. Questo esp interferisce con l'idioma Immutable Value che una classe come "Time" dovrebbe implementare. –

+1

Se l'utente della classe lo dimentica, come accade molto occasionalmente, il problema è con l'utente. Seriamente, cosa li ha spinti a pensare che un costruttore di 0-arg possa dare il "tre e un quarto" ?. È difficile solo nei casi in cui la seconda fase è anche 0-arg e assegna solo risorse. Se utilizzi 2 fasi in qualsiasi luogo, dovresti usarlo ovunque, quindi quando il chiamante lo dimentica, tratta il debug risultante come un'esperienza di apprendimento. Sono d'accordo che è spazzatura avere tipi quasi immutabili, e l'opzione 5 non è la migliore per quelli. Come ho detto, preferisco 1 o 3. –

+0

Oh btw, non ho assunto questo tipo di tempo è immutabile, dal momento che le variabili membro protetto non sono const. È perfettamente ragionevole avere un tipo Time mutabile, ad esempio con un operatore + = (const TimeDelta &) e quant'altro, proprio come è ragionevole avere una stringa mutabile e un intero mutabile (int). Quindi non credo che tutte le classi del tempo "dovrebbero" essere immutabili. A volte è utile avere tipi immutabili. –

1

Solo per elaborare un po 'sulle risposte fornite da onebyone e Timbo. Quando le persone discutono sull'uso delle eccezioni, di solito qualcuno alla fine dice: "Le eccezioni dovrebbero essere usate in situazioni eccezionali".

Come si può notare dalla maggior parte delle risposte qui, se un costruttore non riesce, la risposta corretta è di generare un'eccezione. Tuttavia, nel tuo caso, non è necessariamente che tu possa creare creando l'oggetto, è più che non lo vuoi vuoi per crearlo.

Qualora i valori vengono letti da una sorgente esterna (ad es. Un file o un flusso) c'è una buona probabilità che saranno ricevuti valori non validi e, in tal caso, allora non è davvero un eccezionale situazione .

Personalmente, preferirei convalidare gli argomenti prima di costruire l'oggetto tempo (qualcosa come Timbo's risposta) e avrei quindi un'asserzione nel costruttore per verificare che gli argomenti siano validi.

Nel caso in cui il costruttore abbia bisogno di una risorsa (ad esempio, alloca memoria), allora, IMHO, sarebbe più vicino a una situazione eccezionale e quindi si genererebbe un'eccezione.

3
  • avere il costruttore un'eccezione, e hanno i gestori della funzione per gestire la cosa chiamata.

Sì. Design by Contract e lasciare il precondizionamento attivo e, in caso di errore, lanciare un'eccezione. Nessun orario non valido più.

  • una bandiera nella classe e impostarlo vero solo se i valori sono accettabili dal costruttore, e avere il programma controlla il flag immediatamente dopo la costruzione.

Forse. Accettabile in casi complessi, ma ancora una volta, lancia se il tuo controllo fallisce.

  • hanno una funzione separata (probabilmente statica) da chiamare per verificare i parametri di ingresso immediatamente prima di chiamare il costruttore.

Forse. Si tratta di dire se i dati inseriti sono corretti o meno, e può essere utile se si dice che non è banale, ma si veda sopra per come reagire in caso di dati non validi.

  • ridisegnare la classe in modo che possa essere costruito da qualsiasi parametro di input.

No. In pratica, si rimanda il problema.

3

In genere si dispone di un costruttore privato/protetto e di un metodo di produzione statica pubblica per creare l'oggetto Ora. Non è una buona idea lanciare un'eccezione da un costruttore perché provoca il caos nell'ereditarietà. In questo modo il tuo metodo factory può generare un'eccezione se necessario.

+0

YES! Ecco una risposta CORRETTA finalmente. Usa un metodo statico per chiamare qualcosa come Time.createTime (12,34,65) e può restituire null poiché i valori non sono validi, mentre Time.createTime (12,34,56) restituirebbe un nuovo Time impostato a quelli valori. (anche in questo caso non dimenticare di rendere privato il tuo costruttore) – Ricket

+1

@Ricket: questo non impedisce la creazione di istanze automatiche? –

+3

@Manjit: Potresti approfondire la parte "devastazione dell'eredità"? –

31

Un'altra alternativa, per completezza:

  • riprogettare l'interfaccia in modo che i valori non validi sono "impossibili"

Nella classe "Time", per esempio, si potrebbe avere:

class Time{ 
public: 
    Time(Hours h, Minutes m, Seconds s); 
//... 
}; 

Ore, minuti e secondi essendo valori delimitati. Ad esempio, con il (non ancora) Boost biblioteca Valore vincolata:

typedef bounded_int<unsigned int, 0, 23>::type Hours; 
typedef bounded_int<unsigned int, 0, 59>::type Minutes; 
typedef bounded_int<unsigned int, 0, 59>::type Seconds; 
+0

Vorrei poterlo votare più volte – Tim

+1

Il problema è che le cose che non dovrebbero mai accadere hanno una fastidiosa abitudine di accadere comunque. Kernighan & Plaugher, in "Software Tools", sosteneva di mettere "non può accadere" le affermazioni printf da nessuna parte che il codice potesse rilevare una situazione che teoricamente non poteva accadere. Hanno riferito di essere costernati e umiliati al numero di volte in cui hanno visto "non può accadere" stampato come risultato diretto. –

+4

@tim: E come pensi di gestire Hours (n) quando n è 30? In tal caso il * Ore ctor * deve lanciare un'eccezione! Lavorare all'interno del sistema, che in questo caso significa che i medici devono o lanciare o avere successo, è difficile da evitare. (È certamente possibile che questa sia una soluzione globale migliore --- difficile da capire da un esempio inventato --- ma il punto è che non si evitano le eccezioni.) –

1

Non credo di avere molta scelta.

Se si riceve un input non valido, non è possibile fare molto più del segnale al chiamante che non è valido. È quindi possibile utilizzare un'eccezione o un codice di errore.

Il codice di errore in un costruttore dovrebbe essere passato come parametro di riferimento e in quanto tale apparirebbe molto atroce.

Si potrebbe provare a scoprire come è possibile convalidare gli input all'origine. Perché stai ricevendo input non validi esattamente? In che modo l'input non può essere valido, ecc.

Un esempio per la classe data potrebbe essere forzare l'utente (utente del programma) a immettere solo una data valida (obbligandolo a inserirla in una GUI di tipo calendario, per esempio).

Si potrebbe anche provare a creare un metodo nella propria classe per gestire la convalida dell'input.

Con ciò, l'utente (questa volta il programmatore) può chiamarlo prima della costruzione e assicurarsi che la chiamata non fallirà o ricorrere all'eccezione se l'utente non l'ha convalidato.

Se le prestazioni sono importanti e non si desidera chiamare la funzione di convalida due volte (l'utente chiama, quindi nel costruttore), penso che si possa usare l'idioma del costruttore nominato per avere un CheckedConstructor e un UncheckedConstructor.

Questo però sta cominciando a essere un esagerazione architettonica, penso.

Alla fine, dipenderà dalla classe e dal caso d'uso.

-1

consideri un modello di fabbrica, come per la generazione di oggetti di tempo:

static bool Time::CreateTime(int hour, int min, int second, Time *time) { 
    if (hour <= 12 && hour >= 0 && min < 60 && min >= 0 && 
     second < 60 && second >= 0) { 
    Time t(hour, min, second); 
    *time = t; 
    return true; 
    } 
    printf("Your sense of time seems to be off"); 
    return false; 
} 

Time t; 
if (Time::CreateTime(6, 30, 34, &t)) { 
    t.time(); // :) 
} else { 
    printf("Oh noes!"); 
    return; 
} 

Questo rende il presupposto che il tempo ha:

  • un default costruttore
  • un costruttore di copia
  • una copia operatore di assegnazione
Problemi correlati