Newsgroup: it.comp.lang.c++
Titolo: Fornire informazioni diagnostiche per chiamate eseguono con successo

Ciao a tutti,
Questa e' la traduzione di una domanda che ho appena posto su
comp.lang.c++.moderated. Ho pensato che in fondo non c'e' motivo di
riservare le discussioni piu' interessanti agli anglofoni :-)
Sto cercando consigli su come fornire informazioni diagnostiche per
chiamate a funzioni che completano l'esecuzione con successo.
Sto scrivendo una libreria che non voglio "zavorrare" con un meccanismo
di logging, ma per la quale voglio comunque mettere a disposizione
informazioni diagnostiche sul risultato delle chiamate ad alcune
funzioni. Le situazioni di errore sono facili da gestire, basta
inserire le mie informazioni nelle eccezioni che lancio; quando pero'
le chiamate completano con successo, ma voglio comunque fornire
informazioni diagnostiche, non sono sicuro di come procedere. Vedo
alcune alternative:
1) Lanciare un'eccezione di avviso, nella quale mettere magari anche
l'eventuale valore
   di ritorno;
2) Restituire un oggetto particolare, che combini il valore di ritorno
con l'eventuale
   informazione diagnostica, nel qual caso posso
   2a) Forzare la verifica dell'informazione diagnostica, inserendo un
operatore di
       conversione per il tipo di ritorno originale che lanci
un'eccezione se c'e' informazione
       disponibile e non e' stata consultata;
   2b) Lasciare all'utente l'opzione se consultare o meno
l'informazione diagnostica.
Avevo iniziato ad implementare l'approccio 1), ma comincio a pensare
che gia' il 2a) sia un po' esagerato. Che ne pensate? Avete idee
alternative?
Mi rendo conto che questo e' proprio cio' di cui tratta I'Aspect
Oriented Programming, ma la mia e' una libreria di basso livello
piuttosto che un'applicazione o un framework; la scelta di utilizzare
un approccio di tale portata spetta agli utenti della mia libreria.
Ciao e grazie,
Nicola Musatti

 alternative?
Io userei un sistema log...catch!
Con un sano utilizzo di macro e un po' di fortuna, potrebbe venir fuori 
qualcosa di buono. Purtroppo non ho tempo per tentare questa strada. 
Certo che in futuro potrebbe fare comodo anche a me una simile soluzione.
Kiuhnm

 alternative?
Non so cosa intendi con "appesantire con un sistema di log", ma... io 
sceglierei esattamente un sistema di log.
Con la differenza che terrei il log in ram, a consultazione facoltativa: 
la funzione viene chiamata, fa  quel che deve, scrive nel ramlog il 
risultato (magari invece di una linea di testo ci crea un oggetto con 
delle informazioni numeriche e/o dei flag). Diciamo che il ramlog 
sarebbe di dimensioni finite, una FIFO con solo gli ultimi N risultati, 
tanto per non renderlo "pesante" :-)
Poi starà al codice chiamante decidere se consultare il log per vedere 
com'è andata l'ultima operazione oppure tirare dritto per la sua strada.
Si potrebbe anche pensare di usare diverse FIFO per memorizzare diversi 
tipi di informazione, con la funzione chiamante che scrive nell'una o 
nell'altra a seconda di cosa è successo e il chiamante che testa solo la 
FIFO del tipo di eventi che gli interessa per vedere se ci sono novità 
per lui... ma sto partendo per la tangente :-)

 alcune alternative:
Normalmente quando ho la necessità di comunicare all'esterno informazioni 
diagnostiche (tipicamente per logging o debugging) prevedo uno o più 
meccanismi di callback opzionali basati su puntatori a funzione o -più 
spesso- su puntatori a classe base astratta. In questo modo, il codice 
client decide se e come ricevere tali informazioni, e non devo "inquinare" 
l'interfaccia con dettagli che non sono strettanemente necessari per le 
operazioni che si devono svolgere.
 
Max


