Come gestire i casi di errore nel costruttore di classi C ++?

21

Ho una classe CPP il cui costruttore fa alcune operazioni. Alcune di queste operazioni potrebbero non riuscire. So che i costruttori non restituiscono nulla.

Le mie domande sono,

  1. È permesso fare alcune operazioni oltre che inizializzare i membri in un costruttore?

  2. È possibile dire alla funzione chiamante che alcune operazioni nel costruttore sono fallite?

  3. Posso rendere new ClassName() restituire NULL se si verificano errori nel costruttore?

posta MayurK 27.07.2016 - 11:30
fonte

3 risposte

42
  1. Sì, anche se alcuni standard di codifica potrebbero vietarlo.

  2. Sì. Il modo consigliato è di fare un'eccezione. In alternativa, è possibile memorizzare le informazioni di errore all'interno dell'oggetto e fornire metodi per accedere a queste informazioni.

  3. No.

risposta data 27.07.2016 - 11:34
fonte
20

Potresti creare un metodo statico che esegue il calcolo e restituisce un oggetto in caso di esito positivo o negativo.

A seconda di come è fatta questa costruzione dell'oggetto, potrebbe essere meglio creare un altro oggetto che consenta la costruzione di oggetti in un metodo non statico.

Chiamare indirettamente un costruttore viene spesso definito "fabbrica".

Ciò consentirebbe anche di restituire un oggetto nullo, che potrebbe essere una soluzione migliore del restituire null.

    
risposta data 27.07.2016 - 12:24
fonte
5

@SebastianRedl ha già fornito risposte semplici e dirette, ma potrebbe essere utile qualche spiegazione aggiuntiva.

TL; DR = c'è una regola di stile per mantenere semplici i costruttori, ci sono delle ragioni, ma queste ragioni si riferiscono principalmente a uno stile di codifica storico (o semplicemente cattivo). La gestione delle eccezioni nei costruttori è ben definita e i distruttori saranno ancora chiamati per variabili e membri locali completamente costruiti, il che significa che non dovrebbero esserci problemi nel codice C ++ idiomatico. La regola dello stile persiste comunque, ma normalmente non è un problema - non tutte le inizializzazioni devono essere nel costruttore, e in particolare non necessariamente quel costruttore.

È una regola di stile comune che i costruttori dovrebbero fare il minimo possibile per impostare uno stato valido definito. Se la tua inizializzazione è più complessa, dovrebbe essere gestita al di fuori del costruttore. Se non è disponibile un valore di inizializzazione economico che il costruttore può impostare, è necessario indebolire gli invarianti applicati dalla classe per aggiungerne uno. Ad esempio, se l'allocazione dello spazio di archiviazione per la classe da gestire è troppo costosa, aggiungi uno stato non assegnato ma negativo, perché ovviamente gli stati del caso speciale come null non hanno mai causato problemi a nessuno. Ahem.

Anche se comune, certamente in questa forma estrema è molto lontano dall'assoluto. In particolare, come indica il mio sarcasmo, sono nel campo che dice che gli invarianti indeboliti sono quasi sempre un prezzo troppo alto. Tuttavia, ci sono delle ragioni alla base della regola di stile e ci sono modi per avere invarianti forti sia per i costruttori che per minimi.

I motivi si riferiscono alla pulizia automatica del distruttore, in particolare in presenza di eccezioni. Fondamentalmente, ci deve essere un punto ben definito quando il compilatore diventa responsabile della chiamata ai distruttori. Mentre sei ancora in una chiamata del costruttore, l'oggetto non è necessariamente completamente costruito, quindi non è valido chiamare il distruttore per quell'oggetto. Pertanto, la responsabilità per la distruzione dell'oggetto si trasferisce al compilatore solo quando il costruttore si completa correttamente. Questo è noto come RAII (l'allocazione delle risorse è inizializzazione), che in realtà non è il nome migliore.

Se si verifica un'eccezione all'interno del costruttore, qualsiasi elemento costruito in parte deve essere ripulito in modo esplicito, in genere in try .. catch .

Tuttavia, i componenti dell'oggetto già costruiti con successo sono già responsabilità dei compilatori. Ciò significa che, in pratica, non è davvero un grosso problema. per es.

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

Il corpo di questo costruttore è vuoto. Fintanto che i costruttori di base1 , member2 e member3 sono eccezionalmente sicuri, non c'è nulla di cui preoccuparsi. Ad esempio, se il costruttore di member2 genera, il costruttore è responsabile della pulizia di se stesso. La base base1 era già completamente costruita, quindi il suo distruttore verrà automaticamente chiamato. member3 non è mai stato nemmeno parzialmente costruito, quindi non ha bisogno di pulizia.

Anche quando c'è un corpo, le variabili locali che sono state completamente costruite prima che l'eccezione venga lanciata verranno automaticamente distrutte, proprio come qualsiasi altra funzione. I corpi dei costruttori che manipolano i puntatori grezzi o "possiedono" una sorta di stato implicito (archiviato altrove) - che in genere significa che una chiamata di funzione di inizio / acquisizione deve essere abbinata a una chiamata di fine / rilascio - possono causare problemi di sicurezza di eccezione, ma il vero problema non riesce a gestire correttamente una risorsa tramite una classe. Ad esempio, se sostituisci i puntatori grezzi con unique_ptr nel costruttore, il distruttore per unique_ptr verrà chiamato automaticamente se necessario.

Ci sono ancora altre ragioni che le persone danno per preferire i costruttori del minimo. Uno è semplicemente perché esiste la regola di stile, molte persone assumono che le chiamate del costruttore siano economiche. Un modo per avere questo, ma avere ancora invarianti forti, è avere una classe factory / builder separata che abbia gli invarianti indeboliti e che configuri il valore iniziale necessario usando (potenzialmente molti) normali chiamate di membri. Una volta ottenuto lo stato iniziale necessario, passare quell'oggetto come argomento al costruttore per la classe con gli invarianti forti. Questo può "rubare il coraggio" dell'oggetto debole-invariante - spostare la semantica - che è un'operazione a buon mercato (e solitamente noexcept ).

Ovviamente puoi racchiuderlo in una funzione make_whatever () , quindi i chiamanti di quella funzione non avranno mai bisogno di vedere l'istanza della classe indebolita-invariante.

    
risposta data 27.07.2016 - 12:34
fonte

Leggi altre domande sui tag