Utilita' controllo restituzione delle funzioni

di il
14 risposte

Utilita' controllo restituzione delle funzioni

Salve, vorrei chiedere una vostra opinione , non solo sul c/c++ ma più in generale perchè applicabile a più linguaggi di programmazione.

Secondo il vostro parere, è bene bilanciare il controllo del programma con la sua lunghezza??
Faccio un esempio idiota per chiarire la mia domanda.

/* ESEMPIO 1*/

main()
{
	scanf(a);
	scanf(b);
 	printf(a,b);
 }
 
 
 /* ESEMPIO 2*/

main()
{
	scanf(a);
	CONTROOLLO(scanf);
	scanf(b);
 	CONTROOLLO(scanf);
 	printf(a,b);
 	CONTROOLLO(printff);
 }

In poche parole, idealmente sarebbe opportuno controllare il valore restituito da ogni singola funzione chiamata:
Ogni controllo può aggiungere al programma 3-4 righe di codice, nell'esempio precedente ci troveremo nella situazione che l 'esempio 2 ha più codice per il controllo di scanf e printf rispetto al codice del programma in se, allora mi chiedo, esiste un buon compromesso tra lunghezza del codice e controllo funzioni, oppure per sicurezza e sempre opportuno controllare sempre tutto??
Non parlo solo per il valore restituito delle funzioni sia chiaro, ma anche, ad esempio, che le variabili int non eccedano INT_MAX e INT_MIN, che ogni puntatore adoperato non punti a NULL, ogni apertura di FILE sia sempre ok. nelle funzioni che mi creo io, controllare sempre la validità dei parametri d'ingresso dentro la funzione chiamata, ecc ecc..

voi come vi bilanciate?