Giusto per chiarire, qualcosa come:
class warning {
  public:
    virtual std::string what() const = 0;
    virtual ~warning() {}
};
class my_warning : public warning {
  std::string what() const { return "Dispari!"; }
};
int f(int i, warning ** w = 0) {
  if ( i % 2 ) {
    if ( w != 0 )
      *w = new my_warning;
    return ( i + 1 ) / 2;
  }
  if ( w != 0 )
    *w = 0;
  return i / 2;
}
int main() {
  warning * w;
  f(7, &w);
  if ( w != 0 ) {
    std::cerr << w->what();
    delete w;
  }
  return f(8);
}
Ciao,
Nicola

 
No, intendevo più o meno il contrario.  E' il codice client che decide, e 
instanzia l'implmentazione della classe base. Il codice -diciamo così- 
*della libreria* userà un'interfaccia per comunicare le informazioni, e non 
entrerà nel merito di come e dove queste informazioni vengono memorizzate. 
Per il logging, in particolare, questo approccio mi pare valido, in quanto 
è solitamente più sensato che sia il codice client a decidere le 
strategie di logging, non il codice della libreria.
Esempietto:
  // libreria
  class error_notifier { public: virtual void notify_error(int) = 0 ; } 
  int funzione( int arg, error_notifier* n = 0 );
 // codice client
 struct my_notifier : public error_notifier  
 { int err; virtual void notify_error(int e) { err = e; }  }
 
 my_notifier n;
 int value = funzione(argomento, &n);
 int lasterr = n.err;
 
Ovviamente, se 'funzione' è una funzione membro di una classe, 
l'error_notifier sarà configurato una sola volta all'instanziazione 
dell'oggetto.
Max

Nicola Musatti ha scritto:
un tempo (usavo il VB, che ha una gestione di errori molto 
diversa, e rispetto al C anche avanzata, o quantomeno facile e 
robusta se non molto dettagliata e flessibile) mi ero trovato in 
problemi simili, che penso fossero ricorrenti.
Ho poi trasposto il codice in C++ con qualche adattamento, ma 
senza mai sfruttarlo perché richiede troppa autodisciplina nel 
design, che non ho e alla quale il codice non obbliga.
Cmq, si trattava di questo.
Ogni libreria (intesa come corpo di funzioni affini o correlate) 
in un dato modulo di codice, aveva due strutture di 
memorizzazione interna degli errori (logging in RAM, privato e 
read only dall'esterno) : una queue circolare di dimensione 
prefissata (nel C++ size era argomento di template) per errori 
"veniali" e uno stack per errori gravi.
Poi venivano esposti property per leggere uno a uno gli errori 
(o anche per indice o persino copiare gli interi array di 
stringhe coi messaggi memorizzati), esposte all'esterno.
In C++ si potrebbe fare ereditare una class ErrManager a tutte 
le classi che devono loggare errori.
Man mano che le funzioni della libreria vengono chiamate 
scrivono loro messaggi diagnostici e condizioni di errore.
E' lasciato al chiamante pulire lo stack degli errori gravi, nel 
senso che se uno li leggeva solo erano considerati NON gestiti, 
e se si accumulavano causavano un errore del gestore di errori 
stesso, tipo stack pieno. Con gli errori lievi e i messaggi 
informativi invece si sovrascrivevano circolarmente.
C'erano poi flag impostabili dall'interno per dare eco 
eventualmente delle condizioni di errore, tipo msgbox oppure 
impostare l'handle di uan finestra o controllo grafico 
supportante testo da stampare, e gli errori venivano mostrati 
con modo "simil" callback, senza altra gestione di default che 
mostrarli.
Il casino è appunto che in VB tutto era lasciato libero, il 
codice poteva aderire ai servizi di gestione errori come anche 
no, e se si, doveva leggere i suoi bravi messaggi, e rispondere 
"gestito", cancella il messaggio, al gestore. Nessuna funzione 
era obblicata a farlo se non dall'avere un parametr del tipo 
reference (byref) a gestore di errori.
La gestione automatica, di sola visualizzazione, era poco utile 
in pratica.
In C++ mi sono arenato a metà del porting, perché la filosofia 
di base era troppo diversa per questo approccio grezzo.
La gestione degli errori è una di quelle cose che non ho mai 
imparato a trattare in modo organico.
Se uso una certa efficienza sfruttando caratteristiche 
specifiche-locali del codice, la gestione diventa eterogenea e 
il chiamante non sa bene come si regolano le librerie.
Una gestione iper omogenea mi ha sempre portato a 
sovradimensionare e generalizzare al punto tale che la gestione 
diventava goffa e onerosa.
Le eccezioni non le padroneggio quanto vorrei.
ciao
Soviet


 l'informazione diagnostica.
Pensandoci meglio questi approcci hanno in comune la caratteristica di
effettuare comunque il reperimento dell'informazione diagnostica,
indipendentemente dal fatto che all'utente interessi o meno. Questo
potrebbe essere un aspetto negativo dal momento che il reperimento
comporta attivita' potenzialmente dispendiose. Una soluzione basata su
callback opzionale, come suggerito da Max, potrebbe risultare
preferibile.
Ciao,
Nicola

 indipendentemente dal fatto che all'utente interessi o meno.
Hai già dimenticato il post "return type overloading"?
Pensaci bene.
Kiuhnm


Ni, nel senso che indipendentemente da come l'oggetto warning viene
recuperato dal chiamante posso incapsulare al suo interno
l'informazione minimale necessaria per recuperare l'informazione
completa. A quel punto e' il chiamante che decide se usare
l'informazione completa o meno. Immagino che tu pensassi a qualcosa
tipo:
class info {};
template <typename ReturnT> class warning {
  public:
    warning(ReturnT const & r) : ret(r) {}
    operator ReturnT() { return ret; }
    info get_info() { return info(); }
  private:
    ReturnT ret;
};
warning<int> f() { return warning<int>(42); }
int main() {
  // uso l'informazione
  warning<int> w = f();
  info i = w.get_info();
  int ret = w;
  // non uso l'informazione
  return f();
}
In altri contesti la troverei decisamente accettabile. Si potrebbe
anche fornire un operatore di conversione a info che consentirebbe di
ignorare comodamente anche il valore ritornato.
Nel mio caso mi trovo ad avere il problema di non poter posporre
indefinitamente il reperimento dell'informazione diagnostica (pensa a
errno o allo stato degli stream C++) e quindi diventa inevitabile un
meccanismo dove il chiamante specifica a priori se vuole l'informazione
diagnostica o meno e in caso affermativo questa viene reperita nel
corso della chiamata alla funzione in questione.
Ciao,
Nicola

 corso della chiamata alla funzione in questione.
