Newsgroup: it.comp.lang.c++
Titolo: exception safety [proposta di disacussione]

Siccome mi sono reso conto che in questo gruppo
l'argomento della exception safety non viene mai
(a quanto ne so io) trattato, e che a volte si è
portati a scrivere codice che non è exception safe,
ho pensato di postare un esempio che spero
possa aprire una proficua discussione.
L'esempio in questione è una semplice implementazione
di un vettore e viene posta l'attenzione su come
gestire correttamente una risorsa (la memoria
in questo caso):
#include <cstddef>
#include <new>
#include <iostream>
// settabili a piacere
const bool exception_safe = true;
const bool throw_exception = true;
class vector
{
   private:
      std::size_t n;
      float* p;
   public:
      vector(std::size_t nn, float x)
      : n(nn), p(new float[n])
      {
         for(std::size_t i = 0; i < n; ++i) p[i] = x;
      }
      vector(const vector& other)
      : n(other.n), p(new float[n])
      {
         for(std::size_t i = 0; i < n; ++i) p[i] = other.p[i];
      }
      //
      // il throw esplicito di bad_alloc() simula l'esaurimento
      // della memoria
      //
      vector& operator=(const vector& other)
      {
#if !exception_safe
         // evita l'assegnamento a se stesso: v = v
         if(this != &other)
         {
            delete[] p;
            p = new float[n = other.n];
            if(throw_exception) throw std::bad_alloc();
            for(std::size_t i = 0; i < n; ++i) p[i] = other.p[i];
#else
            // Prima acquisisce le nuove risorse...
            float* p_tmp = new float[other.n];
            if(throw_exception) throw std::bad_alloc();
            for(std::size_t i = 0; i < n; ++i)
             p_tmp[i] = other.p[i];
            // ...quindi rilascia le vecchie.
            n = other.n;
            delete[] p;
            p = p_tmp;
#endif
#if !exception_safe
         }
#endif
         return *this;
      }
      float operator[](std::size_t pos) const { return p[pos]; }
      std::size_t size() const { return n; }
      ~vector() { delete[] p; }
};
/*
Nota che nella versione exception safe non è presente il test
per evitare il self assignment.
Infatti, a differenza dell'altra versione, ora il self assignment
è gestito correttamente.
Il test aumenterebbe solo le prestazioni nel caso di self
assignment, ma questo è un evento raro; nei casi comuni il test
è solo un inutile overhead
*/
int main()
{
   vector v(5, 8.89);
   vector v2(1, 5.7);
   try
   {
      v2 = v; // v2 = v2;
   }
   catch(const std::bad_alloc& err)
   {
      std::cerr << "memoria esaurita\n\n";
   }
   // v2 dopo una eccezione dovrebbe restare in uno stato 'valido'.
   // In questo caso v2 resta invariato.
   std::cout << "v2.size = " << v2.size() << '\n';
   std::cout << "v2[0] = " << v2[0] << '\n';
}
Attendo commenti.
P.S. c'è un modo più elegante per gestire la risorsa,
     lo posterò in seguito.
Manlio Perillo

     lo posterò in seguito.
Ti riferisci allo scambio di interiora ?
La mia opinione in ogni caso e' che come ho gia' detto questo
esempio sia troppo semplice: io partirei dal famoso "stack"
che era il soggetto dell'articolo "C++ Exceptions: A False
Sense of Security" di Tom Cargill.
In particolare secondo me dovremmo partire da un esempio in cui
le fonti di possibili exception siano piu' di una (ad esempio
dei metodi che possono fallire oltre a operator new) e magari
esterne alla classe (potrebbe interfacciare metodi o funzioni
esterne che possono generare exception ignote, o addirittura
scrivere un template parametrizzato da una classe generica).
Opinioni ?
Andrea
--
Andrea Griffini, Programmer
http://www.netservers.com/~agriff

   in uno stato 'valido ma ignoto' dopo una exception ?
Definisci "ignoto". Parlando dello stato di un applicazione i termini
ignoto e valido non paiono compatibili.
Ciao,
Nicola

è gestito correttamente.
potrebbe comunque essere evitato anche nella versione non
exception safe, basterebbe allocare tramite un puntatore temporaneo
e fare prima la copia e poi la deallocazione del vecchio vettore.
Paolo

  in uno stato 'valido ma ignoto' dopo una exception ?
Credo che questo dipenda dal problema...
Paolo
p.s.
   Mi scuso per la brevita` dei miei interventi, ma ultimamente ho
   pochissimo tempo libero, mi riservo sempre di rispondere con piu`
calma (per evitare di dire cavolate), ma poi non trovo il tempo!


#include <cstddef>
#include <new>
#include <algorithm>
#include <iostream>
// settabile a piacere
const bool throw_exception = false;
//
// vector_base rappresenta la risorsa (memoria) esplicitamente.
// "resource acquisition is initialization"
//
struct vector_base
{
   std::size_t n;
   float* p;
   vector_base(std::size_t nn)
   : n(nn), p(new float[nn])
   {  }
   void swap(vector_base& other)
   {
      std::swap(n, other.n);
      std::swap(p, other.p);
   }
   ~vector_base() { delete[] p; }
};
class vector: private vector_base
{
   public:
      vector(std::size_t nn, float x)
      : vector_base(nn)
      {
         for(std::size_t i = 0; i < n; ++i) p[i] = x;
      }
      vector(const vector& other)
      : vector_base(other.n)
      {
        for(std::size_t i = 0; i < n; ++i) p[i] = other.p[i];
      }
      //
      // il throw esplicito di bad_alloc() simula l'esaurimento
      // della memoria
      //
      vector& operator=(const vector& other)
      {
         vector temp(other);
         if(throw_exception) throw std::bad_alloc();
         // scambia le rappresentazioni
         this->swap(temp);
         return *this;
      }
      float operator[](std::size_t pos) const { return p[pos]; }
      std::size_t size() const { return n; }
};
/*
Anche in questo caso ho evitato di mettere il test per il
self assignment;
č gestito correttamente e nei casi comuni il test č solo un
inutile overhead.
Questa versione č equivalente alla precedente, e dovrebbe avere
la stessa performance
*/
Manlio Perillo


Manlio Perillo

 C'è bisogno di una nuova mentalità.
Vorrei solo aggiungere che in alcuni casi la sola gestione delle
eccezioni non è sufficiente a garantire la correttezza di un programma.
Ad esempio, se si usano dei dati persistenti, in particolare un
qualsiasi tipo di database, senza la possibilità di tornare ad uno stato
precedente la transazione che ha generato l'eccezione non è possibile
conservare la coerenza del database stesso.
Questo per dire che il compito di scrivere il corretto gestore
dell'eccezione può essere addirittura più complesso che organizzare una
gestione generale delle eccezioni stesse.
EMG


Ho ridato una occhiata all'articolo di Tom Cargill, non č lungo
come pensavo;
quindi come suggerisci parto da questo articolo.
(Comunqe aggiunge poco a quanto ho detto rispetto al mio
 vector).
Per prima cosa posto il codice originale della classe Stack aggiornata
allo Standard:
#include <cstddef>
#include <stdexcept>
template<class T> class Stack
{
   private:
      std::size_t nelems;
      int top;
      T* v;
   public:
      std::size_t count();
      void push(T);
      T pop();
      Stack();
      ~Stack();
      Stack(const Stack&);
      Stack& operator=(const Stack&);
};
template<class T> Stack<T>::Stack()
{
   top = -1;
   v = new T[nelems = 10];
}
template<class T> Stack<T>::Stack(const Stack& s)
{
   v = new T[nelems = s.nelems];
   if(s.top > -1)
   {
      for(top = 0; top <= s.top; top++)
         v[top] = s.v[top];
      top--;
   }
}
template<class T> Stack<T>::~Stack()
{
   delete[] v;
}
template<class T> void Stack<T>::push(T element)
{
   top++;
   if(top == nelems - 1)
   {
      T* new_buffer = new T[nelems += 10];
      for(int i = 0; i < top; i++)
         new_buffer[i] = v[i];
      delete[] v;
      v = new_buffer;
   }
   v[top] = element;
}
template<class T> T Stack<T>::pop()
{
   if(top < 0)
      throw std::underflow_error("pop on an empty stack");
   return v[top--];
}
template<class T> std::size_t Stack<T>::count()
{
   return top + 1;
}
template<class T>
Stack<T>& Stack<T>::operator=(const Stack<T>& s)
{
   delete[] v;
   v = new T[nelems = s.nelems];
   if(s.top > -1)
   {
      for(top = 0; top <= s.top; top++)
         v[top] = s.v[top];
      top--;
   }
   return *this;
}
Un mio primo commento su questa classe č:
non solo non č exception safe, ma č anche bacata!
Se la gente pensa che questo design č la prassi
stiamo freschi -:).
Reimplementare questa classe in modo che sia
exception safe č molto facile ed il codice
risultante risulta molto piů leggibile ed 'elegante'.
Per non appesantire troppo questo post, spedisco
la mia versione in un reply a questo messaggio.
Manlio Perillo


