Questioni di rispetto nel codice…

Un po’ di tempo fa, mentre parlavo e spiegavo ad un conoscente l’uso di una data funzione di libreria in C è partita una mini-discussione sul perchè, prima di chiamarla e dopo averla chiamata, avessi sistematicamente compiuto alcune operazioni apparentemente superflue dato il contesto come pulire/ripredisporre dei buffer e reinizializzare alcune strutture in memoria.

La cosa “strana” ai sui occhi è stato che quella funzione fosse in realtà chiamata solo ed esclusivamente in un punto del nostro codice cosicchè il “chiamante” e “consumatore” del risultato prodotto di fatto fosse uno solo: noi, nelle sembianze della la nostra funzione.

Il suo ragionamento è stato:

“Ma se tanto chi usa quella funzione g() è sempre la stessa funzione, la nostra f(), perchè dovremmo perdere tempo per assicurarci di reinizializzare tutto? Tanto non c’è nessun altro che la usa in questo codice perchè è nostro!”

La mia risposta al momento è stata un filo stringata:

“Precauzione!”

Mi rendo conto che si tratta di una risposta troppo concisa (soprattutto per me!), per cui forse è bene dire qualche parolina in più…

Esistono almeno due buoni motivi per cui nelle situazioni dubbie, in cui ad esempio si tocca codice altrui, è sempre bene fare le cose per bene e ripulire il tutto: paranoia dovuta all’esperienza e, soprattutto, una sorta diconvezioni e accordi impliciti fra funzioni chiamanti e chiamate.

1. Paranoia dovuta all’esperienza

In primo luogo, la funzione di cui stavamo discutendo appariva ed era stata pensata male. Ad esempio non si capiva bene di chi fosse la responsabilità quanto alla gestione della memoria.

In secondo luogo tale funzione accedeva e sfruttava anche a variabili globali in lettura e soprattutto scrittura. Già questo è un male di suo, un code smell grande come una casa e non importa se poi il consumatore, in quel momento e in quel codice, è solo uno! Parafrasando Tom Hanks in Forrest Gump:

“Le variabili globali sono come una scatola di cioccolatini:non sai mai quello che ti capita (o chi ci sta facendo cosa in un dato momento)”

Forrest Gump

Facendo la somma, quindi un po’ di precauzione non guastava proprio: se non puoi fidarti del codice altrui, essere un po’ più guardinghi del solito non è un atteggiamento sbagliato.

Inoltre, non so se sia semplicemente un’applicazione pratica della Legge di Murphy ma quando si sceglie deliberatamente di non proteggere o non curarsi bene di qualcosa solo perchè tanto il “consumatore è solo uno” o perchè “tanto la uso solo io e la proteggerò solo se serve“, finisce sempre sistematicamente male.

Nel caso specifico, ad esempio, durante lo sviluppo del codice i “consumatori” di quella funzione potevano diventare molti di più e con loro, magia, nuovi bug assurdi. E assurdi è un eufemismo ricorsivamente ed assurdamente infimo.

Esempio: due funzioni che non c’entrano nulla fra loro ma che chiamandone a tempi alternati e in modo pessimo una terza, si pestano i piedi a vicenda in memoria perchè la prima non pulisce la memoria quando la chiamata è conclusa e la seconda si fida troppo ciecamente di quello che trova.

Debuggare il codice quando capitano queste cose diventa un atroce supplizio, da evitare come la peste.

Fra l’altro questo è un esempio di bug in qualche modo correlato ad un antipattern noto come Action at distance e così definito:

“Action at a distance is an anti-pattern (a recognized common error) in which behavior in one part of a program varies wildly based on difficult or impossible to identify operations in another part of the program.”

Quindi, per esperienza, se qualcosa va protetto (e proteggere/proteggersi talvolta implica solamente forzare esplicitamente la pulizia della memoria allocata), fatelo: abbasso la pigrizia e viva la paranoia!

Se qualcuno dovesse lamentarsi delle continue pulizie di memoria, paventando chissà quali fantomatiche perdite di efficienza, ignoratelo: evidentemente sottostima la perizia di compilatori e computer a fare il loro mestiere e, soprattutto, l’attitudine tipicamente umana a generare bug per indolenza per poi correre ai ripari quando è troppo tardi.

Nei miei 10+ anni di esperienza lavorativa mi è capitato un solo caso in cui ho dovuto veramente pensare a come utilizzare in modo iperpreciso/paranoide la memoria, evitando di ripulirla quando possibile per non penalizzare svariati thread in azione in un dato momento: era un’applicazione di ricerca che applicava un algoritmo di branch and bound mentre esplorava in parallelo alberi di milioni di nodi, facendo conti su conti e impiegando ore se non giorni a terminare su un cluster di macchine. In quel caso sì ho dovuto sfruttare la memoria al meglio e ho adottato un meccanismo di memory pooling.