Quindi l'overloading consisterebbe nell'eseguire tutte le funzioni e poi 
nel restituire i risultati solo di quella scelta...
Kiuhnm


Non sono sicuro di seguirti. In ogni caso in queste condizioni
l'approccio della callback opzionale mi appare piu' naturale.
Ciao,
Nicola

 l'approccio della callback opzionale mi appare piu' naturale.
Invece ha il difetto d'inquinare l'interfaccia.
Cosa succede se si vuole scegliere il CB funzione per funzione e le 
funzioni hanno già alcuni parametri con valori di default?
Inoltre non capisco proprio quale problema hai con l'overloading. E' 
chiaro che l'utente comunica *a priori* la propria scelta.
Le funzioni vengono eseguite durante la conversione, non prima.
Tanto è vero che la funzione da eseguire viene scelta basandosi proprio 
sul return type.
Kiuhnm

 sul return type.
Una cosa piuttosto simpatica potrebbe essere:
int i = compute(param1, param2, param3)
                .log(call_back, 500)
                .float_precision(8)
                .check_params(LEVEL3)
                .test(UNIT1, 10, 20)
                .optimize(CPU_P4);
int j = compute(param1)
                .log(call_back);
int k = compute(param1)
                .check_params()
                .fixed_point();
Spero di aver reso l'idea!
Kiuhnm

 sul return type.
Il problema e' che se uno fa
return_with_info<int> & r1 = f1();
Estende indefinitamente il lifetime del valore di ritorno. E' vero che
a questo si puo' ovviare con un doppio passaggio, ritornando un proxy
dalla funzione che puo' essere convertito a return_with_info<int>
scatenando il reperimento dell'informazione oppure ad int, perdendo la
diagnostica.
Devo pero' verificare se posso evitare che questo sopravviva piu' del
necessario.
Ciao,
Nicola

 necessario.
Per esempio cosi':
class diag {};
class shield {
    class proxy {
    public:
        operator int() { return 42; }
        operator diag() { return diag(); }
    };
    friend proxy func();
};
shield::proxy func() {
    return shield::proxy();
}
int main() {
    int i = func();
    diag const & d = func();
// La riga qui sotto e' illegale perche' shield::proxy e' un tipo
privato
//    shield::proxy const & p = func();
}
Che pero', a causa della dichiarazione di amicizia, mi pare un incubo
da generalizzare.
Ciao,
Nicola

 un approccio di tale portata spetta agli utenti della mia libreria.
Si, AOP e' molto bello, ma a volte mi lascia perplesso il matching "a mazzi" 
e il disorientamento che rischia di generare (gli oggetti si sparpagliano in 
giro per il codice in maniera pazzesca). La mia impressione e' che sia una 
buona idea, ma che per arrivare ad essere applicata gli manchi ancora 
qualcosa... ma qui finiamo OT =)
--
Marco 

 Estende indefinitamente il lifetime del valore di ritorno.
Mica vero...
return_with_info<int> diventa una closure che ti porti dietro.
La funzione viene eseguita quando esegui la conversione.
Kiuhnm

 La funzione viene eseguita quando esegui la conversione.
