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ì 22 ottobre 2021

Cry Keyword: complex
come usare le nuove keyword del C - pt.1

Michael "Mike" Milo: [riferendosi ai federali che lo hanno appena lasciato andare in cambio di una mazzetta] Se avessero un cervello, sarebbero pericolosi...

Celebriamo oggi l'ultimo film che ci ha regalato un mito vivente del Cinema con la C maiuscola, Clint Eastwood. Cry Macho è un road-movie "crepuscolare" in cui il nostro Clint interpreta un vecchio cow-boy impegnato in un viaggio, malinconico e gioioso allo stesso tempo, che parte dal sud degli US e si prolunga nel Messico. E questo ci può ispirare a iniziare anche noi un viaggio alla scoperta delle ultime novità del C, quelle che pochi usano ma sono, come sempre, molto utili: nel C si aggiungono poche cose ma succose (ad esempio complex, di cui parleremo tra poco).

...ah, se esistessero i numeri immaginari. Peccato che sono solo frutto della mia immaginazione...

E comincerò con una auto-citazione dal mio ultimo articolo (dopodiché mi auto-denuncerò per plagio):

...un po’ come è successo, per esempio, con le variabili booleane _Bool definite nell’header stdbool.h: questa norma del nome che inizia con undescore+maiuscola è seguita per tutte le nuove keyword del C, che possono, poi, essere usate con l’header corrispondente per “raddrizzare” il nome...

Ecco, credo che, per mantenere la linea con gli ultimi articoli sia venuto il momento di parlare delle nuove (vabbè, risalgono al C99 e al C11, diciamo allora "relativamente nuove") keyword del C, quelle che sono state introdotte un po' per volta, col contagocce (e giustamente! Perché il C è un linguaggio solido, stabile e senza fronzoli). E visto che sono arrivate quando il linguaggio era già da qualche annetto che veniva usato in lungo e in largo per il mondo (è da sempre uno dei linguaggi più usati) bisognava avere un occhio di riguardo per il codice già scritto con implementazioni private delle keyword non ancora standardizzate.

E bool  è il classico esempio: supponendo che qualcuno (in molti, probabilmente) abbia scritto codice con un tipo booleano user-defined e lo abbia chiamato "bool" (molto originale, no?), l'introduzione nel linguaggio di un nuovo tipo con lo stesso nome (e quindi riconosciuto da compilatore) avrebbe creato qualche conflitto con il codice pre-esistente, quindi il committee ISO ha pensato bene che tutte le nuove keyword introdotte si sarebbero chiamate con nomi che iniziano con undescore+maiuscola, cominciando con _Bool che credo sia stato il primo esempio (è stato introdotto nel C99).

Come funziona questo? È semplicissimo: se voglio usare il nuovo tipo booleano scrivo:

#include <stdbool.h>
...
bool mybool_a = true;
bool mybool_b = false;
...

Evidentemente dentro l'header standard stdbool.h  vengono definite, tra le altre cose, le macro true (che vale 1), false (che vale 0) e bool  (che corrisponde al nuovo tipo standard _Bool). E invece, se ho una mia vecchia implementazione di bool e voglio continuare a usarla basta non includere stdbool.h  e amici come prima.

E quali sono le "relativamente nuove" keyword del C? Eccole!

C99:
_Bool
_Complex
_Imaginary

C11:
_Alignas
_Alignof
_Atomic
_Generic
_Noreturn
_Static_assert
_Thread_local

di alcune ho già parlato recentemente (_Atomic nello scorso articolo e _Bool proprio ora, non so se vene siete accorti) o anticamente (_Thread_local, usando però __thread, che è una estensione del GCC sostanzialmente identica). In questo articolo direi di soffermarci sulle due che ci mancano del C99, ovvero _Complex e _Imaginary.

