Argomenti contro il metodo “initialize ()” al posto dei costruttori

Attualmente sono incaricato di trovare tutte le cattive pratiche nella nostra base di codice e di convincere i miei colleghi a risolvere il codice incriminato. Durante la mia speleologia, ho notato che molte persone qui usano il seguente schema:

class Foo { public: Foo() { /* Do nothing here */ } bool initialize() { /* Do all the initialization stuff and return true on success. */ } ~Foo() { /* Do all the cleanup */ } }; 

Ora potrei sbagliarmi, ma per me questa cosa del metodo initialize() è orribile. Credo che annulli l’intero scopo di avere costruttori.

Quando chiedo ai miei colleghi perché è stata presa questa decisione di progettazione, rispondono sempre che non hanno scelta perché non puoi uscire da un costruttore senza lanciare (suppongo che assumano che il lancio è sempre negativo).

Non sono riuscito a convincerli fino ad ora e ammetto che potrei non avere argomenti validi … quindi ecco la mia domanda: ho ragione che questo costrutto è un dolore e se sì, quali problemi vedi in esso?

Grazie.

Sia l’inizializzazione in singolo step (costruttore) sia l’inizializzazione in due fasi (con un metodo init) sono schemi utili. Personalmente ritengo che escludere entrambi sia un errore, anche se se le tue convenzioni vietano completamente l’uso di eccezioni, proibisci l’inizializzazione a step singolo per costruttori che possono fallire.

In generale, preferisco l’inizializzazione a passo singolo perché ciò significa che i tuoi oggetti possono avere invarianti più forti. Io uso solo un’inizializzazione in due fasi quando la considero significativa o utile affinché un object possa esistere in uno stato “non inizializzato”.

Con l’inizializzazione in due passaggi è valido che il tuo object si trovi in ​​uno stato non inizializzato, quindi ogni metodo che funziona con l’object deve essere consapevole e gestire correttamente il fatto che potrebbe trovarsi in uno stato non inizializzato. Questo è analogo a lavorare con i puntatori, dove è una ctriggers forma assumere che un puntatore non sia NULL. Viceversa, se si esegue l’inizializzazione nel costruttore e si fallisce con le eccezioni, è ansible aggiungere “l’object è sempre inizializzato” alla propria lista di invarianti, e quindi diventa più facile e più sicuro fare ipotesi sullo stato dell’object.

Questo è generalmente noto come Inizializzazione a due fasi o multifase ed è particolarmente negativo perché una volta che una chiamata del costruttore è terminata con successo, si dovrebbe avere un object pronto per l’uso, in questo caso non si avrà un object pronto all’uso.

Non posso fare a meno di sottolineare più su quanto segue:
Lanciare un’eccezione dal costruttore in caso di errore è il migliore e l’unico modo conciso per gestire gli errori di costruzione degli oggetti.

Dipende dalla semantica del tuo object. Se l’inizializzazione è qualcosa di cruciale per la struttura dei dati della class stessa, un errore potrebbe essere gestito meglio lanciando un’eccezione dal costruttore (ad esempio se si è fuori memoria) o da un’asserzione (se si sa che il tuo codice non dovrebbe mai fallire, mai).

D’altra parte, se il successo o meno della costruzione dipende dall’input dell’utente, allora l’errore non è una condizione eccezionale, ma piuttosto parte del normale comportamento di runtime previsto che è necessario testare. In questo caso, dovresti avere un costruttore predefinito che crea un object in uno stato “non valido” e una funzione di inizializzazione che può essere richiamata in un costruttore o in un secondo momento e che potrebbe avere o meno successo. Prendi std::ifstream come esempio.

Quindi uno scheletro della tua class potrebbe assomigliare a questo:

 class Foo { bool valid; bool initialize(Args... args) { /* ... */ } public: Foo() : valid(false) { } Foo(Args... args) : valid (false) { valid = initialize(args...); } bool reset(Args... args) // atomic, doesn't change *this on failure { Foo other(args...); if (other) { using std::swap; swap(*this, other); return true; } return false; } explicit operator bool() const { return valid; } }; 

Dipende dal caso.

Se un costruttore può fallire a causa di alcuni argomenti, deve essere generata un’eccezione. Ma, naturalmente, è necessario documentarsi sull’eliminazione di eccezioni dai costruttori.

Se Foo contiene oggetti, saranno inizializzati due volte, una volta nel costruttore, una volta nel metodo di initialize , quindi questo è uno svantaggio.

IMO, il più grande svantaggio è che devi ricordare di chiamare l’ initialize . Qual è il punto di creare un object se non è valido?

Quindi, se la loro unica argomentazione è che non vogliono lanciare eccezioni dal costruttore, è un argomento piuttosto negativo.

Se, tuttavia, vogliono una sorta di inizializzazione pigra, è valida.

È un dolore, ma non hai altra scelta se vuoi evitare di lanciare un’eccezione dal costruttore. C’è anche un’altra opzione, altrettanto dolorosa: fai tutte le inizializzazioni nel costruttore e poi devi controllare se l’object è stato costruito con successo (ad esempio, l’operatore di conversione al metodo IsOK o IsOK ). La vita è dura, ….. e poi muori 🙁