Adesso guardo l'altro post, ma non capisco che fastidio ti dia che la 
closure sopravviva. E' come chiamare la funzione in due passaggi:
   CALL c = set_call(func, param1, param2);
   c->execute();
o qualcosa del genere.
Invece è addirittura utile quando si vuole che A decida i parametri e 
che B, più tardi, esegua effettivamente la funzione.
Kiuhnm

 e dove queste informazioni vengono memorizzate. 
Se A scrive il client, e B scrive la libreria, e se c'è un problema nella 
libreria, non c'è assolutamente pericolo che A scriva un nuovo client per 
estrarre le informazioni da B. Specialmente se il client fatto da A è già 
dal cliente (piccolo calambour)
Per come sono abituato a lavorare A chiede a B di avere i log, e poi chiama 
al telefono B per farseli spiegare. Io sono B, e la *mia* libreria ha il 
suo bravo sistema di log, alla facciaccia di A. Semmai A deve scrivere il 
codice, e sudare 7 camice o magliette che siano, per eliminare i log.
Dal punto di vista architetturale è una soluzione che non mi piace, ma 
stare in ufficio fino a tardi con grazie a soluzioni "belle" ma non 
pratiche mi piace di meno.
++++
A Nicola chiedo che situazione "umana" si aspetta di trovare. Il codice 
viene dopo. Chi e come sarà usata la sua libreria? Il busillis vero è lì.

 da generalizzare.
Per me complichi le cose senza motivo.
Kiuhnm

 viene dopo. Chi e come sarà usata la sua libreria? Il busillis vero è lì.
Probabilmente nessuno, magari solo io, idealmente dovrebbe essere
accettabile per Boost. Si tratta di una libreria di accesso a RDBMS
indipendente dalla tecnologia sottostante, che per ora supporta solo un
sottoinsieme ridottissimo di ODBC. Un embrione di qualche settimana fa
si puo' trovare qui:
http://boost-consulting.com/vault/index.php?directory=Database
Ciao,
Nicola


In effetti gia' non dichiarando const l'operatore di conversione che
restituisce l'informazione diagnostica rendo necessario come minimo
effettuare un const_cast per accedervi da un reference al proxy, cioe':
class diag {};
class proxy {
  public:
    operator diag() { return diag(); }
};
proxy f() { return proxy(); }
int main() {
  diag const & d = f();   // OK, invoca la conversione
  proxy const & p = f(); // Non posso impedirlo
  diag const & d1 = p;  // KO: p e' const, l'operatore no
  diag const & d2 = const_cast<proxy &>(p);
  proxy & p1 = f();        // KO; non posso collegare un temporaneo
                                  // a un reference non const
}
Potrebbe bastare cosi', salvo che il compilatore Borland, e VC++ con le
estensioni abilitate, si limitano ad emettere un warning.
Ciao,
Nicola

 indipendente dalla tecnologia sottostante
Se ti interessa esiste una libreria python che fa esattamente ciň che vuoi.
Se vuoi ti passo il link per vedere se ci trovi idee utili.

 sottoinsieme ridottissimo di ODBC
Se vuoi ti posso dare la mia esperienza di utilizzo di ODBC sotto windows.

Nicola Musatti ha scritto:

Io userei un context globale, come l'API di OpenGL.
Sembra che recentemente non vada più di moda (multithreading?) ma
personalmente mi sembra molto elegante.
Oppure usi classi ovunque e mantieni uno stato interno.
Saluti  Manlio Perillo

 Sembra che recentemente non vada più di moda (multithreading?)
Il multithreading è fortemente utilizzato, almeno nel mondo windows.
Per questo motivo non mi sembra una buona idea dello stato globale.

 Per questo motivo non mi sembra una buona idea dello stato globale.
In questo caso basta fare in modo che ogni thread diverso vede il suo
stato globale.
Se un thread deve accedere allo stato di un altro thread, significa che
č in cerca di guai...
Saluti  Manlio Perillo


Volentieri, anche se magari non qui. Il mio indirizzo e' valido.
Ciao,
Nicola

 estensioni abilitate, si limitano ad emettere un warning.
Puoi provare ad aggiungere dei 'volatile', ma non credo sia auspicabile.
Esiste anche una leggera variazione della versione con la friendship:
---
class diag {};
template <class T>
class proxy {
   public:
     operator diag() { return diag(); }
};
class shield
{
   class pw {};
   friend proxy<pw> f();
};
proxy<shield::pw> f() { return proxy<shield::pw>(); }
---
Kiuhnm