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<>.
|
|