_Complex e _Imaginary, come si può ben intuire dai nomi, sono due nuovi type specifier  (come char, int, ecc.) che permettono l'uso dei numeri complessi nel C. Nessuno storca il naso ("ma chi ha bisogno dei numeri complessi?"): non sono di uso molto comune, ma non guasta poterli usare, e, se non vi servono, pensate che chi li usa (e potrebbero essere più di quelli che pensate) vi manderà maledizioni se dite che se ne può fare a meno (quell'irascibile di mio cuggino ad esempio li usa spesso). Ovviamente non è questa la sede per descrivere cosa sono e a cosa servono i numeri complessi, visto che ci sono milioni di fonti che li descrivono molto meglio di quanto potrei fare io.

Queste due nuove keyword viaggiano in coppia, e si possono utilizzare, entrambe, attraverso l'header complex.h. Un solo header per due nuove keyword? Ebbene si, sono troppo collegate per usarne due diversi, infatti _Imaginary serve a definire la parte immaginaria di un numero complesso, quindi è giustificato che tutto sia definito in complex.h. E come si usano? Prima di tutto bisogna evidenziare che, per la loro natura (matematica) stessa, si accoppiano a tipi float, e che essendo type specifier c'è una certa libertà di scrittura, per cui possiamo scrivere questo:

...
...
// questo formato è Ok
_Complex float cf1;
_Complex double cd1;
_Complex long double cld1;

// ma anche questo è Ok
float _Complex cf2;
double _Complex cd2;
long double _Complex cld2;
...

e, usando le macro definite nell'apposito header complex.h, possiamo "abbellire" il codice in:

#include <complex.h>
...
// questo formato è Ok
complex float cf1;
complex double cd1;
complex long double cld1;

// ma anche questo è Ok
float complex cf2;
double complex cd2;
long double complex cld2;
...

E adesso è venuto il momento di un bel caso pratico di uso di _Complex, che è, spero, più chiaro di mille spiegazioni. Facciamo cantare il codice!

#include <stdio.h>
#include <complex.h>

int main(void)
{
// dichiaro un numero complesso
complex float cf = 5 + 3 * I;

// eseguo due operazioni aritmetiche
complex float mult_cf = cf * 2;
complex float div_cf = mult_cf / 2;

// stampo i risultati
printf("(5.0 + 3.0i) * 2 = %.1f %+.1fi\n",
creal(mult_cf), cimag(mult_cf));
printf("(%.1f %+.1fi) / 2 = %.1f %+.1fi\n",
creal(mult_cf), cimag(mult_cf), creal(div_cf), cimag(div_cf));
}

credo che l'esempio sia abbastanza chiaro: dichiariamo un numero complesso, lo moltiplichiamo per 2 e dividiamo il risultato per 2, per ottenere lo stesso valore di partenza (tanto per dimostrare la correttezza delle operazioni). Se compilate ed eseguite otterrete:

(5.0 + 3.0i) * 2 = 10.0 +6.0i
(10.0 +6.0i) / 2 = 5.0 +3.0i

Funziona! Notare, nel codice, la presenza della macro I (definita, ovviamente, in complex.h) che serve a definire la parte immaginaria del numero complesso (si usa come moltiplicatore per trasformare un "numero reale" in "numero immaginario").

Notare anche che per stampare i valori ho usato delle funzioni di libreria i cui prototipi sono dichiarati in complex.h: creal() e cimag() che, come si intuisce dai nomi, servono a estrarre, rispettivamente, le parti reale e immaginarie di un numero complesso, che poi possono essere agevolmente stampate con una printf(), come nell'esempio. Nel nostro header sono dichiarate molte altre di queste funzioni per numeri complessi, con nomi facilmente riconducibili alle equivalenti funzioni per "numeri normali". Abbiamo, ad esempio, cabs() per il valore assoluto, clog() per il logaritmo naturale, ccos() per il coseno, e chi più ne ha più ne metta.

Ok, per oggi può bastare. Nei prossimi articoli parleremo delle altre keyword della lista mostrata all'inizio dell'articolo. E mi raccomando: non trattenete il respiro nell'attesa!

Ciao, e al prossimo post!

venerdì 17 settembre 2021

Atomic: Endgame
come, quando e perché usare il type qualifier _Atomic in C

