Omettere "distruttori" in C sta portando YAGNI troppo lontano?


9

Sto lavorando su un'applicazione media incorporata in C usando tecniche simili a quelle di OO. Le mie "classi" sono moduli .h / .c che usano strutture di dati e strutture di puntatori di funzioni per emulare incapsulamento, polimorfismo e iniezione di dipendenza.

Ora, ci si aspetterebbe che una myModule_create(void)funzione arrivi con una myModule_destroy(pointer)controparte. Ma quando il progetto viene incorporato, le risorse istanziate realisticamente non dovrebbero mai essere rilasciate.

Voglio dire, se ho 4 porte seriali UART e creo 4 istanze UART con i loro pin e impostazioni richiesti, non c'è assolutamente alcun motivo per voler mai distruggere UART # 2 ad un certo punto durante il runtime.

Quindi, seguendo il principio YAGNI (non ne avrai bisogno), dovrei omettere i distruttori? Mi sembra estremamente strano ma non riesco a pensare a un uso per loro; le risorse vengono liberate quando il dispositivo si spegne.


4
Se non fornisci un modo per disporre l'oggetto, stai trasmettendo un messaggio chiaro che hanno una durata "infinita" una volta creata. Se questo ha senso per la tua applicazione, dico: fallo.
glampert

4
Se hai intenzione di andare così lontano accoppiando il tipo al tuo particolare caso d'uso, perché anche avere una myModule_create(void)funzione? Potresti semplicemente codificare le istanze specifiche che prevedi di utilizzare nell'interfaccia che esponi.
Doval,

@Doval Ci ho pensato. Sono uno stagista che usa parti e parti di codice del mio supervisore, quindi sto cercando di destreggiarmi con "farlo nel modo giusto", sperimentando lo stile OO in C per esperienza e coerenza con gli standard dell'azienda.
Asics,

2
@glampert lo inchioda; Aggiungerei che dovresti rendere pulita la durata infinita prevista nella documentazione della funzione di creazione.
Blrfl,

Risposte:


11

Se non fornisci un modo per disporre l'oggetto, stai trasmettendo un messaggio chiaro che hanno una durata "infinita" una volta creata. Se questo ha senso per la tua applicazione, dico: fallo.

Glampert ha ragione; non c'è bisogno di distruttori qui. Avrebbero semplicemente creato un eccesso di codice e una trappola per gli utenti (l'uso di un oggetto dopo che il suo distruttore è stato chiamato è un comportamento indefinito).

Tuttavia, dovresti essere sicuro che non c'è davvero bisogno di smaltire gli oggetti. Ad esempio, è necessario disporre di un oggetto per un UART che non è attualmente in uso?


3

Il modo più semplice che ho trovato per rilevare perdite di memoria è riuscire a uscire in modo pulito dall'applicazione. Molti compilatori / ambienti forniscono un modo per verificare la memoria che è ancora allocata all'uscita dall'applicazione. Se uno non viene fornito, di solito c'è un modo per aggiungere un po 'di codice prima di uscire che può capirlo.

Quindi, fornirei certamente costruttori, distruttori e logica di spegnimento anche in un sistema embedded che "teoricamente" non dovrebbe mai uscire per facilitare il rilevamento della perdita di memoria da solo. In realtà, il rilevamento delle perdite di memoria è ancora più importante se il codice dell'applicazione non deve mai uscire.


Si noti che ciò non si applica se l'unica volta che si eseguono allocazioni è durante l'avvio, che è un modello che prenderei seriamente in considerazione nei dispositivi con vincoli di memoria.
Codici InCos

@Codes: non c'è motivo di limitarsi. Mentre potresti trovare il grande design che pre-alloca memoria all'avvio, quando le persone dopo di te che non sono al corrente di questo grande schema o non riescono a vederne l'importanza, allocare memoria sul vola e arriva il tuo design. Fallo nel modo giusto e alloca / disalloca e verifica che ciò che hai implementato funzioni davvero. Se si dispone davvero di un dispositivo con memoria limitata, ciò che viene normalmente fatto è sostituire i nuovi blocchi di allocazione operatore / malloc e prereserve.
Dunk,

3

Nel mio sviluppo, che fa ampio uso di tipi di dati opachi per favorire un approccio simile a OO, ho anche affrontato questa domanda. All'inizio, ero decisamente nel campo di eliminare il distruttore dalla prospettiva YAGNI, così come la prospettiva del "codice morto" MISRA. (Avevo un sacco di spazio per le risorse, non era una considerazione.)

Tuttavia ... la mancanza di un distruttore può rendere più difficili i test, come nei test automatizzati di unità / integrazione. Convenzionalmente, ogni test dovrebbe supportare una configurazione / smontaggio in modo che gli oggetti possano essere creati, manipolati e quindi distrutti. Vengono distrutti per assicurare un punto di partenza pulito e non contaminato per il test successivo. Per fare questo, la classe ha bisogno di un distruttore.

Pertanto, nella mia esperienza, il "non" in YAGNI risulta essere un "essere" e ho finito per creare distruttori per ogni classe, sia che pensassi che ne avessi bisogno o meno. Anche se ho saltato i test, esiste almeno un distruttore progettato correttamente per il povero sciatto che mi segue ne avrà uno. (E, a un valore molto inferiore, rende il codice più riutilizzabile in quanto può essere utilizzato in un ambiente in cui sarebbe distrutto.)

Mentre quello indirizza a YAGNI, non indirizza il codice morto. Per questo, trovo che una macro di compilazione condizionale come #define BUILD_FOR_TESTING consenta di eliminare il distruttore dalla build di produzione finale.

In questo modo: disponi di un distruttore per i test / riutilizzo futuro e soddisfi gli obiettivi di progettazione di YAGNI e le regole "nessun codice morto".


Fai attenzione a # ifdef'ing del tuo codice test / prod. È ragionevolmente sicuro se applicato a un'intera funzione, come descrivi, perché se la funzione è effettivamente necessaria, la compilazione fallirà. Tuttavia, l'uso di #ifdef inline all'interno di una funzione è molto più rischioso poiché ora stai testando un codepath diverso da quello che è in esecuzione in prod.
Kevin,

0

Potresti avere un distruttore no-op, qualcosa del genere

  void noop_destructor(void*) {};

quindi imposta il distruttore Uartforse usando

  #define Uart_destructor noop_destructor

(aggiungi il cast adatto se è necessario)

Non dimenticare di documentare. Forse vuoi anche

 #define Uart_destructor abort

In alternativa, caso speciale nel codice comune che chiama il distruttore il caso in cui la funzione del puntatore del distruttore serve NULLad evitare di chiamarlo.

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.