Introduzione alle API Win32

Capitolo 9: problemi di struttura

Il programmino presentato nel Capitolo 7 ci ha già suggerito un tipo di problema, quello di garantire l'univocita' di identificazione delle risorse, cui abbiamo proposto una soluzione al Capitolo 8.

Più sottile è un secondo tipo di problema, la cui radice sta nel semplice "if ... else if ..." che funge da "corpo" della dialog procedure -- e non è che sostituire questo costrutto con uno switch migliorerebbe di molto le cose!

La "natura profonda" del problema è questa: una dialog procedure può ricevere centinaia di tipi diversi di messaggi a cui rispondere -- e alcuni di essi, come WM_COMMAND, prevedono poi a loro volta molti sotto-casi, completamente distinti l'uno dall'altro dal punto di vista della logica funzionale del programma.

"Ficcare" in una sola funzione, come si può, ahimè sin troppo facilmente!, essere tentati di fare, tutta questa logica disparata e varia, sarebbe però un gravissimo errore strutturale: una funzione dovrebbe, infatti, "svolgere un compito", non centinaia di compiti diversi a seconda degli argomenti...! Il "motto" da ricordare e seguire: "una funzione ha una funzione" (leggendo la parola "funzione" in due sensi diversi dai due lati del verbo "ha": a sinistra, quello del linguaggio C o C++, a destra, quello di "scopo", "ruolo", "compito").

Se si adotta invece la "struttura spontanea" per una dialog procedure, imperniata su di un unico grosso switch (o, equivalentemente, un solo grosso albero di if/else), si buttano alle ortiche tutti i principi di modularità, e, con essi, ogni chance di scrivere un programma di buona qualità, tale da essere ragionevolmente "manutenibile" in futuro.

La soluzione non è difficile -- e infatti, è stata reinventata più volte, indipendentemente. Si tratta, in sostanza, di approcci alternativi alla soluzione del problema, di per sè assai generale, di "eseguire compiti diversi a seconda di un valore di input (o più di uno)".

L'approccio "ingenuo" usa switch, o if/else if, per dirimere fra gli input, e, una volta determinato in questo modo quale caso si sia presentato, esegue codice nel corpo stesso dello switch (o if/else); quello "semi-ingenuo", non molto migliore, usa lo stesso tipo di switch, ma, una volta determinato il caso che si è presentato, chiama poi una funzione per fare "il lavoro vero". (Ci sono anche sub-approcci "più svegli", imperniati su quello "semi-ingenuo" ma con una qualche "idea salvatrice", di varie possibili nature, che li rende un poco migliori; ma quello che tutti hanno in comune è che la logica di base della "scelta del compito da eseguire" è espressa in termini di "codice eseguibile" -- il famoso switch o if/else).

Gli approcci migliori si basano su una struttura dati per registrare la corrispondenza fra "caso che si è presentato" e "lavoro da fare in quel caso"; i puntatori a funzione servono, in C, proprio per accedere all'istante a questo "lavoro da fare", espresso, normalmente, in una funzione (una funzione che, come da motto, ha una funzione, specificamente quella di gestire un singolo, specifico messaggio, o un piccolo gruppo di messaggi fortemente correlati fra di loro).

In C++, sono possibili approcci abbastanza diversi (anche se, alla fin fine, anch'essi si concretano in "tabelle di puntatori a funzione" -- ma molto può, se vogliamo, gestircelo rapidamente il compilatore, in modo implicito, elegante, e sicuro), ma penso che, visto che molti utenti C++ non hanno comunque un chiaro modello mentale di cosa corrisponde a ogni costrutto del linguaggio (testo caldamente suggerito: "Inside the C++ Object Model", S. Lippmann, Addison-Wesley!), sia più istruttivo esaminare solo approcci C (usabili naturalmente anche in C++, ma, come "funzione", bisognerà usare una funzione "libera", oppure una funzione che sia membro statico di una classe, NON una "normale" funzione-membro di una classe, che è tutt'altra cosa...!).

Un framework di questo tipo, nei primi anni 90, lo promulgò ad esempio A. Schulmann (per Windows 3.0, un prodotto a 16 bit), nel suo ottimo testo "Undocumented Windows". Nello schema di Schulmann, si associa ad ogni finestra un semplice array, che usa come indice il codice di messaggio ricevuto; ogni cella dell'array contiene, semplicemente, il puntatore alla funzione da eseguire, oppure 0 per indicare "nulla di speciale, usa il default". Ci sono circa un migliaio di messaggi, quindi 4,000 puntatori per finestra (oggi, a 32 bit, 16Kbyte) è l'overhead di spazio di questo metodo; a fronte dello spazio notevole, bisogna ammettere la sua semplicità, rapidità, flessibilità.