Scott Lang/Ant-Man: È una pazzia!
Natasha Romanoff/Vedova Nera: Scott, io ricevo e-mail da un procione, perciò nulla sembra una pazzia ormai.

E siamo arrivati al terzo capitolo della mini-saga degli Endgame sui type qualifier del C. Questa volta tocca a _Atomic, che, rispetto ai qualificatori trattati nei due precedenti articoli (su volatile e restrict), ha un ruolo più importante nel linguaggio. E, visto che nel nuovo mondo post Infinity War la nostra Natasha non si sorprende di parlare con un procione, noi non siamo affatto sorpresi di avere (finalmente!) la possibilità di usare variabili atomiche nel C.

...con le variabili atomiche la mia vita è cambiata da così a così...

Anche stavolta (per la solita pigrizia) seguirò la stessa impostazione dei due articoli precedenti ("Squadra che vince non si cambia") quindi questo è il momento delle notizie, e cominceremo con la buona:

  • La buona notizia (che ho già spoilerato sopra) è che _Atomic è un qualificatore veramente molto utile, che ci può rendere la vita più facile nella scrittura del codice (solo nella programmazione multithread, eh!). _Atomic è arrivato con lo Standard C11 insieme ad altre poche, mirate e utili novità (che vi invito a esaminare e ad usare). _Atomic ha un nome, ahimè, un po' strano che è stato scelto per curare la retrocompatibilità, visto che è possibile che ci siano delle implementazioni "private" e pre-C11 delle variabili atomiche (l'attesa è stata lunga!); un po' come è successo, per esempio, con le variabili booleane _Bool definite nell'header stdbool.h: questa norma del nome che inizia con undescore+maiuscola è seguita per tutte le nuove keyword del C, che possono, poi, essere usate con l'header corrispondente per "raddrizzare" il nome (la stabilità e la retrocompatibilità sopra tutto!).

(...e visto che siamo in argomento: come ben sapete le periodiche revisioni dello Standard del C si limitano sempre a poche e essenziali aggiunte, già che il C è un linguaggio, per sua natura ed origine, stabile e completo. Proprio il contrario del C++ che, ad ogni revisione, sembra dire: "Fino ad oggi abbiamo scherzato, ma da adesso il C++ è un linguaggio nuovo.". Ma non è che avere un linguaggio nuovo ogni tre (3!) anni è un po' eccessivo? E non è che aggiungere n-mila novità ogni volta (molte di uso dubbioso e che, probabilmente, quasi nessuno userà mai, come già fatto notare dal Maestro Rob Pike) è un po' ridicolo e offensivo per chi già lo usa bene (tra cui, modestamente, il sottoscritto) da anni? Ma questa è un altra storia...)

E ora è il turno della cattiva notizia, che è meno cattiva di quel che sembra:

  • _Atomic ha una doppia personalità: è un type qualifier ma è anche un type specifier, e questo origina, come vedremo tra poco, alcune considerazioni e dubbi.

E veniamo al dunque: nel titolo di questo articolo preannunciavo la descrizione del come, quando e perché usare _Atomic in C, per cui ora ci tocca cominciare con:

IL COME:

Come abbiamo appena visto _Atomic ha due personalità, e quindi possiamo scrivere cose come queste:

int dummy; // un int normalissimo che si chiama dummy
_Atomic int ato_dummy1; // uso come type qualifier: ato_dummy1 è una
// variabile che è una versione atomica di un int
_Atomic(int) ato_dummy2; // uso come type specifier: ato_dummy2 è una
// variabile di un nuovo tipo "int atomico"