#include <cstddef>
#include <new>
#include <algorithm>
#include <stdexcept>
//
// Per prima cosa ci liberiamo dalla diretta gestione
// della memoria
//
template<class T> struct Stack_base
{
   std::size_t nelems;
   T* v;
   Stack_base(std::size_t n)
   : nelems(n), v(new T[n])
   {  }
   void swap(Stack_base& other)
   {
      std::swap(nelems, other.nelems);
      std::swap(v, other.v);
   }
   ~Stack_base() { delete[] v; }
};
/*
Ho cambiato un poco l'interfaccia di Stack per renderla
piů coerente, inoltre ho modificato il comportamento
di top.
Non c'č ragione per cui push non debba accettare una const
reference, cosě l'ho modificato.
Lo stesso varrebbe per pop, ma mi rimendo a dopo
*/
//
// Classe Stack
// E' richiesto che il distruttore di T non lanci
// eccezioni
//
template<class T> class Stack: private Stack_base<T>
{
   private:
      std::size_t top;
      static const std::size_t min_size = 10;
   public:
      Stack();
      Stack(const Stack&);
      Stack& operator=(const Stack&);
      std::size_t count() const { return top; }
      void push(const T&);
      T pop();
};
template<class T> inline Stack<T>::Stack()
: Stack_base<T>(min_size), top(0)
{  }
template<class T> inline Stack<T>::Stack(const Stack<T>& other)
: Stack_base<T>(other.nelems), top(other.top)
{
   std::copy(other.v, other.v + other.top, v);
}
//
// Offre la strong guarantee: se una eccezione č
// lanciata, l'oggetto resta immutato
//
template<class T>
inline Stack<T>& Stack<T>::operator=(const Stack<T>& other)
{
   // crea un nuovo stack temporaneo
   Stack<T> tmp(other);
   // solo ora aggiorna lo stato di this
   top = tmp.top;
   this->swap(tmp);
   return *this;
}
//
// Offre la strong guarantee: se una eccezione č
// lanciata, l'oggetto resta immutato
//
template<class T> void Stack<T>::push(const T& x)
{
   if(top == nelems)
   {
      // crea memoria per un nuovo stack temporaneo
      Stack_base<T> tmp(nelems + min_size);
      std::copy(v, v + top, tmp.v);
      tmp.v[top] = x;
      // solo ora aggiorna lo stato di this
      this->swap(tmp);
      ++top;
   }
   else
   {
      v[top] = x;
      // solo ora aggiorna lo stato
      ++top;
   }
}
//
// Offre la strong guarantee: se una eccezione č
// lanciata, l'oggetto resta immutato
//
// meglio restituire un const T&
template<class T> T Stack<T>::pop()
{
   if(top == 0) throw std::underflow_error("pop on an empty stack");
   try
   {
      return v[--top];
   }
   catch(...)
   {
      // ripristina lo stato originale
      ++top;
      throw;
   }
}
/*
Non so se questo metodo sia valido, comunque con il BCC 5.5
funziona.
La cosa migliore č perň restituire una const reference, avvvisando
l'utente che una successiva esecuzione di un metodo non const
sullo Stack invaliderebbe la referenza (come del resto per
gli iteratori nei container).
*/
/*---------------------------------------------------------------*/
Come si vede ora il codice č molto piů leggibile.
Inoltre, come commentato, la classe Stack presenta
una strong guarantee rispetto riguardo alle eccezioni.
Questa č la situazione ideale (anche se non sempre
possibile).
Fatto questo ora Stack č una 'scatola chiusa', e puň
essere usata senza problemi (a patto di rispettare
le richieste della classe).
Resta ovviamente la gestione delle eccezioni dal lato
'utente', ma questa č un'altra storia.
Manlio Perillo

 conservare la coerenza del database stesso.
In effetti Herb Sutter, che e' uno dei maggiori esperti di gestione
delle eccezioni e che si occupa pure di database, prevede come garanzia
massima di "buon comportamento" in presenza di eccezioni la
transazionalita', ovvero la capacita' di una classe di riportarsi allo
stato iniziale rispetto alla sequenza di attivita' interrotta
dall'eccezione.
Al di la' dal problema di scalare alle dimensioni di un'intera
applicazione, tutto questo ha senso all'interno di un programma; se
questo interagisce con il mondo esterno, o anche altre applicazioni,
come dicono gli inglesi "all bets are off".
Ciao,
Nicola

}
Anche io quando ho letto gli articoli di Herb Sutter sulla
classe Stack ho pensato a incapsulare return v[--top] in un
try/catch e, per quello che ne so, la cosa dovrebbe essere
funzionante. Riguardo al tornare un const reference pero'
(o anche al tornare un valore) resta un problema intrinseco.
Immagina che il codice utente scriva qualcosa come:
  Stack<MyClass> s;
  ...
  MyClass c = s.pop();
e immagina che questa assegnazione generi una exception.
A questo punto la copia in c non sarebbe stata effettuata
(supponiamo che anche MyClass sia scritta per offrire la
strong guarantee) ma l'elemento sarebbe stato comunque
rimosso dallo stack; in altre parole l'elemento e' perso
nel vuoto e l'utente non ha alcuna possiblita' di scrivere
codice exception-safe se decide di usare la classe Stack.
Anche se formalmente credo sia giusto dire che Stack e'
exception safe chi deve scrivere codice exception-safe
non puo' utilizare la classe. Di fatto e' come se la
classe non fosse affatto exception safe.
Anche tentare di incapsulare lo statement in un
blocco try/catch non porta giovamento.
Secondo me inoltre e' ingenuo classificare questo come
un problema "lato utilizzatore della classe" visto che
in realta' il problema e' essenzialmente dovuto
all'interfaccia della classe stessa (e alla filosofia
delle exception C++).
La soluzione standard proposta (e usata anche dalle
librerie standard) e' quella di aggiungere una funzione
top() che restituisce il valore ma che non altera lo stack
(in modo che una eventuale exception sia gestibile senza
perdere dati) e cambiare pop in modo che rimuova
semplicemente l'elemento senza ritornarlo.


 semplicemente l'elemento senza ritornarlo.
Certo questa è la soluzione migliore, ma io ho cercato di
non stravolgere l'interfaccia dello Stack originale.
Manlio Perillo

 non stravolgere l'interfaccia dello Stack originale.
Purtroppo la filosofia di gestione delle exception in C++
non permette questo. A volte per poter scrivere codice che
sia exception-safe e' *obbligatorio* modificare l'interfaccia
della classe in quanto alcuni approcci (come il pop() che hai
scritto) sono *intrinsecamente* non exception safe (se intendiamo
la strong guarantee).
Andrea
-- 
----------------------------------------------------------------
 <\___/>  Andrea        email: agriff at tin dot it
 / . . \  "6502"          irc: _6502_@EFNET#italia
