6502's cornerTransaction handling in C++ [2]
2002-12-28
Index

Welcome!
Who am I
Demo
Documents
Small ones
Problems
Chess
Images
Music
My blog
Write me

Versione italiana 


Some C++ code

//
// TrOp class contains an operation that may be
// undone during a rollback. It's an ABC from
// which all undoable operations derive.
//
class TrOp
{
  public:
    virtual void Undo() = 0;
    virtual ~TrOp() {}
};

//
// The Change template generates depending on
// the data type all possible operations (derived
// from TrOp) that may be undone. For example
// the instruction:
//
//           new Change<int>(a);
//
// generates a new "change of an integer" object
// and saves the old value to be able to restore
// it if an undo is requested.
//
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; }
};

//
// The Transaction vector contains all the
// operations that must be undone to get
// back to the last good state of the
// database. It's a vector of pointers as
// TrOps are used polymorphically.
//
std::vector<TrOp *> Transaction;

//
// The Field template generates all possible
// fields in an database object. The Field<T>
// class is responsible for generating an
// appropriate Change<T> object before any
// modification.
//
template<class T> class Field : public T
{
  private:
    void Save()
    {
      Transaction.push_back(
        new Change<T>(*static_cast<T*>(this))
      );
    }
    Field& operator&();
  
  public:
    // Costructor from a T object
    Field(const T& s) : T(s) { }
    
    // Conversion to a T value (const!!!)
    operator const T() const { return *this; }
    
    // Assigning
    Field& operator=(const T& v)
    { Save(); T::operator=(v); return *this; }
    
    Field& operator=(const Field<T>& v)
    { Save(); T::operator=(v); return *this; }
    
    // Remapping of ++/--
    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; }
    
    // Remapping of modifiers
    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; }

    //
    // Note: if there are methods that change
    // the object they should be overridden to
    // be able to call Save() before invoking
    // the original method of T.
    //
};

These declarations made possible to define all classes containing production management data in terms of Field<...>and to get a zero-cost(1) rollback logic on memory representation when implementing logical transactions.

One problem with this approach is that for every change a new Change<type>object is created and this is overkilling (indeed only the very first change could be recorded as other intermediate values for the field are not used if a rollback is requested).

I decided to ignore the issue as with this solution there is no size overhead for the fields: i.e. the size of a Field<int> is exactly the same as the size of a int . Moreover this approach makes easy to implement nested transactions with partial rollback(2)

The template class Field<class T>has been made publically derived from T, so that all method calls are automatically redirected from Field<T>to the corresponding method of T. What is important to consider is that all operations that modify the object must be preceded by a Save() call, that generates the required Change<>object.

In the template Save() is called before all operators that normally modify the object (like assigning, pre/post increment, or +=). If there are other methods that may modify the object the these must be wrapped in the template(3) .

Moreover, because it's not possible to derive from native types, a specialization of Field<>is necessary for all native types (int, long, float, ...) and for pointers.

 

(1)Of course there is a serious overhead in both space and time. This means that modifying a Field<>implies additionals hidden operations and memory allocations that slow down execution. With zero-cost I mean a null cost from a logical point of view of transaction implementation.
(2)I didn't feel the need for such a flexibility but multi-level transaction handling could just use a NULL pointer in the Transaction vector to store a stop point when rolling back.
(3)This transaction handling solution isn't general at all; however it can handle well the problem of data strutures made up with pointers and fields relatively "stupid" that behave similarly to native types (e.g. std::string). This solution can't be easily extended to complex objects stored in Field<>.