14 Risposte

  • Re: Utilita' controllo restituzione delle funzioni

    Diciamo che se il programma va in "produzione" è necessario utilizzare una programmazione "difensiva" e controllare tutto il controllabile.
  • Re: Utilita' controllo restituzione delle funzioni

    La tua domanda non ha una risposta univoca perché spesso è una questione pragmatica. In genere l'inserimento o meno di codice che controlla tutto ciò che viene restituito da una funzione o passato in input è sempre una buona cosa. L'importante è non scrivere codice ridondante altrimenti si rischia di diventare prolissi. Occorre sempre mantenere il giusto trade-off.
  • Re: Utilita' controllo restituzione delle funzioni

    La questione e' decisamente spinosa:

    1) non esiste una soluzione univoca, ovviamente
    2) e' buona pratica controllare tutto il controllabile
    3) controllare troppo senza avere una chiara idea di che cosa fare e' deleterio tanto quanto non controllare nulla

    Una possibile regola e' la seguente:

    SOLO le librerie a piu' basso livello dovrebbero ritornare dei codici di controllo.

    Queste andrebbero WRAPPATE da una libreria che controlla i codici di controllo e, in caso di malfunzionamento, generare un'opportuna eccezzione.

    L'implementazione in generale dovrebbe essere realizzato come se tutto funzionasse correttamente. In caso di problemi, generare un'eccezione, NON ritornare un codice di errore (che andrebbe a sua volta controllato, ...).

    L'eccezione dovrebbe essere lasciata liberala di propagarsi fino a dove ha senso intercettarla.

    Solo li' farne il log e agire opportunamente.

    Ovviamente, questo richiede un'opportuo stile di programmazione, tra cui quello di assicurarsi che all'uscita di ogni metodo/funzione, le risorse locali a quel metodo/funzione vengano opportuamente rilasciate.

    Poi bisogna distinguere tra errori a runtime, ed errori di implementazione.
    Gli errori a runtime devono necessariamente essere gestiti mediante eccezioni.
    Gli errori di implementazione possono essere gestiti mediante l'introduzione di un'opportuna quantita' di ASSERZIONI sui valori in ingresso al metodo/funzione e sul risultato.

    Le asserzioni servono SOLO in fase di sviluppo (in modalita' DEBUG), ed assicurano che una certa funzione/metodo venga chiamato con dei parametri che hanno senso.

    Ad esempio, una funzione che apre un file, non e' ragionevole che venga chiamata passandoci la stringa vuota o il puntatore a null: se cio' avviene c'e' un problema a monte. Qui basta sicuramente un'asserzione.

    Invece e' ragionevole immaginare che possa essere chiamata con il path di un file non esistente. Questo e' un errore a runtime e deve essere essere opportunamente gestito.

    Ma di queste elucubrazioni filosofiche ce ne sono n-mila. Bisogna un po' farci la mano e con buon senso scegleire una soluzione o l'altra.
  • Re: Utilita' controllo restituzione delle funzioni

    Nei sistemi safety-critical esistono coding standards e regole di engineering resi obbligatori dalle varie normative internazionali. La più blanda e probabilmente più nota anche i non addetti tra tali raccomandazioni normative è la MISRA/C 2012, la quale al proposito si esprime come segue:
    Dir 4.7 (Required) "If a function returns error information, then that error information shall be tested."
    Exception: If it can be shown, for example by checking arguments, that a function cannot return an error indication, then there is no need to perform a check.

    Dir 4.11 (Required) "The validity of values passed to library functions shall be checked."

    Da tali direttive di codifica scaturisce poi la regola:

    Rule 17.7 (Required, Decidable, STU) "The value returned by a function having a non-void type shall always be used."
    Tali regole sono inoltre ampiamente rafforzate dalla pratica del Design by Contract ideato da Bertrand Meyer (nativamente supportato da linguaggi per applicazioni ad altissima affidabilità, come Eiffel e Ada, come pure da appositi framework o preprocessori C), che richiede di stabilire formalmente un contratto tra funzione chiamata e chiamante, con obblighi da rispettare per ambo le parti nel passaggio di parametri e nella eventuale restituzione di valori.

    Da notare, a margine, il fatto che in molti standard di codifica C proprietari adottati dalle più grandi multinazionali che gestiscono il mercato dei sistemi altamente critici esiste una direttiva del tipo "Functions type must exclusively be either void or boolean, where the boolean type will feed back the caller with the information about the absence or presence of any error.".

    Colgo l'occasione da ricordare che i coding standards di cui si parla (soprattutto quelli a livello entry come MISRA/C) non sono assolutamente appannaggio esclusivo delle torri d'avorio dove opera la ristretta elite dei professionisti dei sistemi safety-critical, che hanno completato un ciclo di studi non inferiore a 12-15 anni tra accademia e corsi interni prima di metter mano al più banale dei progetti nel nostro ambito. Con opportune variazioni e compromessi, si tratta di regole la cui adozione (e verifica automatica tramite appositi parser e checker) apporta un netto beneficio alla qualità di codifica di un qualsiasi programmatore quadratico medio.

    Non a caso le norme MISRA/C e gli standard di codifica NASA (che in gran parte si basano su dette norme, ricorsivamente) sono strettamente alla base anche dei pochi coding standards mirati esclusivamente al mass market e ai sistemi mainstream, come quello elaborato dal (ad ulteriore conferma di quanto si va ripetendo da almeno vent'anni, da quando sono esplose talune mode, ossia che un software ingegneristicamente robusto è anche automaticamente un software sicuro, nelle accezioni comunemente diffuse in letteratura).

    I tempi in cui il footprint di un'applicazione era soggetto a limitazioni talmente drastiche da sconsigliare l'adozione generalizzata delle regole sopra esposte sono tramontati da decenni, perfino nel mondo dei sistemi embedded entry level. Oramai gli unici a sostenere la tendenza suicida di trascurare sistematicamente i controlli "per aumentare la leggibilità" in nome di principi empirici assolutamente esilaranti (come la "regola" che una funzione debba rientrare in una singola schermata), con l'ovvio effetto di lasciare andare in crash l'applicazione al minimo errore di allocazione o di I/O per aver "risparmiato" un paio di if() e un early exit, sono i cialtroni dell'agile. Ciò si pone, come si vede, apertamente in rotta di collisione con i pirincipi più saldi del software engineering, dei quali ho appena dato un breve ma significativo assaggio.
  • Re: Utilita' controllo restituzione delle funzioni

    migliorabile ha scritto:


    Ovviamente, questo richiede un'opportuo stile di programmazione, tra cui quello di assicurarsi che all'uscita di ogni metodo/funzione, le risorse locali a quel metodo/funzione vengano opportuamente rilasciate.
    questo è il nocciolo della mia domanda. Sto iniziando ora col C , e non vorrei prendermi da ora abitudini non buone, anzi vorrei iniziare a prendere la mano con uno stile di programmazzione oculato e attento a queste cose. Ovviamente visto che per ora non devo programmare i robot della NASA posso stare più tranquillo e rilassato mentre programmo, ma vorrei fin da ora iniziare a prendere delle buone abitudini.
  • Re: Utilita' controllo restituzione delle funzioni

    Aggiungo, a beneficio dell'OP, che l'ultima mezza dozzina di testi in questa bibliografia è specificamente dedicata alla costruzione di uno stile di codifica C per uso general purpose esente da pessime e pericolose abitudini, orientato a robustezza e mantenibilità secondo i canoni asseverati del software engineering classico. A proposito di engineering, vale a fortiori il consiglio bibliografico elargito appena pochi giorni fa su questo stesso forum, inerente il testo di Meyer "OOSC", oltre ai classici e .
  • Re: Utilita' controllo restituzione delle funzioni

    M.A.W. 1968 ha scritto:


    Aggiungo, a beneficio dell'OP, che l'ultima mezza dozzina di testi in questa bibliografia è specificamente dedicata alla costruzione di uno stile di codifica C per uso general purpose esente da pessime e pericolose abitudini, orientato a robustezza e mantenibilità secondo i canoni asseverati del software engineering classico.

    Grazie della lista... andrò a guardare qualche libro di essa.


    Facciamo un esempio pratico perchè mi sto un pò confondendo:
    
    
    char* Time( void )
    {
        struct tm *timeinfo = NULL;
        time_t rawtime = 0;
        time_t ver = 0;   
        char *buffer = NULL;
        
        buffer = (char *)calloc( 25 , sizeof( char ) );/* CONTROLLO ALLOCAZIONE*/
        time( &rawtime );
        timeinfo = localtime( &rawtime );
        strftime( buffer , 25 , "%d-%m-%Y alle %H:%M:%S" , timeinfo );   /* CONTROLLO SCRITTURA */
    
        return buffer;
    
    }
    
    Voi controllereste pure time() e localtime()?
    ESEMPIO:
    
    char* TimeError( void )
    {
        /* ======================================================================= */
        struct tm *timeinfo = NULL;               
        time_t rawtime = 0;                         
        time_t ver = 0;                
        char *buffer = NULL; 
        char *nullo = NULL;                        
        /* ====================================================================== */
    
        /* ====================================================================== */
        buffer = (char *)calloc( 25 , sizeof( char ) );         
        if( buffer == NULL ) {              
            fprintf( stderr , "%s" , "memoria per bufferT non allocata\n" );   
            return nullo;                             
        }            
        /* ====================================================================== */
    
        
        
        /* ====================================================================== */
        ver = time( &rawtime );     
        if( ver == -1 ) {      
            fprintf( stderr , "%s" , "time() non riuscita\n" );   
            return nullo;
        }     
        /* ====================================================================== */
        
        
        
        /* ====================================================================== */
        timeinfo = localtime( &rawtime );  
        if( timeinfo == NULL ) { 
            fprintf( stderr , "%s" , "struct timeinfo non riempita \n" );    
            return nullo;  
        }                                          
        /* ====================================================================== */
    
        
        
        /* ====================================================================== */
        ver = strftime( buffer , 25 , "%d-%m-%Y alle %H:%M:%S" , timeinfo ); 
        if( ver != 0 ) {                                                    
            fprintf( stderr , "%s" , "strftime non riuscita\n" );            
            return nullo;                                
        }                                                                    
        /* ====================================================================== */
        
        return buffer;
    
    }
    
    
    
    
  • Re: Utilita' controllo restituzione delle funzioni

    Mikelius ha scritto:


    Voi controllereste pure time() e localtime()?
    Se il tuo codice dovesse essere sottoposto ad un parser MISRA/C, il valore di ritorno di time() e localtime() dovrebbe essere controllato come qualsiasi altro, e ciò risolve ogni dubbio in tale caso.

    In ambienti applicativi più rilassati, ed a scopo puramente propedeutico, dovresti e potresti fare una mini-analisi FMEA ispirata alla norma IEC 60812-2006 (ne parlano un po' tutti i testi di software engineering, a livelli più o meno approfonditi), dopo avere studiato l'implementazione della libreria di runtime del tuo compilatore (praticamente sempre disponibile, anche nei prodotti commerciali, e comunque al 99% basata sulla Dinkum) e l'architettura hardware di riferimento: quanto è importante il corretto recupero di tale informazione? Può compromettere la funzionalità del programma? Posso prevedere un valore di default, un controllo aggiuntivo, una verifica interattiva dell'utente? Il codice di libreria è affidabile? E poi, più a fondo: cosa può effettivamente causare un valore di ritorno non valido in dette funzioni? Invecchiamento o guasto della batteria di backup sulla motherboard? Un reale guasto hardware? Una sovratensione? Una scarica elettrostatica? Che genere di provvedimenti posso adottare?

    Dalle risposte a queste (ed a molte altre) domande dipende il comportamento da adottare nel codice, e, in casi non giocattolo, anche a livello di hardware, ambiente, interazione con l'utente, eccetera.
  • Re: Utilita' controllo restituzione delle funzioni

    Capito più o meno...
    Grazie delle info.
  • Re: Utilita' controllo restituzione delle funzioni

    Fermo che è ovvio (almeno per me) che ogni e qualsiasi cosa possa andar male, e che quindi vada debitamente controllata (senza se e senza ma, non serve chissà quale superstudio, basta aver venduto anche un programma al fornaio sotto casa), aggiungo un dettaglio spesso non tanto considerato.
    Ovvero le funzioni di libreria, spessissimo foriere di devastanti problemi, sia per errori veri e propri, sia per assunzioni più o meno occulte, sia per possessioni demoniache.
    Soprattutto nel caso (frequente) di portabilità di codice tra compilatori diversi, sistemi operativi diversi e addirittura architetture diverse (che poi oggi sono in tutto 3 o 4).
    Quindi "fidarsi" del valore tornato da una funzione, qualsiasi essa sia (cioè del proprio programma, o di libreria) è male a prescindere.
    Per sincerarsene basta aver compilato almeno una volta il programma del fornaio per Marvell ad esempio, o addirittura per i vari pseudocompilatori C su Windows.

    ---
    Riguardo alla concisione, per tutti i casi "normali" cioè del mondo moderno, dove un telefono ha 3GB di RAM e un processore con 8 core, e stabilito che la semantica varia a seconda di quali aspetti si vuole privilegiare, la regola del "buon fornaio di famiglia" è di NON aggiungere orpelli inutili.
    Come già i mitici teteski ai tempi della I guerra mondiale: se è semplice, FORSE funziona. Se non c'è, non si può rompere.

    Quindi, per quanto mi riguarda,
    SI al test di tutti i valori di ingresso alle funzioni
    SI a verificare assunzioni "occulte" (ad esempio: passo un percorso dove dovrò scrivere qualcosa. Ma il path è scrivibile? Non mi basta testare che esista, voglio proprio una funzione "testadirectoryscrivibile" dove scriverò un file di testo di prova, lo rileggerò, controllerò che sia uguale. Perchè magari su BSD eseguendo sotto un utente wheel scrivo dove voglio, ma con altri non riesco, o peggio ancora incappo in una condivisione SMB di un server Windows e così via)
    SI a non fidarsi dei valori delle funzioni, sia "proprie" che di libreria
    SI a preparare opportuni meccanismi di "pulizia" delle risorse allocate (distruttori, except e così via a seconda dell'ambiente)
    SI a tornare sempre risultati dalle proprie funzioni (per quanto mi riguarda non mi limito a vero-falso, bensì ho una mia "scala" di OK, grave, fallimento modesto, morte, ma è questione di abitudine)
    SI (per quanto mi riguarda) a parametri di default che dichiarino sia la silenziosità, sia il check dei risultati. Silenziosità intesa come torna errore e basta, oppure logga i risultati.
    check inteso come cose strane, tipo una funzione copia file da A a B, che normalmente copia e basta, ma se voglio dopo aver copiato B controlla a blocchi od a hash i due file per essere sicuro che la copia sia andata bene.
    Perchè se copio il file su LAN (cioè chiamo le funzioni di sistema che lo fanno, difficilmente mi farò la mia socket personale) tutto in generale va bene. Ma se sto usando una VPN allora i dati possono andare persi facilmente, o venire corrotti etc.
    Quindi se mi "fido", perchè sono in un ambiente ragionevolmente "affidabile", il mio programma funzionerà rapidamente.
    Se invece sono "timido", e non mi fido, allora mi basta cambiare un flag per forzare il controllo di tutto quello che normalmente non si controlla (cioè le funzioni API o di libreria).
    Fino ad arrivare in certi casi a fare DUE diverse computazioni dello stesso valore (es. nel caso di rischio di instabilità numerica per piccoli valori, su compilatori diversi e pure hardware diverso) con relativa verifica.

    NO a mettere dentro il codice del fornaio quello del verduraio, se non strettamente necessario
  • Re: Utilita' controllo restituzione delle funzioni

    Postillina: attenzione ad esempio agli interi negativi, che non sono minimamente scontati in funzionamento e portabilità.
    mentre gli interi unsigned bene o male (nei limiti dell'overflow) sono abbastanza trustabili, ciò non accade affatto per i negativi, con i relativi riflessi sui valori da tornare dalle funzioni per segnalare gli errori.

    in certi casi è prassi comune sottoporre a test molto approfonditi (per quanto possibile) i compilatori per testare a priori il comportamento per i casi "dubbi", con makefile anche molto complessi, creati automaticamente (con tool) e/o "a mano".

    Purtroppo "una volta" ci si poteva fidare dei manuali dei compilatori, oggi o non esistono proprio (perchè sono progetti talmente grandi e talmente aggiornati di frequente, e pure stravolti, da perdere di significatività) oppure sono spesso a loro volta incompleti quando non del tutto fuorvianti.

    Riassumendo, per quanto mi riguarda, NON mi fido dei compilatori, soprattutto nei casi "strani" (e come accennato per me "strano" è già un intero signed).

    Non parliamo poi del mondo RDBMS e di come si implementano database (vagamente) ben funzionanti
  • Re: Utilita' controllo restituzione delle funzioni

    M.A.W. 1968 ha scritto:


    Aggiungo, a beneficio dell'OP, che l'ultima mezza dozzina di testi in questa bibliografia è specificamente dedicata alla costruzione di uno stile di codifica C per uso general purpose esente da pessime e pericolose abitudini...
    Bhè diciamo che alcuni dei testi citati sono un pochino... pleonastici per usare un eufemismo.
    Addirittura c'è il classico controesempio di una 20ina di anni fa (Maguire).

    In pratica: per quanto possa sembrare ridicolo leggere un testo di un programmatore Microsoft sullo scrivere programmi con pochi bug (ogni volta mi scappa da ridere), e fatta la tara per un testo di 20 anni fa (che quindi non ricomprende i trend moderni), per il niubbo se dovessi indicare UN libro, informale e facile da capire tra quelli indicati, sceglierei proprio QUELLO.

    Vabbè è questione, come più o meno tutto, di "gusti"
  • Re: Utilita' controllo restituzione delle funzioni

    Lascio poi stare i metodi automatici (dal mitico lint agli odierni sistemi per ampliare lo spazio di test sui dati di input) per... carta.
    Sì, carta.

    Non è insolito, almeno per me, stampare su carta i sorgenti e rivedermeli con calma nel week end, proprio con un debug statico e refactoring stilistico.

    Ci vuole tempo e cura, e normalmente lo adotto DOPO aver esaurito i meccanismi "automatici e scemi" di verifica.
    Ma spesso rendere un codice "bello" può voler dire migliorarne anche la qualità
  • Re: Utilita' controllo restituzione delle funzioni

    Più o meno ho capito il succo del discorso...

    grazie a tutti per il contributo...
Devi accedere o registrarti per scrivere nel forum
14 risposte