(  (")  ) Griffini   homepage: 
http://www.netservers.com/~agriff


---------da approfondire?---------<
P.S.
Come detto già in precedenza, sono sempre più convinto che
l'argomento delle eccezioni deve essere trattato in modo
approfondito.
In questo modo si imparano tecniche e mentalità per risolvere
casi via via più complessi
(
 ammesso e non concesso che la complessità della gestione
 delle eccezioni cresca con la complessità del programma.
 Potrebbe essere possibile, usando tecniche di base,
 'arrestare' la complessità o 'localizzarla'.
 Mi sto riferendo al caso:
     registry[name]=getvalue();
).
Manlio Perillo

L'argomento è *molto* interessante e spero che insieme a quello sulla covarianza/controvarianza finisca in un documento del newsgroup.
Detto questo, provo a dare un contributo alla discussione tentando di chiarire i contorni del problema.
------------
Il problema del exception safety ha due aspetti:
1) l'exception safety di un oggetto o di un metodo dell'oggetto;
2) l'exception safety di una applicazione o di un livello di astrazione della applicazione o di un sottosistema della applicazione.
Benchè in pratica i punti 1 e 2 debbano essere realizzati in armonia l'uno con l'altro, sono due problemi ortogonali, indipendenti l'uno dall'altro.
A differenza di come si potrebbe essere tentati dal credere non si può ottenere il punto 2 applicando il punto 1 a tutti gli oggetti di una applicazione (o di un livello di astrazione o di un sottosistema).
- Per quanto riguarda l'exception safety di metodo di un oggetto, questo può essere definito come la proprietà del metodo di far passare l'oggetto da uno stato coerente ad un altro stato coerente mediante transazioni che siano a.c.i.d. e serializzabili e che questo avvenga anche nel caso in cui sia lanciata un'eccezione. Questa definizione tramite transazioni è un modo per descrivere il problema con solide basi teoriche ed allo stesso tempo ne propone una tecnica di implementazione.
L'implementazione potrebbe anche essere facile (applicando il pattern del memento/token per es.), la difficoltà stà nel trovare modi di implementazione semplici ed efficenti.
Questo è l'aspetto del problema attinente alla programmazione C++.
E' facile dimostrare che un metodo per essere exception-safe non è necessario che utilizzi solo operazioni/metodi che siano a loro volta exception-safe; e non è nemmeno sufficente.
Quindi non è necessario che tutti i metodi di un oggetto/libreria siano exception-safe. 
Anzi, nella pratica accade più spesso che le eccezioni generate da un metodo siano di tre tipi:
  a) Eccezioni generate internamente dal metodo (a causa di un errore interno o a causa di una eccezione lanciata da un oggetto utilizzato internamente, che è stata intercettata e contestualizzata) che lasciano l'oggetto in uno stato valido. Quindi chi intercetta queste eccezioni potrà risolverne la causa e riutilizzare l'oggetto/metodo. 
  b) Eccezioni generate internamente dal metodo ma che sono "gravi" e lasciano l'oggetto in uno stato non valido. Quindi chi intercetta queste eccezioni non dovrà riutilizzare l'oggetto/metodo bensì distruggerlo.
  c) Eccezioni _impreviste_ che vengono lanciate da oggetti/operazioni utilizzate internamente dal metodo e che quindi non garantiscono che l'oggetto si trovi ancora in uno stato valido.
Anche in questo caso chi intercetta queste eccezioni non dovrà riutilizzare l'oggetto/metodo bensì distruggerlo.
Questa divisione deve essere chiaramente documentata o meglio esplicitata facendo derivare le eccezioni di tipo a), b) e c) da opportune eccezioni astratte che sottolineano il dato (per es. recoverable_exception per a, fatal_exception per b e dichiarando std::bad_exception tra le eccezioni del metodo in modo che tutte le eccezioni al punto c vengano trasformate in std::bad_exception).
Ne consegue che il numero dei casi in cui è necessario occuparsi della exception safety di un metodo si riducono notevolmente.
A questo punto, noto che lo scope della transazione è l'esecuzione del metodo, resta da determinare la granularità con cui considerare le operazioni elementari facenti parte della transazione e che quindi dovrebbero essere invertibili (in caso di fallimento della transazione) e idempotenti (per riapplicare un passo quando è fallito e dopo averne corretto le cause deve essere riapplicato per procedere nella transazione).
La granularità potrebbe essere una singola istruzione C++, un sequence point oppure un numero di operazioni che abbiano un significato applicativo. Quale scegliere? Perchè?
- Per quanto riguarda l'exception safety di una applicazione o di un livello di astrazione o di un sottosistema, anche in questo caso si può utilizzare il concetto di transazione. Ma lo stato in questione è quello di tutta l'applicazione, del livello di astrazione o del sottosistema (per esempio del sottosistema di persistenza).
Lo scope della transazione è l'esecuzione di una singola operazione esposta dalla applicazione,  dal livello di astrazione o da un sottosistema.
Anche qui è facile dimostrare che per ottenere l'exception safety di una applicazione o di un livello di astrazione o di un sottosistema non è necessario ne sufficente che tutti gli oggetti utilizzati siano exception-safe.
------------
In conclusione i contorni del problema sono questi:
- trovare implementazioni C++ semplici ed efficenti (della transazionalità) di un _singolo_ metodo exception-safe; individuare la granularità più conveniente a cui lavorare (la singola istruzione C++, un sequence point oppure un numero di operazioni che abbiano un significato applicativo?);
- trovare una politica di gestione delle eccezioni da applicare coerententemente all'intera applicazione C++ per discriminare le eccezioni che compromettono lo stato (equivalentemente l'invariante) dell'oggetto coinvolto e quelle che no.
HTH
_______________________________________
 Luca Minudel   Consulente Informatico
 
luca.m...@nline.it
 Conegliano (TV)
 WWW    
http://www.nline.it/consulting

Hai ragione!
Ragazzi, vi faccio una preghiera : cercate di condensare i messaggi e
soprattutto le risposte ai messaggi. Non so se l'avete notato, ma
questo thread ha un numero di linee per messaggio esorbitante.

 l'invariante) dell'oggetto coinvolto e quelle che no.
Sulla questione dell'invariante ho dei dubbi.
Considera un oggetto formato da una sequenza di 'numeri'
ed il cui invariante sia che i numeri sono ordinati.
Se una eccezione compromette questo inviariante (i numeri
non sono più ordinati), si può dire che l'oggetto non
è più in uno stato valido?
Il punto è che si può ancora accedere all'oggetto in questione,
quindi si potrebbe (in principio) ripristinare l'invariante.
Oppure l'oggetto, come dici tu, deve essere distrutto?
Comunque debbo dire che il concetto di invariante non mi
è ancora completamente chiaro, anche questo concetto
andrebbe approfondito.
Manlio Perillo

).
In ambiti diversi dal C++ la cosa e' dominabile; in particolare
una volta che sono disponibili le transazioni nidificate la
complessita' non aumenta (almeno teoricamente) con la composizione
ovvero combinare oggetti semplici o complessi e' equivalente.
Le exception C++ non offrono questa interessante proprieta' in
quanto si occupano solo del problema base (non cadere
nell'undefined behaviour).
Quello che non so e' se utilizzare questa logica che rende
la gestione degli errori meno evidente sia un passo verso
la semplificazione della scrittura del software complesso
oppure no. Nella versione finale dello stack proposta da Herb
Sutter non esiste nemmeno un blocco try/catch. Il fatto che la
classe sia stata pensata exception safe e' in alcuni punti
assolutamente trasparente per chi osserva il codice.
Il problema e' che altro codice che a prima vista sembrerebbe
equivalente risulterebbe invece in un disastro (vedi il portare
la copia degli elementi nel costruttore di copia di Stack_base
nel codice che hai scritto tu).
La gestione per return code ha sicuramente degli svantaggi
ma e' anche piu' esplicita e visibile. Visto che il valore
aggiunto dalle exception e' limitato (mancando le transazioni
in C++) e' giustificato il loro uso al posto dei return code ?
In altre parole quanto realmente semplificano la vita per
un programmatore o un team di programmatori nei casi reali ?
Non ho esperienze di codice complesso che abbia usato le
exception come sistema di segnalazione errori.
In un progetto che sto realizzando uso le exception ma in un
ambito particolare in cui mi sono potuto dotare di transazioni.
Andrea


questo thread ha un numero di linee per messaggio esorbitante.
Ouch... incasso.
Non e' facile trovare il giusto equilibrio fra l'essere
troppo stringati (con rischio di fraintendimenti) e l'essere
troppo prolissi (con il rischio di far addormentare chi legge).
Io so ad esempio di ricadere nella particolare fascia dei
"prolissi poco chiari" i cui testi sono cioe' spesso fonte
di fraintendimenti :)
Comunque non mi sembra chi i messaggi fossero citazioni di
lunghi testi altrui seguiti da un semplice "concordo"; il
quoting e' stato usato per discussioni puntuali.
Andrea


quoting e' stato usato per discussioni puntuali.
Non da tutti.
Mi fermo qui :)

 Anche per illustrare questo aspetto occorrerebbe un esempio.