Ecco, solo in casi come quelli, dove le prestazioni e/o la sicurezza estrema sono tutto, mi sentirei di consigliare un tale livello di ottimizzazione. In tutti gli altri casi no.

Dico di più: chi si ostina a voler levare per principio una misera malloc/realloc al secondo in più rispetto alla sicurezza del farlo e basta, evidentemente ama farsi male. O comunque non sa quello che rischia.

2. Accordi taciti fra funzioni chiamanti e chiamate

Questo invece è un punto, sconosciuto ai più, su quali vale la pena soffermarci un attimo.

Per una misera questione di principio se prestate qualcosa a qualcuno, vi aspettate che vi ritorni indietro nelle medesime condizioni di quando l’avete concessa e viceversa.

Ecco, nel mondo della programmazione vige la stessa logica e, nel caso delle funzioni, prende la forma di un accordo tacito fra le funzioni. In pratica a fine prestito, tutto deve tornare alle condizioni iniziali a prescindere da chi l’ha usata e chi la userà, se la userà.

Si chiama rispetto e sì, ne esiste anche una controparte a livello di programmazione!

Questo è una variante del più generico accordo fra funzione chiamante e chiamata.

Mi permetto di citarne una bella definzione, dal bellissimo libro Writing Solid Code di Steve Maguire (già citato un paio di volte in passato: qui e qui):

I privilegi dei dati

Potete anche non averlo mai letto nei manuali di programmazione ma ogni dato utilizzato nel vostro codice implica privilegi in lettura e scrittura ad esso associati. Tali privilegi non sono espliciti, ma sono insiti nel progetto delle vostre procedure e delle interfacce delle vostre funzioni.

Per esempio sussite un tacito accordo tra un programmatore che chiama una funzione e quello che l’ha scritta.

Tale accordo recita:

“Se io, il Chiamante, passo a te, il Chiamato, un puntatore a un input, tu accetti di trattare quell’input come fosse una costante e prometti di non modificarne il valore. Inoltre, se io passo a te un puntatore ad un output, tu accetti di trattarlo come un oggetto a sola scrittura e prometti di non leggerlo mai. Infine, se il puntatore punta a un input o a un output, tu accetti di referenziare la memoria strettamente indispensabile per contenere quell’input o quell’output. Da parte mia, io, il Chiamante, accetto di trattare gli output in sola lettura come se fossero costanti e prometto di non modificarli mai. Accetto, inoltre, di referenziare la memoria strettamente indispensabile per memorizzare questi output.”

In altre parole: ‘Non ficcare il naso nei miei affari e io non lo farò nei tuoi’. Ricordatevi questo: ogni volta che violate un privilegio implicito di lettura o scrittura rischiate di danneggiare il codice scritto da altri programmatori che confidavano nel rispetto delle regole. Un programmatore che chiama una funzione come memchr non dovrebbe preoccuparsi del fatto che memchr possa comportarsi in modo irregolare.

Questo è un discorso lapalissiano ma spesso ignorato per pigrizia.

Il problema è che nessuno è un indovino e nessuno può prevedere eventuali usi futuri. Per cui vale la pena non solo progettare bene funzioni e relative interfacce perchè non siano prone all’errore, ma anche accertarsi che tutti i taciti accordi fra funzioni siano sempre rispettati.

Che ne pensate?

Contrassegnato da tag , , ,

4 thoughts on “Questioni di rispetto nel codice…

  1. Stefano scrive:

    Penso che un controllo in più non guasta mai. Fidarsi è bene, non fidarsi è meglio :)

  2. NeXuS scrive:

    Concordo in pieno con te e con Stefano.

    Senza contare che, talvolta, le convenzioni cambiano non a seconda della funzione/libreria, bensi’ a seconda della suite di compilazione/runtime… come ti potrebbe raccontare chi si dimentica di inizializzare le variabili compilando del codice C con il compilatore GNU. ;)

  3. Stefano scrive:

    Uh (è lunedì, scusate).

    Non volevo scrivere ‘controllo’ ma ‘precauzione’. Differenza sottile ma ci tenevo a distinguere..

  4. jp scrive:

    @Stefano: tranquillo. I controlli sono un’implementazione di “precauzione“. :D

    NeXuS: sì, le convenzioni specifiche possono cambiare, ma il buon senso alla fine è uno solo… :D

    Ciao & grazie di essere passati! ^^

Lascia un Commento

Fill in your details below or click an icon to log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Log Out / Modifica )

Foto Twitter

You are commenting using your Twitter account. Log Out / Modifica )

Foto di Facebook

You are commenting using your Facebook account. Log Out / Modifica )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 259 other followers