L'angolo di 6502Transazioni in C++ [2]
2002-12-28
Indice

Benvenuti!
Chi sono
Demo
Documenti
Quelli piccoli
Problemi
Scacchi
Immagini
Musica
Il mio blog
Scrivimi

English version 


Un po' di codice C++

//
// La classe TrOp contiene una operazione che puo'
// essere annullata durante un rollback.
// Si tratta di una classe base astratta da cui
// derivano tutti i tipi di operazione annullabile.
//
class TrOp
{
  public:
    virtual void Undo() = 0;
    virtual ~TrOp() {}
};

//
// Il template Change genera al variare del tipo
// dati usato le possibili operazioni (derivate
// da TrOp) che e' possibile annullare in un
// rollback. In pratica l'istruzione
//
//           new Change<int>(a);
//
// crea un oggetto "cambio di intero" e salva
// il vecchio valore in modo da rendere
// annullabile l'operazione eseguita.
//
template<class T> class Change : public TrOp
{
  public:
    T& who;
    T old_value;
    
    Change(T& s) : who(s), old_value(s) { }
    void Undo() { who=old_value; }
};

//
// Il vettore Transaction contiene tutte le
// operazioni da annullare in caso di una
// richiesta di rollback.
//
std::vector<TrOp *> Transaction;

//
// Il template field genera al variare del
// tipo dati i possibili campi di un oggetto.
// Il campo provvede a generare un opportuno
// oggetto Change<tipo> ad ogni modifica.
//
template<class T> class Field : public T
{
  private:
    void Save()
    {
      Transaction.push_back(
        new Change<T>(*static_cast<T*>(this))
      );
    }
    Field& operator&();
  
  public:
    // Costruttore da oggetto T
    Field(const T& s) : T(s) { }
    
    // Conversione a valore (const!!!)
    operator const T() const { return *this; }
    
    // Assegnazione
    Field& operator=(const T& v)
    { Save(); T::operator=(v); return *this; }

    Field& operator=(const Field<T>& v)
    { Save(); T::operator=(v); return *this; }
    
    // Rimappatura ++/--
    const T operator++(int)
    { Save(); return T::operator++(0); }
    
    const T operator--(int)
    { Save(); return T::operator--(0); }
    
    Field& operator++()
    { Save(); T::operator++(); return *this; }
    
    Field& operator--()
    { Save(); T::operator--(); return *this; }
    
    // Rimappatura incrementi/decrementi
    Field& operator+=(const T& v)
    { Save(); T::operator+=(v); return *this; }
    
    Field& operator-=(const T& v)
    { Save(); T::operator-=(v); return *this; }
    
    Field& operator*=(const T& v)
    { Save(); T::operator*=(v); return *this; }
    
    Field& operator/=(const T& v)
    { Save(); T::operator/=(v); return *this; }
    
    Field& operator%=(const T& v)
    { Save(); T::operator%=(v); return *this; }

    //
    // Nota: se sono presenti metodi che
    // modificano una classe e' necessario
    // inserire un opportuno override in modo
    // da effettuare una Save() completa o
    // Save() specifiche delle parti modificate
    // prima di invocare il metodo in questione.
    //
};

Queste dichiarazioni mi hanno permesso di definire le classi contenenti i dati gestionali in termini di Field<...>e di ottenere a costo zero(1) la logica di rollback sulla rappresentazione in memoria.

Uno dei problemi della soluzione e' quello che per ogni modifica viene creato un nuovo oggetto di tipo Change<tipo> e questo comporta un inutile spreco (infatti in caso di rollback solo il valore originale precedente la prima modifica e' realmente necessario).

Io ho deciso di non occuparmi del problema perche' con la soluzione attuale non esiste un overhead di spazio per i campi; in altre parole un campo di tipo Field<int> occupa esattamente quanto un campo di tipo int . Inoltre la soluzione corrente permette anche la gestione di transazioni nidificate con rollback parziali(2) .

La classe template Field<class T>e' stata dichiarata come derivata della classe T, in modo da ottenere un automatico "dirottamento" di tutte le chiamate a metodi di Field<T>verso i corrispondenti metodi di T. Quello che pero' e' importante notare e' che tutte le operazioni che modificano l'oggetto devono essere precedute da una chiamata di Save() che provvede a generare il necessario oggetto Change<>corrispondente.

Nel template vengono incapsulati salvataggi prima di tutti gli operatori che normalmente modificano l'oggetto (come assegnazione, pre/post incremento o +=). Se sono presenti altri metodi che modificano l'oggetto questi devono essere indicati nel template(3) .

Inoltre, visto che non e' possibile derivare dai tipi base, una specializzazione di Field<>e' necessaria per tutti i tipi nativi (int, long, float, ...) e per i puntatori.

 

(1)Ovviamente esiste un costo affatto trascurabile in termini di prestazioni e occupazione di memoria. In altre parole la modifica di un Field<>porta ad operazioni aggiuntive di copia/allocazione che ne rallentano l'esecuzione. Con costo zero intendo un costo nullo dal punto di vista della logica di implementazione delle transazioni.
(2)Non ho sentito per ora la necessita' di implementare la cosa ma molto semplicemente utilizzare un puntatore NULL all'interno di Transaction mi permetterebbe di indicare il punto in cui fermare una richiesta di rollback.
(3)La soluzione che ho trovato non e' affatto generale; tuttavia e' in grado di risolvere egregiamente il problema di strutture dati formate da puntatori e campi relativamente "stupidi" come si comportino in maniera molto simile ai tipi nativi (es. std::string). Questa soluzione non e' facilmente estendibile al caso di oggetti complessi memorizzati in Field<>.