L'esempio segue la stessa logica di quello sopra riportato per il metodo di un oggetto.
Sono riuscito a spiegarmi?



E come ti stai trovando?
Manlio Perillo

 con Deja.
Si potrebbe provare a raccoglierle in un libro; chissa' se qualcuno le
pubblicherebbe... Avrei anche un titolo:
"Tutto quello che avreste voluto sapere sul C++ (e avete osato
chiedere)"
Ciao,
Nicola


Personalmente credo che un messaggio debba essere il più possibile
autocontenuto.
Se si eliminassero i riferimenti a ciò a cui si stà rispondendo
chi legge rischia di non capire.
Sul punto di condensare le risposte è facile a dirsi.
Probabilmente si dovrebbe fare un poco più di ordine,
ad esempio rileggendo i messaggi mi sono accorto che
un certo concetto è ripetuto più volte;
resta il fatto che però ogni volta viene approfondito
(o almeno spero).
Comunque se io personalmente sto causando problemi
direttamente o indirettamente, me ne scuso.
Manlio Perillo


Dal contesto del messaggio non sono riuscito a capire se stai
scherzando o se dici sul serio.
Manlio Perillo

 Sono riuscito a spiegarmi?
Credo proprio di si.
Anche se su queste cose bisogna essere cauti.
Uno potrebbe credere di aver capito mentre poi non ha capito
quasi niente.
P.S.
Perchè quando rispondo ad un tuo messaggio il tuo testo
non è preceduto da >?
Usi qualche formato particolare?
Poi vorrei chiederti un piacere.
Di quante linee è il tuo rientro del testo?
A me è di 70 (perchè richiesto da un newsgroup che frequento
saltuariamente).
E' facile capire che se ognuno usa un rientro diverso la cosa è
problematica.
Credo ci si dovrebbe mettere d'accordo una volta per tutte.
Manlio Perillo

 o il ripristinarlo sia molto espansivo.
                                   ^
                        Costoso, Manlio, costoso.
Ciao,
Nicola

In realta' sono serissimo. In origine avevo pensato di raccogliere i
contributi di Alex Martelli, che spesso avevano gia' quasi la forma di
capitoli di libri. Il problema, ovviamente, e' il tempo (piu' il trovare
l'editore, cosa che potrebbe rendere il progetto piu' stuzzicante per il
portafoglio e soprattutto per l'orgoglio)
Ciao,
Nicola

 (o almeno spero).
Trovo che l'importante sia che la densita' semantica sia adeguata: se
chi scrive molto scrive cose interessanti, ben venga.
Ciao,
Nicola

 In realta' sono serissimo.
Una cosa che si può fare è semplicemente salvare i singoli messaggi
più interessanti e archiviarli da qualche parte.
Poi qualcuno di buona volontà può ordinarli, sistemarli
o altro.
Manlio Perillo

 che sia stato "vittima" di una eccezione "unsafe".
Dimenticavo una cosa importante.
Alla luce di quanto hai scritto cosa intendi per
exception-safe?
a) Anche se vengono lanciate eccezioni l'oggetto mantiene
   il suo invariante (stato 'logico' valido).
b) Anche se vengono lanciate eccezioni l'oggetto rimane
   valido nel senso che vi si può accedere
   (stato 'fisico' valido).
Ovviamente a) include b).
Manlio Perillo

...


Credo ci si dovrebbe mettere d'accordo una volta per tutte.
Se esiste un formato preferenziale per i news sarei contento di conoscerlo ed
adattarmi.
Ciao.


 Intendo il caso a).
Ora mi č tutto chiaro.
Il fatto č che non capivo come si poteva distruggere o ripristinare
un oggetto che non fosse piů in uno stato 'fisico' valido :-).
Comunque mi sembra che lo Stroustrup per exception-safe intenda
il caso b).
Manlio Perillo


Il problema è che quando tento di rispondere ad un tuo messaggio
il tuo testo non è preceduto da '>';
inoltre se non aggiusto i rientri il testo diventa scomodo
da leggere quando lo invio.
Manlio Perillo


Mi sembra logico che sia così, ma ci devo pensare meglio.
Comunque ho un'altro dubbio.
Negli esempi forniti dallo Stroustrup la situazione di cui sopra
non avviene mai.
Quello che succede è che un certo metodo (ad esempio insert) di
un container è implementato in questo modo:
try
{
    // operazioni per portare l'oggetto dallo stato iniziale
    // a quallo finale
    // ...
}
catch(...)
{
    // operazioni per riportare l'oggetto nelle condizioni iniziali
    // ...
    throw;
}
A questo punto se nel blocco catch viene lanciata una eccezione
(perchè ad esempio il costruttore di copie o l'operatore di
 assegnamento del tipo di elemento lanciano una eccezione)
 il metodo non offre la strong guarantee ma solo la basic.
Nota:
    L'uso del try-catch si può evitare (a volte?) usando la
    tecnica della "resource aquisition is initialization"
Quindi l'invariante è comunque mantenuto sempre.
A questo punto mi chiedo che approccio si deve avere se
le operazioni invalidano temporaneamente l'invariante
(come nel tuo esempio di Unsafe).
Ecco una idea:
CUnsafe::f()
{                
    try
    {
        // qui Inv(S) è True
        CSafeA SafeA;
        S = SafeA.g(S); 
        // qui Inv(S) è False
        CSafeB SafeB;
        S = SafeB.h(S);
        // qui Inv(S) è nuovamente True
    }
    catch(...)
    {
        try
        {
            // 
tenta di ripristinare l'invariante
            // ...
        }
        catch(...)
        {
            // non ci è riuscito:
            throw invariant_failure();
        }
        // c'è riuscito:
        throw;
    }
}
Ma non mi sembra molto elegante.
Quindi mi chiedo:
1) Questo approccio può essere necessario?
2) Si può evitare l'uso di try-catch con la tecnica
   della "resource aquisition is initialization"?
3) Se si deve anche fornire la strong guarantee non è
   che le cose cominciano a complicarsi?
Comunque c'è una tecnica che credo si possa
ritenere molto comoda ma che presenta problemi
di efficienza (niente è gratis):
CUnsafe::f()
{                
    // costruisce una *copia* temporanea di this
    CUnsafe tmp(*this);     
 
    // qui Inv(tmp.S) è True
    CSafeA SafeA;
    tmp.S = SafeA.g(tmp.S); 
    // qui Inv(tmp.S) è False
    CSafeB SafeB;
    tmp.S = SafeB.h(tmp.S);
    // qui Inv(tmp.S) è nuovamente True
    // se arriva qui tutto è andato bene
    swap(*this, tmp);
}
Ovviamente è richiesto che swap non lanci eccezioni
(Questo è sempre possibile?
 Ad esempio per lo swap di map è richiesto che la copia
 del 'comparison' non lanci eccezioni).
 
In questo modo, non solo f() è exception-safe ma offre
anche la strong-guarantee.
Potrebbe essere quindi una soluzione per gestire
delle situazioni critiche.    
Manlio Perillo

 Forse intendevi SafeB.h?
Esatto.
Ciao


 
Concordo.
Ciao


 Mi pare un'ottima tecnica.
Potreste rapidamente ricordarmi di cosa si tratta?
EMG

 un numero limitato di eccezioni che possono verificarsi spesso.
Tutto questo non fa che rafforzare la mia convinzione che le eccezioni
richiedano e portino ad una nuova mentalità.
Infatti ora gli errori entrano a far parte di una interfaccia:
void f();   significa che f() 'può' lanciare eccezioni
ed in più ci sono le specifiche di throw.
Tutto questo 'forza' il programmatore a tener conto degli errori
sin dal progetto di un programma.
Inoltre il programmatore è tenuto (o dovrebbe) ad indicare le
richieste e le garanzie offerte da ogni modulo/oggetto/funzione.
Che cosa ne pensate?
Manlio Perillo

 ha scritto nel
messaggio news:39F7ED3F...@flashnet.it...

Se hai una risorsa che devi acquisire e rilasciare quando
non serve più la cosa migliore e usare una classe che
implementa tale paradigma (acquisizione - rilascio) tramite
il costruttore ed il distruttore.
In tale modo anche se vengono lanciate eccezioni la risorsa
viene rilasciata perchè viene chiamato il distruttore dell'
oggetto che la gestisce.
Inoltre per l'accesso alla risorsa si può fare affidamento
sull'invariante della classe (ossia l'oggetto viene creato
solo se la risorsa può essere acquisita e per tutta la durata
dell'oggetto la risorsa è 'valida').
Manlio Perillo

 il costruttore ed il distruttore.