In termini moderni, avremmo qualcosa come...:

typedef DLGPROC dlgarray[WM_USER]; dlgarray gestori; BOOL CALLBACK genericDP(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { DLGPROC delega = 0; if(uMsg<WM_USER) delega = gestori[uMsg]; if(delega) return delega(hwndDlg,uMsg,wParam,lParam); return FALSE; }

C'è ancora un piccolo problema, naturalmente...: questa cosiddetta-generica dialog procedure usa lo stesso array di puntatori a funzione per tutti i dialoghi che gestisce -- soluzione poco elegante, in quanto imperniata su di un array globale, e, per la stessa ragione, assai poco flessibile e generale.

Per migliorare la flessibilità, si può associare a ogni HWND di un dialogo gestito, un diverso array di puntatori a funzione (dlgarray); a questo scopo si può usare qualsiasi struttura dati che implementi una corrispondenza (o "mappa", o "array associativo", ecc, che dir si voglia) -- o, in alternativa, si può utilizzare uno dei vari tipi di corrispondenze HWND->dati-utente che l'API di Windows mette a disposizione. Esamineremo meglio queste funzionalità di "corrispondenza" più avanti nel corso del tutorial.

Vi sono inoltre dei problemi di prestazioni. La grande maggioranza dei dialoghi hanno effettiva necessità di gestire un modesto numero di messaggi, sulle centinaia che sono possibili -- per la grande maggioranza dei messaggi, la funzionalità di default che si ottiene ritornando FALSE ("non gestito") dalla dialog procedure è proprio quello che si desidera. Ma l'approccio "array di gestori" obbliga comunque a riservare migliaia di byte di memoria nell'array di puntatori a funzioni -- anche se la grande maggioranza delle entry saranno zero, e solo poche avranno effettivamente dei puntatori "utili". Lo spazio sprecato implica anche spreco di tempo, a causa dell'uso della memoria virtuale.

Questo è il classico "problema della matrice sparsa", ed è suscettibile delle classiche soluzioni a questo problema. Una di esse, fra le più semplici, è di sostituire all'array una lista di coppie (indice,contenuto); invece di array[indice] si usa un ciclo di ricerca, in pseudo-codice:

esamina tutte le coppie nella lista: se l'indice che cerchiamo e` il primo elemento della coppia, abbiamo trovato, torna il contenuto se non trovato, torna zero

Naturalmente, se vi sono troppe coppie nella lista, questa ricerca lineare può dare tempi troppo lunghi; possono usarsi tutti i classici schemi di algoritmi e strutture dati applicabili in questi casi, dalla ricerca binaria su array sortato, alle tabelle di hash, agli alberi red-black (o altre variazioni), eccetera. In pratica, visti i piccoli numeri di messaggi tipicamente gestiti (al massimo qualche decina), la ricerca lineare va benissimo, magari con un minimo di cura nel mettere verso l'inizio della lista quei messaggi da gestire che sappiamo essere particolarmente frequenti, o particolarmente vincolati a tempi di risposta più stretti.

Vista l'importanza centrale di questo tema per l'organizzazione di "grosse" applicazioni Windows, esistono molti altri schemi, e variazioni su di essi, comprese "raffinatezze" (ovvero "complicazioni":-) di ogni tipo. Accenneremo più avanti a uno schema semplice, e built-in in Windows, detto "message crackers", che presenta vantaggi d'altro tipo.

Il punto importante, in quest'ottica, è un altro: non ci preoccuperemo più di tanto, nel seguito, del tema "come determinare quale codice eseguire a fronte di un certo messaggio o comando", dando invece per scontato che sia in piedi un qualche "schema di gestione", tra i vari suelencati, che, per una via o per l'altra, sia in grado di "consegnare" alla funzione più appropriata il messaggio da gestire, coi relativi parametri; e ci interesseremo, invece, sostanzialmente a quali siano questi messaggi, e quale lavoro debbano fare le nostre funzioni scritte per gestirli.


Capitolo 8: identificare le risorse
Capitolo 10: controlli: i BUTTON
Elenco dei capitoli