Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

venerdì 20 dicembre 2019

sprintf? No, grazie!
considerazioni sull'uso della sprintf in C

(...una premessa: questo post è un remake di un mio vecchio post. L'ho riadattato e "modernizzato" per pubblicarlo su quell'altro bel blog collettivo dove scrivo. Visto che le modifiche sono molte e (forse) interessanti, lo ripubblico anche qui. Questo potrebbe ripetersi in futuro...)
Peppe: Eva come fa uno a sapere se è innamorato di un'altra persona?
Eva:
Eh, lo chiedi a me?
Peppe:
Eh, sei tu che studi queste cose.
Bianca:
Te lo dico io, allora se ci parli trenta minuti al giorno sei innamorato.
Peppe:
E se ci parlo sessanta?
Carlotta:
Eh allora vuol dire che sei molto innamorato.
Lele: Poi se non ce parli più, vordì che sei sposato
!
Nel bel Perfetti Sconosciuti il personaggio Lele si caratterizza per le frasi a effetto come quella sopra. Con una leggera forzatura possiamo dire che quella frase e questo post si riferiscono allo stesso tema: l'abitudine. Lele pensa che lui e sua moglie si sono già detti tutto e, analogamente, i programmatori C usano la sprintf() solo perché è una pratica comune, una abitudine. Ecco, bisognerebbe fare un piccolo sforzo per risvegliare la passione originale (Lele, parla di più con tua moglie!) e cercare di evitare di trasformare una abitudine in una cattiva abitudine.
...poi, se non funziona, vordì che hai usato la sprintf...
Premessa: la sprintf() se la usi bene e hai il 100% di controllo sul codice scritto la puoi usare senza grossi problemi ma, come dicono gli inglesi, è una funzione "error prone", e cioè induce facilmente a commettere errori, anche gravi. Il problema più grave ed evidente con la sprintf() si chiama buffer overflow, e non credo che sia necessario spenderci molte parole: se il buffer che passiamo come primo argomento non è correttamente dimensionato il disastro è dietro l'angolo.

La soluzione però è semplice, visto che, fortunatamente, ci viene in aiuto la snprintf(), che è della stessa famiglia ma molto più sicura. Vediamo i due prototipi a confronto:
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
Come si può ben vedere, la snprintf() ci obbliga a mettere il size del buffer come secondo argomento, per cui è facilissimo abituarsi a scrivere in un modo error-free come questo:
char buffer[32];
snprintf(buffer, sizeof(buffer), "Hello world!");
E, se al posto di "Hello world!" avessimo scritto una stringa di più di 32 chars, nessun problema: la snprintf() la tronca opportunamente e siamo salvi. Fantastico.

E adesso siamo pronti per analizzare un semplicissimo esempio reale: scriviamo una funzione che ritorna una stringa formattata con data e ora (inclusi i microsecondi), e la implementiamo in due versioni, una buona che chiameremo getDateUsec(), e una cattiva che chiameremo badGetDateUsec(). Noterete che la versione cattiva passa solo l'array destinazione e internamente usa la sprintf(), mentre la versione buona passa l'array destinazione e il suo size, e internamente usa la snprintf(). Notare che è veramente una ottima abitudine quella di passare "destinazione + size" senza usare una allocazione interna alla funzione (con malloc()) che richiederebbe l'uso della corrispondente deallocazione (con free()) da parte del chiamante, come visto recentemente qui. Ed è anche una ottima abitudine evitare l'uso di una destinazione statica dentro la funzione, per i noti problemi con il multithreading (anche questo già visto qui). Ma bando alle ciance, vai col codice!
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>

// prototipi locali
char *getDateUsec(char *dest, size_t size);
char *badGetDateUsec(char *dest);

// funzione main
int main(int argc, char* argv[])
{
    // chiama getDateUsec (o badGetDateUsec) e scrive il risultato
    char dest[12];
    printf("date con usec: %s\n", getDateUsec(dest, sizeof(dest)));
    //printf("date con usec: %s\n", badGetDateUsec(dest));

    return EXIT_SUCCESS;
}

// getDateUsec() - Genera una stringa con data e ora (usa i microsecondi)
char *getDateUsec(char *dest, size_t size)
{
    // get time: gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format stringa destinazione dest (deve essere allocata dal chiamante) e aggiunge i usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    snprintf(dest, size, fmt, tv.tv_usec);

    // return stringa destinazione dest
    return dest;
}

// badGetDateUsec() - Genera una stringa con data e ora (usa i microsecondi) (versione bad)
char *badGetDateUsec(char *dest)
{
    // get time: gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format stringa destinazione dest (deve essere allocata dal chiamante) e aggiunge i usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    sprintf(dest, fmt, tv.tv_usec);

    // return stringa destinazione dest
    return dest;
}
Provate a compilare l'esempio dove, volutamente, ho sottodimensionato il buffer di destinazione: commentando la badGetDateUsec() e usando la getDateUsec(), funziona perfettamente, troncando l'output a 12 chars. Se, invece, si commenta la getDateUsec() e si usa la badGetDateUsec() il programma si schianta durante l'esecuzione. Provare per credere!

E già che siamo in argomento sprintf() un piccolo consiglio un po' OT: se dovete aggiungere sequenzialmente delle stringhe (in un loop, ad esempio) su una stringa base (per comporre un testo, ad esempio) non fate mai cosi:
char buf[256] = "";
for (int i = 0; i < 10; i++)
    sprintf(buf, "%s aggiunto alla stringa %d\n", buf, i);
il metodo qui sopra sembra funzionare, ma, in realtà, funziona quando c'ha voglia lui. Fate invece così:
char buf[256] = "";
for (int i = 0; i < 10; i++) {
    char tmpbuf[256];
    sprintf(tmpbuf, "%s aggiunto alla stringa %d\n", buf, i);
    sprintf(buf, "%s", tmpbuf);
}
E se non ci credete provate a passare il codice cattivo con un lint tipo cppchek, e il risultato sarà questo:
(error) Undefined behavior: Variable 'buf' is used as parameter and destination in s[n]printf().
Quanto sopra è, del resto, ben specificato nel manuale della sprintf():
C99 and POSIX.1-2001 specify that the results are undefined if  a  call
to  sprintf(), snprintf(), vsprintf(), or vsnprintf() would cause copy‐
ing to take place between objects that overlap  (e.g.,  if  the  target
string  array and one of the supplied input arguments refer to the same
buffer).
E, ovviamente, anche in quest’ultimo esempio, fatto per semplicità con la sprintf() ,sarebbe stato raccomandabile usare la snprintf(). E per oggi è tutto, sono già entrato in fase pre-natalizia, quindi cominciamo a rilassarci...
 

Ciao e al prossimo post!

Nessun commento:

Posta un commento