Chiarissimo. Molte grazie.
EMG

Aggiungerei che probabilmente l'esplorazione dei problemi legati alla
gestione delle eccezioni provochera' una tendenza alla proliferazione di
micro-classi RAII (una classe, un compito, un attributo, o poco piu')
per sfruttare al massimo il meccanismo di svolgimento dello stack.
Ciao,
Nicola

 ha scritto nel messaggio
news:39F93887...@divalsim.it...

E lo vedi come un bene o come un male?
La tecnica non introduce nessun overhead ed anzi aiuta a localizzare
una risorsa.
E' chiaro comunque che tale tecnica non deve essere fine a se stessa.
Uno potrebbe implementare micro classi RAII (a proposito, che
significa?)
solo per uso interno a classi/moduli più grandi.
Manlio Perillo

 solo per uso interno a classi/moduli più grandi.
"Resource Acquisition is initialization" un termine sfortunato ma
popolare, coniato (credo) da B.S. stesso. Qualche mese fa e' stato
lanciato un tentativo di trovare una denominazione migliore su
c.l.c++.m, ma senza esito.
Ciao,
Nicola

 serve a ripararsi dalle eccezioni stesse.
non dimenticare che le eccezioni mappano gli errori.
Sono gli errori che costringono a scrivere codice
'eccezionale' per ripararsi da essi.
Manlio Perillo

...


 Che cosa ne pensate?
personalmente l'esempio di Java č molto illuminante, su come
potrebbero essere gestite le eccezioni:
void f();    // non lancia eccezioni
void f() throw int    // lancia eccezioni di tipo int
void g()
{
    f();
}
    //    errore, g() non lancia eccezioni, dovrebbe essere cosě:
void g() throw int
{
    f();
}

 void f();    // non lancia eccezioni
Peccato che la cosa sia improponibile in C++ perchè sconvolgerebbe
tutto.
E poi, come detto, si può semplicemente scrivere
void f() throw();
Per curiosità, come si fa ad indicare in Java che una funzione
può lanciare qualsiasi eccezione?
Manlio Perillo

L'auto_ptr mi sembra l'esempio *perfetto* di micro-classe RAII
Ciao,
Nicola

 'eccezionale' per ripararsi da essi.
Certo, ma ho la sensazione che in molti casi la buona vecchia
programmazione strutturata coi codici di errore conme valori di ritorno
risulti piu' chiara e piu' pulita.
Ciao,
Nicola

Il problema delle specifiche di throw e' che il default e' "puo'
lanciare qualsiasi eccezione" e che i compilatori non sono tenuti a
controllarne il rispetto staticamente. Di fatto, a differenza di quelle
di Java, servono a poco (e su alcuni compilatori peggiorano le
prestazioni)
Ciao,
Nicola

 risulti piu' chiara e piu' pulita.
Secondo me c'è spazio per entrambe le tecniche (codici di errore e
exceptions). Infatti alcune situazioni si prestano naturalmente ad
essere risolte con i codici di errore ma per altre può risultare
praticamente impossibile.
EMG


Hai perfettamente ragione, ricordati però che stai parlando di
programmazione strutturata.
Il C++ aggiunge la programmazione modulare ed a oggetti, e qui
i codici di errori come valori di ritorno sono impraticabili.
Leggi l'altro mio post.
Manlio Perillo


Per chiarire una volta per tutte quello che penso, supponiamo
di aver mandato a quel paese le eccezioni.
Come dovremmo scrivere la classe Stack
(come proposta nell'articolo di Tom Cargill)?
Il primo problema è come riportare gli errori avvenuti
all'interno di una classe.
L'unica soluzione che vedo è quella di aggiungere un
metodo:
    bool failed() (come per iostream).
Siccome ci farebbe piacere sapere quale errore è occorso
dovremmo aggiungere un'altro metodo per saperlo:
    error_type what() const.
Ovviamente questa notazione *deve* essere usata da tutte
le classi usate.
Ecco come scriverei il costruttore:
template<class T> Stack<T>::Stack()
{
    top = -1;
    v = new T[nelems = 10];
    if(v == 0)
    {
        failed_ = true;
        error = out_of_memory;
    }
}
template<class T> Stack<T>::(const Stack<T>& s)
{
    v = new T[nelems = s.nelems];
    if(v != 0)
    {
        if(s.top > -1)
        {
            for(top = 0; top <= s.top; top++)
            {
                v[top] = s.v[top];
                if(v[top].failed())
                {
                    failed_ = true;
                    error = v[top].what();
                    break;
                }
            }
            top--;
        }
    else
    {
        failed_ = true;
        error = out_of_memory;
    }
}
// ed ora pop:
template<class T> T Stack<T>::pop()
{
    if(top < 0) return ????;
    else return v[top---];
}
Non scrivo le altre funzioni perchè è una faticaccia.
Come si vede aver mandato a quel paese le eccezioni non significa
che abbiamo mandato a quel paese gli errori e la loro gestione.
E' proprio questo il punto che vorrei evidenziare.
Ecco ora un esempio di uso di pop:
Stack<myClass> s;
// ...
myClass k = s.pop();
// il fatto che non ci siano le eccezioni non elimina il
// fatto che il costruttore di myClass possa fallire:
if(k.failed()) ?????
Quindi si ripresentano gli *STESSI* problemi che si avevano
con le eccezioni.
*Anzi*, senza le eccezioni le cose si sono complicate molto,
ogni accesso ad una classe deve essere controllato, inoltre
insieme alle eccezioni abbiamo mandato mandato a quel paese
anche il concetto di invariante (il che significa che ogni
metodo dovrebbe testare lo stato di this prima di procedere,
con un evidente aumento di overhead).
Si presentano anche i problemi con i distruttori...
Pensateci sopra e fatemi sapere...
Manlio Perillo

 Pensateci sopra e fatemi sapere...
Il mio intervento e' provocatorio, non voglio negare l'utilita' delle
eccezioni, ne il vantaggio di single phase construction/destruction
rispetto all'uso di funzioni di inizializzazione esplicita. Da qui a
dire che le eccezioni risolvono problemi per i quali non c'erano
soluzioni altrettanto valide il passo mi pare lungo: il vero problema
delle eccezioni e' che ormai non si possono (o quasi) non usare!
Resto della mia idea che le eccezioni sono state introdotte
prevalentemente perche' chi programmava in modo non strutturato non
sapeva come altro fare per non duplicare il codice di clean-up (questo
messaggio e' gia' troppo lungo: se volete un esempio, ve lo scrivo in un
follow-up).
Ciao,
Nicola


Non lo vedo come un *problema*.
Se hai letto l'altro mio post, anche in C i puntatori non
si possono (o quasi) non usare.
Manlio Perillo

 Non lo vedo come un *problema*.
Per un linguaggio di cui si vanta l'usabilita' secondo molteplici
paradigmi secondo me lo e'.
Ciao,
Nicola


Ancora una volta, considera le alternative:
(supponi anche che non ci siano le eccezioni)
int f()
{
    char* v = new char[SIZE];
    // ...
    if(err_cond_1)
    {
        delete[] v;
        return err_1;
    }
    // ...
    if(err_cond_n)
    {
        delete[] v;
        return err_n;
    }
    delete[] v;
    return ok;
}
Mi sono limitato ad un caso lineare.
Come si vede , anche senza eccezioni l'uso di auto_ptr o simili
aiuta a scrivere meno codice (e quindi ad evitare di sbagliare).
Il concetto č che nello *scope* di f acquisiamo delle risorse
che devono essere rilasciate alla fine dello scope stesso.
Con le eccezioni attivate f() non cambia molto, solo che la cosa
č piů 'leggibile' (sono comunque pareri personali)
void f()
{
    char* v = new char[SIZE];
    try
    {
        // ...
    }
    catch(...)
    {
        delete[] v;
        throw;
    }
}
Manlio Perillo

Guarda che non intendevo dire che auto_ptr sia inutile, ne che serva
solo in presenza di eccezioni. Pero' e' *proprio* una micro-classe RAII:
ingloba una singola funzione atomica, indipendente dal dominio
applicativo.
Ciao,
Nicola

 paradigmi secondo me lo e'.
Secondo me sbagli a pensare una cosa del genere.
Io posso comunque usare il C++ in stile C.
In questo caso per gli errori ci sono i return-code.
Resta il fatto che se comincio ad usare delle classi non banali
il modo migliore per riportare gli errori sono le eccezioni.
Non è detto che le due cose non possano convivere.
Manlio Perillo

 può lanciare qualsiasi eccezione?
void f() throws Exception;
se l'eccezione deriva da Exception (come avviene per tutte le
eccezioni della libreria di base). Se poi vuoi andare sul sicuro
void f() throws Object;
visto che Object è la classe base di *tutte* le classi Java (anche
quelle definite dal programmatore).

 Non è detto che le due cose non possano convivere.
Non per ripetermi, ma c'e' qualcosa nella libreria standard di meno
banale degli stream? :-)
Ciao,
Nicola

 migliore del c++.
