Come gestire i casi di errore nel costruttore della classe C ++?


21

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

Le mie domande sono:

  1. È consentito eseguire alcune operazioni diverse dall'inizializzazione dei membri in un costruttore?

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

  3. Posso rendere new ClassName()NULL in caso di errori nel costruttore?


22
È possibile generare un'eccezione all'interno del costruttore. È un modello completamente valido.
Andy,

1
Probabilmente dovresti dare un'occhiata ad alcuni dei modelli creativi del GoF . Raccomando il modello di fabbrica.
SpaceTrucker,

2
Un esempio comune di # 1 è la convalida dei dati. IE se hai una classe Square, con un costruttore che accetta un parametro, la lunghezza di un lato, vuoi controllare se quel valore è maggiore di 0.
David dice Reinstate Monica il

1
Per la prima domanda, lascia che ti avverta che le funzioni virtuali possono comportarsi in modo non intuitivo nei costruttori. Lo stesso vale per i decostruttori. Fai attenzione a chiamarlo.

1
# 3 - Perché dovresti voler restituire un NULL? Uno dei vantaggi di OO NON è la verifica dei valori di ritorno. Cattura () le potenziali eccezioni appropriate.
MrWonderful,

Risposte:


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

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

  3. No.


4
A meno che l'oggetto non sia ancora in uno stato valido anche se una parte degli argomenti del costruttore non ha soddisfatto i requisiti e quindi è contrassegnato come errore, 2) non è davvero consigliato. È meglio quando un oggetto esiste in uno stato valido o non esiste affatto.
Andy,

@DavidPacker D'accordo, vedere qui: stackoverflow.com/questions/77639/… Ma alcune linee guida per la codifica vietano le eccezioni, il che è problematico per i costruttori.
Sebastian Redl,

In qualche modo ti ho già dato un voto per quella risposta, Sebastian. Interessante. : D
Andy,

10
@ooxi No, non lo è. Il nuovo ignorato viene chiamato per allocare memoria, ma la chiamata al costruttore viene effettuata dal compilatore dopo che l'operatore è tornato, il che significa che non si riesce a rilevare l'errore. Ciò presuppone che il nuovo sia chiamato affatto; non è per gli oggetti allocati in pila, che dovrebbe essere la maggior parte di essi.
Sebastian Redl,

1
Per # 1, RAII è un esempio comune in cui potrebbe essere necessario fare di più nel costruttore.
Eric,

20

È possibile creare un metodo statico che esegue il calcolo e restituisce un oggetto in caso di successo oppure no.

A seconda di come viene eseguita 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 null, che potrebbe essere una soluzione migliore rispetto alla restituzione di null.