La differenza tra qualificatore e specificatore è sottile: ad esempio nel semplice caso descritto sopra ato_dummy1  e ato_dummy2  sono, in effetti, la stessa cosa. Diciamo che la versione "qualifier"  ha un uso più familiare (specialmente dopo aver letto gli ultimi articoli... li avete letti, vero?) e può essere anche combinata con gli altri type qualifier (volatile, restrict e const). Invece la versione "specifier" non può essere combinata con i qualificatori  (incluso lo stesso _Atomic). Bisogna anche aggiungere che (come specificato nello standard) un "tipo atomico" non necessariamente ha la stessa struttura di basso livello (size, allineamento, ecc.) del tipo da cui deriva (però potrebbe averla: dipende dall'implementazione).

E adesso un piccolo esempio di casi reali:

_Atomic const int *ptr1; // pointer a un atomic const int
const _Atomic(int) *ptr2; // anche questo è un pointer a un
// atomic const int

_Atomic const volatile int *ptr3; // pointer a un atomic const volatile int
const _Atomic(volatile int) *ptr4; // errore: non si può combinare se si usa
// come type specifier

E per concludere in bellezza questo paragrafo, vi dirò un segreto di pulcinella: anche se esistono le due possibilità di uso viste sopra, in pratica non se ne usa nessuna delle due, visto che, molto convenientemente, l'uso delle variabili atomiche passa attraverso l'inclusione dell'header  stdatomic.h  che contiene un gran gruppo di tipi predefiniti (sono ben 37), che si dovrebbero usare in maniera preferente. Quindi, ad esempio, un codice normale è questo:

#include <stdatomic.h> // include standard del type qualifier _Atomic

// funzione main
int main(void)
{
// dichiaro due variabili atomiche usando i tipi predefiniti in stdatomic.h
atomic_bool my_ato_bool;
atomic_int my_ato_int;

// uso le variabili atomiche my_ato_bool e my_ato_int
...
}

E direi che a questo punto è evidente che perdersi nel dilemma "uso il qualificatore o lo specificatore?"  invece di usare i tipi predefiniti e un po' una sega mentale, tranne in particolari casi in cui si vogliono usare versioni atomiche di tipi complessi (strutture) invece che di tipi semplici (e anche questo si puo fare!).

E ora siamo pronti a passare al prossimo punto:

IL QUANDO:

Come anticipato sopra, _Atomic è decisamente riservato all'uso nella programmazione multithread: infatti, in questo tipo di programmazione l'accesso ai dati condivisi (shared, per gli amici) deve essere sempre "sicuro", perché se due (o più) thread accedono simultaneamente allo stesso dato si può produrre il famigerato "data race" con effetti indesiderati e imprevedibili. Al proposito lo standard del C dice:

"The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior." [The C Standard, section 5.1.2.4, paragraph 25 [ISO/IEC 9899:2011]]

Nel mondo pre-C11 queste situazioni si risolvevano, al solito, usando i classici strumenti di sincronizzazione dei thread (ad esempio i mutex, oppure usando, molto erroneamente, volatile), ma ora, grazie a _Atomic è molto più semplice scrivere codice senza "data races", usando variabili atomiche per i dati condivisi. Gli strumenti di sincronizzazione tipo mutex e spinlock si usano ancora, ma per usi più specifici di sincronizzazione e non di accesso a dati shared.

Quale è, ora, il prossimo punto? Ah si, è:

IL PERCHÉ:

Il perché direi che si può descrivere facilmente: come visto nel punto precedente, nella programmazione multithread è obbligatorio evitare i "data races". Questo si è sempre ottenuto usando gli strumenti classici a disposizione (mutex, spinlock, ecc.). Ora, con l'introduzione in C11 di _Atomic ci viene data la possibilità di risolvere lo stesso problema in modo più semplice (ottenendo codice più compatto e pulito) ed efficiente (le operazioni atomiche non eseguono lock dei thread, e quindi sono generalmente più veloci delle operazioni fatte con i mutex). Quindi direi che usare _Atomic è una scelta azzeccatissima e quasi obbligata.

E adesso, come sempre, è venuto il momento del codice, per cui vi propongo un esempio semplicissimo, che è uno dei tanti possibili. Anche stavolta l'ho scopiazzato da cppreference.com, perché non avevo voglia di scriverne uno ad-hoc, ma anche perché è veramente un bell'esempio, molto calzante. E vi dirò di più: questo stesso esempio (identico o con piccoli cambi) l'ho trovato anche in altre pagine della rete, quindi non posso dire esattamente quale sia la fonte originale (che potrebbe non essere neppure cppreference.com, ah ah ah). Vai col codice!

#include <stdio.h>
#include <threads.h>
#include <stdatomic.h>

atomic_int ato_cnt; // un int atomico usato come contatore
int cnt; // un int normale usato come contatore

// updateCnt() - funzione di update dei counter eseguita da ogni thread
int updateCnt(void* thr_data)
{
// incremento i counter per 1000 volte
for (int n = 0; n < 1000; ++n) {
++ato_cnt;
++cnt;
// per questo esempio, il relaxed memory order è sufficiente,
// quindi si dovrebbe usare:
// atomic_fetch_add_explicit(&ato_cnt, 1, memory_order_relaxed),
// ma va bene anche il semplice incremento (++) usato
}

return 0;
}

// funzione main
int main(void)
{
// avvio 10 thread
thrd_t thr[10];
for (int n = 0; n < 10; ++n)
thrd_create(&thr[n], updateCnt, NULL);

// attendo la fine dei 10 thread
for(int n = 0; n < 10; ++n)
thrd_join(thr[n], NULL);

// i risultati!
printf("Il contatore atomico vale: %u\n", ato_cnt);
printf("Il contatore normale vale: %u\n", cnt);
}

E se compilate ed eseguite l'esempio i risultati saranno (si fa per dire) sorprendenti: ato_cnt  varrà 10000 mentre cnt  varrà molto meno (ma chissà perché?).

Notare, nella nota scritta dopo il ++cnt , che si cita la funzione atomic_fetch_add_explicit(): ecco, le operazioni atomiche possono essere semplici (ad esempio il "++" eseguito su una variabile atomica è garantito come "atomico"), ma in alcune situazioni bisogna usare alcune operazioni speciali (tipicamente di fetch, load e store) e, magari, bisogna anche curare il modo di riempimento della memoria (il memory_order). Ma questi sono dettagli che vi consiglio di approfondire autonomamente leggendo bene i manuali e scrivendo qualche programma di test.

E per oggi penso che possa bastare. Teoricamente dovrei dedicare un altro Endgame al type qualifier const... ma const non è proprio una novità, ed è, oramai, di uso molto comune (specialmente per chi usa anche il C++), quindi quasi quasi lo salto, perché non ci sono molti misteri da svelare sull'argomento. Vedremo...

Ciao, e al prossimo post!

domenica 15 agosto 2021

Restrict: Endgame
come, quando e perché usare il type qualifier restrict in C

Scott Lang/Ant-Man: Voi avete mai studiato la fisica quantistica?
Natasha Romanoff/Vedova Nera: Solo per fare conversazione.

Beh, vediamo un po': siamo ad Agosto, fa caldo, sono in ferie... macchimmelofaffare di scrivere un post? E vabbè, lo faccio, ma sarà un post leggero e frivolo, in cui riutilizzerò la struttura, il tema e perfino il titolo dell'ultimo articolo che ho scritto. E quindi mi ispirerò ancora al formato tutto trame e sottotrame del bel Avengers: Endgame per fare una dotta (?) disquisizione nonché "chiusura finale"  su un altro misterioso e (fortunatamente) poco usato type qualifier del C: restrict.

...ti spiego: restrict non serve a niente...

Per seguire lo stesso schema dell'ultimo articolo bisogna ora dare le notizie, e direi di cominciare con quella buona:

  • La buona notizia è che, similmente a volatile che si usava solo col contagocce, restrict si usa ancora meno, visto che è veramente poco utile (e nella citazione di chiusura dell'articolo capirete il perché) ma, comunque, non è vietato usarlo.

E anche qui c'è una cattiva notizia collegata a quella buona:

  • Visto che usare restrict è possibile, se lo si usa bisogna usarlo bene, altrimenti si possono avere comportamenti indefiniti (undefined behavior) durante l'esecuzione del programma: macchittelofaffare?

E veniamo al dunque: nel titolo di questo articolo preannunciavo la descrizione del come, quando e perché usare restrict in C, per cui ora ci tocca cominciare con:

IL COME:

Ricordiamo che restrict è un type qualifier, e quindi si usa (solo) per aggiungere una proprietà a un pointer (ripeto, solo a un pointer) a un tipo qualsiasi già esistente, quindi il come è semplicissimo e, ad esempio, possiamo scrivere:

int *dummy; // un int pointer normalissimo che si chiama dummy
int *restrict res_dummy; // un int pointer di tipo restrict che si chiama res_dummy

E vi ricordo nuovamente che i type qualifier sono solo quattro: volatile, restrict, const e _Atomic, quindi ce ne mancano ancora due da analizzare... (uff, siamo solo a metà!). E ora siamo pronti a passare al prossimo punto:

IL QUANDO:

Per capire il quando bisogna descrivere brevemente cosa fa restrict applicato a un pointer: dice al compilatore che l'oggetto puntato da quel pointer sarà puntato esclusivamente attraverso quel pointer e mai con un altro: quindi restrict si dovrebbe usare quando abbiamo il controllo totale del codice scritto e siamo sicuri che questa condizione di uso esclusivo sarà sempre rispettata (e se no: undefined behaviour!). Uhm, "controllo totale del codice scritto" questo è possibile per programmatori molto esperti (ne ho già parlato) però diventa molto difficile lavorando in team (anche piccoli).

E ora siamo pronti per il prossimo punto, che è:

IL PERCHÉ:

Il perché direi che si può riassumere in: perché voglio ottenere un codice super-ultra-stra-mega efficiente e ottimizzato e, visto che non mi fido delle ottimizzazioni realizzate autonomamente dal compilatore, voglio aiutarlo un po'... beh, questo appare un po' presuntuoso e pretenzioso e, tra l'altro, ne ho già parlato qui.

Riassumendo, a questo punto possiamo trarre la inevitabile conclusione, che è: restrict non è del tutto inutile, ma è da usare con tale parsimonia e attenzione che si può benissimo farne a meno, e non a caso in alcuni ambienti di programmazione molto restrittivi (ad esempio quelli in cui si usano le norme MISRA) l'uso è scoraggiato.

È però una buona abitudine usare restrict quando si scrivono funzioni molto ottimizzate (magari di libreria) con uso di argomenti "limitati"  (ma con la condizione di documentare adeguatamente agli utilizzatori le restrizioni d'uso). Ad esempio, se ho bisogno di scrivere una mia versione in C della memcpy(), perché non voglio/posso usare quella standard, dovrei dichiararla come:

void *myMemcpy(void *restrict dest, const void *restrict src, size_t n);

Così la nuova funzione avrebbe esattamente le stesse limitazioni e lo stesso prototipo della memcpy() standard, dove gli argomenti src e dest non possono "overlappare".

E adesso, dopo tante parole, ci vuole un po' di codice, per cui vi propongo un esempio semplicissimo, che è uno dei tanti possibili (l'ho scopiazzato da cppreference.com perché non avevo voglia di scriverne uno ad-hoc):

// copy() - funzione di copia senza overlap
void copy(int n, int *restrict dest, int *restrict src)
{
while (n-- > 0)
*dest++ = *src++; // nessuno degli oggetti modificati tramite *dest
// è uguale a nessuno degli oggetti letti tramite
// *src. Il compilatore è libero di ottimizzare,
// vettorializzare, mappare le pagine, ecc.
}

// funzione main
int main()
{
extern int d[100];
copy(50, d + 50, d); // OK
copy(50, d + 1, d); // Undefined behavior: d[1] è utilizzato attraverso
// entrambi dest e src in copy()
}

E per concludere, vi aggiungo la citazione (che ho già usato qui e qui) di uno molto più bravo di me, come avevo promesso all'inizio dell'articolo. Una citazione che riassume quello che penso di restrict, e che può aiutare a tornare coi piedi per terra quando ci si perde in dettagli ininfluenti e si scrive del pessimo codice pensando di scrivere un capolavoro:

"The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming" [Donald Ervin Knuth, "Computer Programming as an Art", 1974]

E per oggi penso che possa bastare. Rilassatevi, che siamo in Agosto!

Ciao, e al prossimo post!