Perche' hanno potuto permettersi di ripartire da zero su molte cose, non
dovendo garantire compatibilita'. E secondo me non l'hanno fatto
abbastanza, ma questa e' un'altra storia.
 
Ciao,
Nicola

 ha scritto nel messaggio
news:39FDA768...@divalsim.it...

Il fatto è che le micro classi RAII costituiscono le fondamenta
per costruire classi via via più complesse.
Considera l'esempio di Stack e delle semplificazioni portarte
da Stack_base.
Se una classe deve acquisire più risorse (come file, lock, thread,
memoria), l'uso di micro classi semplifica molto la gestione del
codice.
Manlio Perillo


Il locale (che purtroppo mi sembra essere 'sconosciuto' in questo
newsgroup).
Comunque gli stream sono *molto* complessi.
Solo che la complessità è 'nascosta' all'utente.
Manlio Perillo


Questa cosa non mi sembra molto *bella*.
Comunque in C++ si può adottare benissimo questo approccio.
Manlio Perillo

 newsgroup).
E' che gia sono in pochi a porsi questo tipo di problema in generale, in
piu' chi lavora con framework GUI finisce per essere costretto a usare i
meccanismi specifici del framework. Non che per gli stream la situazione
sia molto diversa, a dire il vero.
Ciao,
Nicola


Questa, in effetti, è una cosa che mi rattrista molto.
La libreria Standard è pensata per essere estensibile.
Se che progetta i framework conoscesse a fondo il C++,
probabilmente potrebbe usare molto della libreria Standard.
Manlio Perillo

 probabilmente potrebbe usare molto della libreria Standard.
Il problema e' che i framework piu' diffusi sono nati prima che lo
standard venisse formalizzato e, se e' vero che e' difficile
implementare un compilatore inseguendo uno standard in evoluzione, e'
ancora piu' vero che pensare di basarci sopra l'architettura di una
libreria complessa e' un suicidio. Bisognera' aspettare qualche anno.
Ciao,
Nicola


Sono abbastanza pessimista su questo punto...
Manlio Perillo

 anche altre voci.
Hai ragione. Purtroppo io personalmente non ho molto da dire
sull'argomento per mancanza di esperienza concreta.
EMG

 ha scritto nel
messaggio news:3A000044...@flashnet.it...

Se è per questo nemmeno io ho esperienza concreta :-).
Ovviamente intendendo per esperienza concreta l'aver scritto
un grosso sistema usando le eccezioni.
Le mie idee sono derivate dalla lettura dello Stroustrup e da
'intuito' personale.
Inoltre ho una certa 'esperienza' di codici C (perchè mi piace
andare a leggere i sorgenti dei programmi su cui riesco a mettere
mano, e purtroppo i programmi GNU sono *quasi* tutti scritti in C)
e ne constato le limitazioni oltre a farmi una idea di come
implementarei io la cosa in C++.
Anche quei pochi sorgenti di programmi C++ che ho letto non mi
soddisfano, anzi credo siano non molto corretti.
Manlio Perillo

 Comunque in C++ si può adottare benissimo questo approccio.
non ho capito né cosa e perchè non ti sembra molto *bella*, né di
quale approccio parli! se mi spiegassi... :)

 abbastanza, ma questa e' un'altra storia.
anche secondo me, ma _questa_ l'hanno fatta bene


Mi riferivo al fatto che ogni classe è derivata da una classe base
comune Object.
Manlio Perillo

 'intuito' personale.
Non voglio assolutamente sminuire la tua gią notevole preparazione ma
secondo me le idee si formano e chiariscono molto solo dopo aver
"combattuto" per anni con un certo linguaggio per *lavoro*. Prima di
questa esperienza manca quella consapevolezza necessaria e quel "sesto
senso" che si acquista solo dopo aver affrontato almeno centinaia di
casi *reali* diversi. Lo studio su "esempi giocattolo" serve moltissimo
a chiarire i concetti base del linguaggio ma non puņ in alcun modo
portare all'esperienza come la intendo io.
EMG

 (Non sono stato molto chiaro ma spero di aver reso l'idea)
No, sono due problemi strettamente collegati, ma secondo me in teoria
dovrebbe andare cosi':
1) Si identifica il problema
2) Si delinea a grandi linee una soluzione
3) Si scelgono gli strumenti piu' adatti a realizzarla
4) Si adatta la soluzione dettagliata a quello che gli strumenti fanno
meglio
Di architettura cominciamo quindi a parlare al punto 2, di linguaggio al
punto 3, anche se in seguito l'uno influenza l'altra.
In pratica (e in molti libri) si tende a dimenticare troppo spesso che
un'applicazione e' qualcosa di piu' che un programma e che anche il
"resto" conviene essere impostato il prima possibile e non a posteriori.
 
Ciao,
Nicola


Sono pienamente d'accordo con quello che dici
(ed è una cosa che anche io ho sottolineato).
Il problema è che se uno non ha una conoscenza 'globale' delle
eccezioni, non comincerà mai ad usarle "come si deve" in programmi
'reali'.
Quindi non potrà mai esprimere con imparzialità e consapevolezza
l'impatto delle eccezioni in un "real system".
Il C++ è un linguaggio molto vasto e versatile, probabilmente
sbaglia chi dice che per usarlo 'bene' non è necessario conoscerlo
tutto.
P.S.
Qualcuno sa se esiste un sistema non banale che usa le eccezioni
'correttamente'?
Manlio Perillo

 tutto.
Sembra anche a me. E' comunque quel 'bene' che può essere interpretato
in vari modi.
EMG

 comune Object.
ho capito. Conta che in Java tutti i metodi delle classi sono
"virtual" (per come lo intende uno che conosce il c++), e il suo
obiettivo non è certo l'efficienza. Se tieni conto di queste cose,
l'uso di Object ti torna! avevo anche cercato di costruire una classe
del genere da usare in C++, ma non sono riuscito a farla come volevo
io.


Come la volevi?
Sempre a proposito di eccezioni, volevo ricordarti che in C++
è anche possibile fare:
    int e;
    throw e;
In Java è possibile?
I tipi builtin (int, float) derivano anch'essi da Object?
Manlio Perillo

 I tipi builtin (int, float) derivano anch'essi da Object?
no, ma esiste, ad esempio, la classe Integer che è possibile costruire
a partire da un int. Va comunque sottolineato il fatto che in Java non
esiste la conversione implicita. Ad esempio, nella classe Integer
esiste il costruttore dichiarato come
Integer( int);
ma non significa che, come accadrebbe in c++, il codice
Integer a = 10;
sia corretto. Bisogna proprio fare
Integer a = new Integer( 10);


E' una cosa abbastanza comune nei sistemi ad ereditarieta' singola. In
particolare Object e' l'aggancio dei meccanismi di riflessione e
serializzazione.
Ciao,
Nicola