Grazie @null! Sfortunatamente non posso accettare due risposte qui :( Altrimenti avrei accettato anche questa risposta !! Grazie ancora!
MayurK

@MayurK non preoccuparti, la risposta accettata non è quella di contrassegnare la risposta corretta, ma quella che ha funzionato per te.
null,

3
@null: in C ++, non puoi semplicemente tornare NULL. Ad esempio, in int foo() { return NULLte verrebbe effettivamente restituito 0(zero) un oggetto intero. Nel std::string foo() { return NULL; }caso avessi chiamato accidentalmente std::string::string((const char*)NULL)Undefined Behavior (NULL non punta a una stringa terminata \ 0).
Salterio

3
std :: optional potrebbe essere un po 'lontano ma potresti sempre usare boost :: optional se vuoi andare in quel modo.
Sean Burton,

1
@Vld: in C ++, gli oggetti non sono limitati ai tipi di classe. E con la programmazione generica, non è raro finire con le fabbriche per int. Ad esempio std::allocator<int>è una fabbrica perfettamente sana.
MSalters,

5

@SebastianRedl ha già dato le risposte semplici e dirette, ma alcune spiegazioni aggiuntive potrebbero essere utili.

TL; DR = c'è una regola di stile per mantenere semplici i costruttori, ci sono ragioni per questo, ma quelle ragioni si riferiscono principalmente a uno stile storico (o semplicemente cattivo) di codifica. La gestione delle eccezioni nei costruttori è ben definita e i distruttori saranno comunque chiamati per variabili e membri locali completamente costruiti, il che significa che non dovrebbero esserci problemi nel codice C ++ idiomatico. La regola di 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 assoluto possibile per impostare uno stato valido definito. Se l'inizializzazione è più complessa, deve essere gestita all'esterno del costruttore. Se non c'è un valore economico da inizializzare che il tuo costruttore può impostare, dovresti indebolire gli invarianti applicati dalla tua classe per aggiungerne uno. Ad esempio, se l'allocazione dello spazio di archiviazione per la classe da gestire è troppo costosa, aggiungi uno stato null non allocato, poiché ovviamente avere stati di casi speciali come null non ha mai causato problemi a nessuno. Ahem.

Sebbene comune, certamente in questa forma estrema è molto lontano dall'assoluto. In particolare, come indica il mio sarcasmo, sono nel campo che dice che indebolire gli invarianti è quasi sempre un prezzo troppo alto. Tuttavia, ci sono ragioni dietro la regola dello stile e ci sono modi per avere sia costruttori minimi che forti invarianti.

Le ragioni si riferiscono alla pulizia automatica del distruttore, in particolare a fronte di eccezioni. Fondamentalmente, ci deve essere un punto ben definito quando il compilatore diventa responsabile della chiamata dei 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à della distruzione dell'oggetto viene trasferita al compilatore solo quando il costruttore viene completato correttamente. Questo è noto come RAII (Resource Allocation Is Initialization) che non è proprio il nome migliore.

Se si verifica un'eccezione all'interno del costruttore, tutto ciò che è parzialmente costruito deve essere ripulito esplicitamente, in genere in a try .. catch.

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

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

Il corpo di questo costruttore è vuoto. Finché i costruttori base1, member2e member3sono eccezionalmente sicuri, non c'è nulla di cui preoccuparsi. Ad esempio, se il costruttore di member2lanci, quel costruttore è responsabile della pulizia. La base base1era già completamente costruita, quindi il suo distruttore verrà automaticamente chiamato. member3non è mai stato nemmeno parzialmente costruito, quindi non necessita di pulizia.

Anche quando c'è un corpo, le variabili locali che sono state completamente costruite prima che fosse generata l'eccezione verranno automaticamente distrutte, proprio come qualsiasi altra funzione. I corpi di costruttori che manipolano puntatori non elaborati o "possiedono" un qualche tipo di stato implicito (memorizzato 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 delle eccezioni, ma il vero problema lì non riesce a gestire correttamente una risorsa tramite una classe. Ad esempio, se si sostituiscono i puntatori non elaborati con unique_ptrnel costruttore, il distruttore per unique_ptrverrà chiamato automaticamente se necessario.

Ci sono ancora altri motivi per cui le persone preferiscono i costruttori più esigenti. Uno è semplicemente perché esiste la regola di stile, molte persone presumono che le chiamate del costruttore siano economiche. Un modo per ottenerlo, pur avendo ancora forti invarianti, è di avere una classe factory / builder separata che ha invece gli invarianti indeboliti e che imposta il valore iniziale necessario usando (potenzialmente molte) normali chiamate di funzione membro. Una volta che hai lo stato iniziale di cui hai bisogno, passa quell'oggetto come argomento al costruttore per la classe con gli invarianti forti. Ciò può "rubare le viscere" dell'oggetto debole-invariante - spostare la semantica - che è un'operazione economica (e di solito noexcept).

E ovviamente puoi includerlo in una make_whatever ()funzione, così i chiamanti di quella funzione non dovranno mai vedere l'istanza di classe degli invarianti indeboliti.


Il paragrafo in cui scrivi "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à di distruggere l'oggetto si trasferisce solo al compilatore quando il costruttore viene completato correttamente "potrebbe davvero utilizzare un aggiornamento relativo alla delega dei costruttori. L'oggetto viene completamente costruito al termine di qualsiasi costruttore derivato dalla maggior parte e il distruttore verrà chiamato se si verifica un'eccezione all'interno di un costruttore delegato.
Ben Voigt,

Pertanto, il costruttore "fai il minimo" può essere privato e la funzione "make_whatever ()" può essere un altro costruttore che chiama quello privato.
Ben Voigt,

Questa non è la definizione di RAII che conosco. La mia comprensione di RAII è di acquisire intenzionalmente una risorsa (e solo in) il costruttore di un oggetto e rilasciarlo nel suo distruttore. In questo modo, l'oggetto può essere utilizzato nello stack per gestire automaticamente l'acquisizione e il rilascio delle risorse che incapsula. L'esempio classico è un lucchetto che acquisisce un mutex quando viene costruito e lo rilascia alla distruzione.
Eric,

1
@Eric - Sì, è una pratica assolutamente standard - una pratica standard che viene comunemente chiamata RAII. Non sono solo io che estendo la definizione - è persino Stroustrup, in alcuni colloqui. Sì, RAII riguarda il collegamento tra i cicli di vita delle risorse e i cicli di vita degli oggetti, il modello mentale è la proprietà.
Steve314,

1
@Eric: risposte precedenti cancellate perché spiegate male. Comunque, gli oggetti stessi sono risorse che possono essere possedute. Tutto dovrebbe avere un proprietario, in una catena fino alla mainfunzione o alle variabili statiche / globali. Un oggetto allocato utilizzando new, non è di proprietà fino a quando si assegna questa responsabilità, ma puntatori intelligenti possiede gli oggetti heap allocata a cui fanno riferimento, e contenitori di possedere le loro strutture di dati. I proprietari possono scegliere di eliminare in anticipo, il distruttore proprietario è in definitiva responsabile.
Steve314,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.