Ci sono un poco di cose non adatte per il C++.
Per prima cosa, per fare quello che vuoi si può usare il polimorfismo.
#include <vector>
class Object
{
   // per essere polimorfico deve avere funzioni virtuali
   virtual void foo() = 0;
};
template<class T> class JavaVector: public std::vector<T>,
                                    public Object
{
    virtual void foo() { }
    // l'unica scocciatura è il dover ridefinire i costruttori,
    // peccato non si possa fare:
    // using std::vector<T>::vector;
    //   | usa i costruttori di std::vector<T>
    // meglio ancora una cosa del genere:
    // implicit JavaVector(const std::vector<T>& v)
    // : std::vector<T>(v) {  }
    //
    // implicit dovrebbe significare che posso fare, ad esempio:
    // int a[3] = {1, 2, 3};
    // JavaVector<int> v(a, a + 3);
    // al posto di
    // JavaVector<int> v(std::vector<int>(a, a + 3));
    // *credo sia una cosa fattibile ed utile*
};
int main()
{
   JavaVector<int> a;
   // per il polimorfismo si devono usare puntatori o
   // referenze
   Object& obj = a;
   // lancia bad_cast
   JavaVector<double>& b = dynamic_cast< JavaVector<double>& >(obj);
}
Come vedi la cosa è fattibile, tuttavia le classi standard del
C++ non sono derivate da un'unica classe come in Java, quindi
serve un workaround.
Resta poi da decidere se Object debba avere funzioni virtuali che
abbiano senso.
Se ti scoccia ripetere tutto questo per ogni classe C++, basta
usare i template:
template<class CppClass> JavaClass: public CppClass,
                                    public Object
{
    // ...
};
Anche a questo proposito sarebbe utile una comoda nuova feature:
i template typedef.
template<class T>
typedef JavaClass< std::vector<T> > JavaVector<T>;
template<class charT>
typedef JavaClass< std::string<charT> > JavaString<charT>;
....
(sinceramente non capisco perchè non facciano parte
 dello Standard)
Comunque, volendo, si può fare di più.
Poichè il codice è un poco lungo, lo posto in un'altro
messaggio.
Manlio Perillo

ti faccio vedere veramente come la intendevo, visto che oggi pome sono
riuscito nell'intento:
#include <typeinfo>
#include <iostream>
class Obj
{
public:
 virtual const std::type_info &get_typeid() = 0;
 virtual ~Obj() {};
};
template< typename T> class RealObj: protected Obj
{
 friend class Object;
 T _parm;
protected:
 RealObj( const T &parm): _parm( parm) {}
 operator T()
 { return _parm;}
 const std::type_info &get_typeid()
 { return typeid( _parm);}
};
class Object
{
 Obj *_parm;
public:
 template< typename T> Object( const T &parm)
 { _parm = new RealObj< T>( parm);}
 template< typename T> operator T()
 { return dynamic_cast< RealObj< T> &>( *_parm);}
 const std::type_info &get_typeid()
 { return _parm->get_typeid();}
 ~Object()
 { delete _parm;}
};
int main()
{
 int a = 5;
 Object obj( a);
 std::cout << obj.get_typeid().name() << '\n';
 try
 {
  int i = obj // non solleva eccezioni
  double d = ( int)obj // neanche questa
  double d = obj // questa sě
 }
 catch ( std::bad_cast &exc)
 { return 1;}
 return 0;
}
purtroppo nel BCC55 non riesco a capire che eccezioni sollevi un
dynamic_cast fallito. Inutile dire che su VC++ 6 questo codice non
funziona (non č che non compili, non funziona proprio). Fammi sapere
cosa ne pensi


Mi sembra interessante, la soluzione che avevo trovato
io č piů elaborata.
Comunque con questo esempio, in tuo programma va in crash...
int tmp_a[5] = {1, 2, 3, 4, 5};
std::vector<int> a( std::vector<int>(tmp_a, tmp_a + 5) );
Object obj(a);
std::vector<int> b = obj;
try
{
  std::vector<double> c;
  obj = c;
}
catch(...)
{
  std::cerr << "bad_cast...\n";
}
Con la mia soluzione viene lanciata bad_cast.
Manlio Perillo

 riuscito nell'intento:
Ecco cosa ho immaginato io:
#include <vector>
#include <typeinfo>
#include <iostream>
class Object
{
   public:
      virtual Object& operator=(Object&) = 0;
      virtual Object& operator*() = 0;
      virtual Object* operator->() = 0;
};
//
// Esempio completo.
//
template<class T> class JavaVector: public std::vector<T>,
                            public Object
{
    private:
      // 'trucco'.
      JavaVector* this_;
      bool self;
    public:
      //
      // Costruttori per std::vector<T>
      //
      JavaVector(const std::vector<T>& v)
      : std::vector<T>(v), this_(0), self(true)
      {
         this_ = new JavaVector(*this);
      }
      JavaVector& operator=(const std::vector<T>& v)
      {
         // -- un poco di exception-safety --
         // aggiorna std::vector
         std::vector<T>::operator=(*v.this_);
         JavaVector* this_tmp = new JavaVector(*this);
         if(self)
         {
            delete this_;
            self = true;
         }
         this_ = this_tmp;
         return *this;
      }
      //
      // il costruttore di copie ed l'operatore di assegnamento
      // di default non vanno bene
      //
      JavaVector(const JavaVector& other)
      : this_(other.this_), self(false)
      {  }
      JavaVector& operator=(const JavaVector& other)
      {
         if(self)
         {
            delete this_;
            self = false;
         }
         this_ = other.self;
         return *this;
      }
      //
      // Costruttori per Object
      //
      JavaVector(Object& obj)
      : this_(dynamic_cast<JavaVector*>(&obj)), self(false)
      {
         if(!this_) throw std::bad_cast();
         // aggiorna std::vector
         std::vector<T>::operator=(*this_);
      }
      // sfrutta la covarianza sul return value
      virtual JavaVector& operator=(Object& obj)
      {
         // -- un poco di exception-safety --
         JavaVector* this_tmp =  dynamic_cast<JavaVector*>(&obj);
         if(!this_tmp) throw std::bad_cast();
         if(self)
         {
            delete this_;
            self = false;
         }
         // aggiorna std::vector
         std::vector<T>::operator=(*this_tmp);
         this_ = this_tmp;
         return *this;
      }
      // volendo si possono mettere anche i costruttori
      // per Object*. Sono identici.
      virtual JavaVector& operator*() { return *this_; }
      virtual JavaVector* operator->() { return this_; }
      ~JavaVector() { if(self) delete this_; }
};
template<class T>
std::ostream& operator<<(std::ostream& os, const std::vector<T> v)
{
   for(std::size_t i = 0; i < v.size(); ++i)
     os << v[i] << ' ';
   return os;
}
int main()
{
   int tmp_a[5] = {1, 2, 3, 4, 5};
   JavaVector<int> a( std::vector<int>(tmp_a, tmp_a + 5) );
   // deve essere comunque un puntatore (o una referenza)
   Object* obj = &a;
   JavaVector<int> b = *obj;
   std::cout << b.size() << '\n';
   std::cout << b << '\n';
   try
   {
     // lancia bad_cast
     JavaVector<double> c = *obj;
   }
   catch(std::bad_cast&) { std::cerr << "bad cast\n"; }
   // test più complesso
   float tmp_c[5] = {5., 4., 3., 2., 1.};
   JavaVector<float> c( std::vector<float>(tmp_c, tmp_c + 5) );
   std::cout << "*obj = c\n";
   try
   {
      *obj = c;   // lancia bad_cast. (Forse non dovrebbe?)
   }
   catch(std::bad_cast&) { std::cerr << "bad cast\n"; }
   std::cout << "b = *obj\n";
   try
   {
      b = *obj;
   }
   catch(std::bad_cast&) { std::cerr << "bad cast\n"; }
   std::cout << b.size() << '\n';    // safe
   std::cout << b << '\n';
   // i seguenti non compilano.
   // Infatti non è questa la filosofia del C++.
   // std::cout << (*obj)[0] << '\n';
   // std::cout << obj->size() << '\n';
   // comunque:
   std::cout << std::boolalpha;
   std::cout << (typeid(*obj) == typeid(JavaVector<int>)) << '\n';
   std::cout << typeid(obj).name() << '\n';
}
Come vedi è abbastanza complesso.
Io stesso non ben chiare alcune cose :-).
In pratica credo si possa dire che JavaVector è un handle
a se stesso.
Ho usato una classe concreta, std::vector, ma si può
benissimo usare un template:
template<class CppClass> class JavaClass: public CppClass,
                                          public Object
{ /*...*/ };
Chiaramente JavaClass non può essere usata sui built-int, ma
non mi sembra un problema.
Fammi sapere che ne pensi.
Manlio Perillo

 Ecco cosa ho immaginato io:
Mi sono accorto di alcuni errori (anche piuttosto gravi)
1) Nella classe Object mi sono dimenticato di mettere un
   distruttore virtuale
       virtual ~Object() {  }
2) In JavaVector& operator=(const std::vector<T>& v)
   ho scritto
       std::vector<T>::operator=(*v.this_);
   ma al posto di *v.this_ ci vuole v.
   In seguito faccio
       if(self)
       {
           delete this_;
           self = true;
       }
   mentre deve essere:
       if(self) delete this_;
       self = true;
3) In JavaVector(const JavaVector& other)
   nella initialization-list manca:
       std::vector<T>(static_cast< std::vector<T> >(other))
4) In JavaVector operator=(const JavaVector& other)
   manca
       std::vector<T>::operator=(
            static_cast< std::vector<T> >(other));
   inoltre in seguito faccio
       this_ = other.self;
   ma al posto di self deve esserci this_;
5) Infine per operator* e operator-> deve esere:
       virtual JavaVector& operator*() { return *this; }
       virtual JavaVector* operator->() { return this; }
   (ossia this al posto di this_)
Con queste correzioni JavaVector mi sembra piů coerente.
Nei piccoli test che ho ideato, sembra comportarsi bene.
Manlio Perillo

 non mi sembra un problema.
a dir la verità sarebbe meglio se si potessero usare, ma non è poi un
grossissimo problema, in Java effettivamente Object non può puntare ad
un built-in (int, byte o quel che è). quindi è giusto così.
Quello che mi turba invece è il puntatore this_ interno a
JavaVector... perchè c'è?
Poi, qui ho messo giù una cosa che fa praticamente quello che fa il
tuo:
# include <typeinfo>
class Object
{
public:
 virtual const std::type_info &get_typeid() const = 0;
 virtual Object &operator =( const Object &) = 0;
 virtual const Object *get() const = 0;
 virtual ~Object() {}
};
template< typename T> class JavaClass: public Object, public T
{
public:
 JavaClass(): T() {}
 JavaClass( const T& t2): T( t2) { }
 JavaClass( const JavaClass &jc2): T( static_cast< const T &>( jc2))
 }
 JavaClass( const Object &o2) throw ( std::bad_cast): T( static_cast<
const T &>( dynamic_cast< const JavaClass< T> &>( o2))) { }
 JavaClass &operator =( const JavaClass &jc2)
 {
  static_cast< T &>( *this) = static_cast< const T &>( jc2);
  return *this;
 }
 JavaClass &operator =( const Object &o2) throw ( std::bad_cast)
 { return *this = dynamic_cast< const JavaClass< T> &>( o2); }
 const JavaClass *get() const
 { return this;}
 const std::type_info &get_typeid() const
 { return typeid( T);}
 operator T()
 { return static_cast< T &>( *this);}
};
dacci un'occhiata... peccato solo che il mio BCC55 non ne voglia
sapere di lanciare bad_cast su un dynamic_cast fallito e purtroppo non
ho un altro compilatore su cui provarlo (gcc 2.95.2 e MSVC 6 non
supportano i puntatori covarianti), così non sono sicuro che funzioni
bene!

 Comunque con questo esempio, in tuo programma va in crash...
la versione di object che avevo postato mancava dell'operatore =. Ne
ho scritta una nuova e con quella il tuo programma non va in crash.


Che intendi per slicing?
Manlio Perillo


Grazie al tuo esempio, mi sono reso conto che è possibilissimo
usare le referenze.
Ecco la mia implementazione rivista e corretta:
<java.hpp>
#include <typeinfo>
class Object
{
   public:
      virtual Object& operator=(Object&) = 0;
      virtual Object& operator*() = 0;
      virtual Object* operator->() = 0;
      // + gli overload per const
      virtual ~Object() {  };
};
template<class CppClass> class JavaClass: public CppClass,
                                          public Object
{
    public:
      //
      // Costruttori per CppClass
      //
      JavaClass(const CppClass& v)
      : CppClass(v) {  }
      JavaClass& operator=(const CppClass& v)
      {
         CppClass::operator=(v);
         return *this;
      }
      // il costruttore di copie e l'operatore di assegnamento
      // di default vanno bene
      //
      // Costruttori per Object
      //
      JavaClass(Object& obj)
      : CppClass(static_cast<CppClass&>
                 (dynamic_cast<JavaClass&>(obj))) {  }
      // sfrutta la covarianza sul return value
      virtual JavaClass& operator=(Object& obj)
      {
         CppClass::operator=(static_cast<CppClass&>
                             (dynamic_cast<JavaClass&>(obj)));
         return *this;
      }
      // volendo si possono mettere anche i costruttori
      // per Object*. Sono identici.
      virtual JavaClass& operator*() { return *this; }
      virtual JavaClass* operator->() { return this; }
};
Ed ecco anche un test abbastanza completo:
#include <iostream>
#include <vector>
#include "java.hpp"
template<class T>
std::ostream& operator<<(std::ostream& os, const std::vector<T> v)
{
   for(std::size_t i = 0; i < v.size(); ++i)
      os << v[i] << ' ';
   return os;
}
typedef JavaClass< std::vector<int> > JavaVector_int;
typedef JavaClass< std::vector<double> > JavaVector_double;
typedef JavaClass< std::vector<float> > JavaVector_float;
// Se solo ci fossero i typedef template...
int main()
{
   int tmp_a[5] = {1, 2, 3, 4, 5};
   JavaVector_int a( std::vector<int>(tmp_a, tmp_a + 5) );
   // deve essere comunque un puntatore (o una referenza)
   Object* obj = &a;
   JavaVector_int b = *obj;
   std::cout << b.size() << '\n';
   std::cout << b << '\n';
   try
   {
      // lancia bad_cast
      JavaVector_double c = *obj;
   }
   catch(std::bad_cast&) { std::cerr << "bad cast\n"; }
   // test più complesso
   float tmp_c[5] = {5., 4., 3., 2., 1.};
   JavaVector_float c( std::vector<float>(tmp_c, tmp_c + 5) );
   std::cout << "*obj = c\n";
   try
   {
      *obj = c;   // lancia bad_cast. (Forse non dovrebbe?)
   }
   catch(std::bad_cast&) { std::cerr << "bad cast\n"; }
   std::cout << "b = *obj\n";
   try
   {
      b = *obj;
   }
   catch(std::bad_cast&) { std::cerr << "bad cast\n"; }
   std::cout << b.size() << '\n';
   std::cout << b << '\n';
   // i seguenti non compilano.
   // Infatti non è questa la filosofia del C++.
   // std::cout << (*obj)[0] << '\n';
   // std::cout << obj->size() << '\n';
   // comunque:
   std::cout << std::boolalpha;
   std::cout << (typeid(*obj) == typeid(JavaVector_int)) << '\n';
   std::cout << typeid(obj).name() << '\n';
   std::cout << typeid(*obj).name() << '\n';
   a.resize(3);
   b = a;
   std::cout << "b = " << b << '\n';
   std::cout << "a = " << a << '\n';
   JavaVector_int k = a;
   std::cout << "k = " << k << '\n';
}
Controlla bene il tuo BCC 5.5 perchè a me con questi esempi
funziona bene.
Manlio Perillo

 [...]
Manlio Perillo

 sia la soluzione 'giusta'.
d'accordissimo!

 Che intendi per slicing?
ho detto una castroneria, non c'è nessun problema per lo slicing... ad
ogni modo, si parla di slicing quando hai una situazione del genere:
#include <iostream>
class Base
{
public:
    virtual void f()
    {    std::cout << "Base";}
};
class Derived: public Base
{
public:
    virtual void f()
    {    std::cout << "Derived";}
};
void f( Base b)
{
    b.f();
    return;
}
int main()
{
    Derived d;
    f( d);    // stampa sempre e comunque "Base"
    return 0;
}
dovuto al fatto che d viene 'affettato' quando viene passato come
parametro a d. L'unico modo per avere il comportamento corretto è
quello di utilizzare reference o puntatori, e spesso una dichiarazione
come quella di f è in realtà un errore di programmazione. Nel tuo caso
comunque non c'è questo problema perchè Object è una classe astratta e
non la puoi utilizzare così come ho fatto nell'esempio.
Ti consiglio di iscriverti alla newsletter di Carlo Pescio, al sito
www.eptacom.net
,
fino ad ora sono usciti 12 numeri molto interessanti, dove parla anche
di questo problema.