POST++ uses default constructors for initializing object while loading
from storage. Programmer should include macro
CLASSINFO(NAME, FIELD_LIST)
in definition of any class, which
instances can be saved in the storage. NAME
corresponds to the
name of this class. FIELD_LIST
describes reference fields of this
class. There are three macros defined in file
classinfo.h for describing references:
REF(x)
REFS(x)
VREFS(x)
List of these macros should be separates by spaces:
REF(a) REF(b) REFS(c)
.
Macro CLASSINFO
defines default constructor (constructor without
parameters) and declares class descriptor of this class. Class descriptor
is static component of the class with name self_class
.
So class descriptor of the class foo
can be accessed by
foo::self_class
. As far as constructors without arguments
are called for base classes and components automatically by compiler,
you should not worry about calling them explicitly. But do not forget
to include REGISTER(NAME)
. Class names
are placed in the storage together with objects. Mapping between application
and storage classes is established during storage opening. Names of all classes
stored in the storage are compared with names of application classes.
If some class name is not found within application classes or
correspondent application and storage classes have different size, then
program assertion will fail.
These rules are illustrated by the following example:
struct branch { object* obj; int key; CLASSINFO(branch, REF(obj)); }; class foo : public object { protected: foo* next; foo* prev; object* arr[10]; branch branches[8]; int x; int y; object* childs[1]; public: CLASSINFO(foo, REF(next) REF(prev) REFS(arr) VREFS(linked)); foo(int x, int y); }; REGISTER(1, foo); main() { storage my_storage("foo.odb"); if (my_storage.open()) { my_root_class* root = (my_root_class*)my_storage.get_root_object(); if (root == NULL) { root = new_in(my_storage, my_root)("some parameters for root"); } ... int n_childs = ...; size_t varying_size = (n_childs-1)*sizeof(object*); // We should subtract 1 from n_childs, because one element is already // present in fixed part of class. foo* fp = new (foo:self_class, my_storage, varying_size) foo(x, y); ... my_storage.close(); } }
It is up to the programmer whether to use explicit or implicit memory
deallocation. Explicit memory deallocation is faster
(especially for small objects) but implicit deallocation (garbage collection)
is more reliable. In POST++ mark and sweep garbage collection scheme is used.
There is special object in the storage: do_garbage_collection
attribute to
storage::open()
method). It is also possible to explicitly
invoke garbage collection during program execution by calling
storage::do_mark_and_sweep()
method. But be sure that there are
no program variable pointed to objects inaccessible from the root objects
(these objects will be deallocated by GC).
Because of multiple inheritance C++ classes can have non zero offset
within object and references inside object are possible. That is why
we have to use special technic to access object header.
POST++ maintains page allocation bitmap each bit of which corresponds to
the page in the storage. If some large object is allocated
at several pages, then bits corresponding to all pages occupied by this object
except first one will be set to 1. All other pages have correspondent bits in
bitmap cleared. To find start address of the object, we first align pointer
value on the page size. Then POST++ finds page in bitmap that contains
beginning of the object (this page should have zero bit in bitmap).
Then we extract information about the object size from object header placed
at the beginning of this page. If size is greater than half of page size then
we have already found object descriptor: it is at the beginning of the page.
Otherwise we calculate fixed block size used for this page and round down
offset of pointer within this page to block size. This scheme of
header location is used by garbage collector, operator delete
defined in object
class and by methods extracting information
from the object header about object size and class.
In POST++ special overloaded new
method is provided
for allocation of objects in the storage. This method takes as extra
parameters class descriptor of created object, storage in which object
should be created and, optionally, size of varying part of the object instance.
Macro new_in(STORAGE, CLASS)
provides
"syntax sugar" for persistent object creation.
Persistent object can be delete by redefined operator delete
.
object
class defined in object.h.
This class contains no variables and provides methods for object
allocation/deallocation and obtaining
information about object class and size at runtime. It is possible to
use object
class as one of multiple bases of inheritance
(order of bases is not significant). Each persistent class should
have constructor which is used by POST++ system (see section
Describing object class).
That means that you should not use constructor without parameters for
normal object initialization. If your class constructor even has no
meaningful parameters, you should add dummy one to distinguish your
constructor with constructor created by macro CLASSINFO
.To access objects in persistent storage programmer needs some kind of root object from which each other object in storage can be accessed by normal C pointers. POST++ storage provides two methods allowing you to specify and obtain reference to the root object:
void set_root_object(object* obj); object* get_root_object();When you create new storage
get_root_object()
returns NULL.
You should create root object and store reference to it by
set_root_object()
method. Next time you are opening storage,
root object can be retrieved by get_root_object()
.
Hint: In practice application classes used to be changed during
program development and support. Unfortunately POST++ due to its simplicity
provides no facilities for automatic object conversion (see for example
lazy object update scheme in
GOODS),
So to avoid problems with adding new fields to the objects, I can recommend
you to reserve some free space in objects for future use. This is especially
significant for root object, because it is first candidate for adding new
components. You should also avoid reverse references to the root object.
If no other object has reference to the root objects, then root object
can be simply changed (by means of set_root_object
method)
to instance of new class. POST++ storage provides methods for setting
and retrieving storage version identifier. This identifier can be used
by application for updating objects in the storage depending on the storage
and the application versions.
file description | when used | suffix |
---|---|---|
temporary file with new storage image | used in non-transaction mode to store new image of storage | ".tmp" |
transaction log file | used in transaction mode to saved shadow pages | ".log" |
saved copy of storage file | used only in Windows-95 for renaming temporary file | ".sav" |
Description | Interface | Implementation |
---|---|---|
Arrays of scalars and references, matrixes and strings | array.h | array.cxx |
L2-list and AVL-tree | avltree.h | avltree.cxx |
Hash table with collision chains | hashtab.h | hashab.cxx |
R-tree with quadratic method of nodes splitting | rtree.h | rtree.cxx |
T-tree (combination of AVL tree and array) | ttree.h | ttree.cxx |
Text object with modified Boyer and Moore search algorithm | textobj.h | textobj.cxx |
In the article "A study of index structures for main memory database management systems" T.J. Lehman and M.J Carey proposed T-trees as a storage efficient data structure for main memory databases. T-trees are based on AVL trees proposed by Adelson-Velsky and Landis. In this subsection, we provide an overview of T-trees as implemented in POST++.
Like AVL trees, the height of left and right subtrees of a T-tree may differ by at most one. Unlike AVL trees, each node in a T-tree stores multiple key values in a sorted order, rather than a single key value. The left-most and the right-most key value in a node define the range of key values contained in the node. Thus, the left subtree of a node contains only key values less than the left-most key value, while the right subtree contains key values greater than the right-most key value in the node. A key value which is falls between the smallest and largest key values in a node is said to be bounded by that node. Note that keys equal to the smallest or largest key in the node may or may not be considered to be bounded based on whether the index is unique and based on the search condition (e.g. "greater-than" versus "greater-than or equal-to").
A node with both a left and a right child is referred to as an internal node, a node with only one child is referred to as a semi-leaf, and a node with no children is referred to as a leaf. In order to keep occupancy high, every internal node has a minimum number of key values that it must contain (typically k-2, if k is the maximum number of keys that can be stored in a node). However, there is no occupancy condition on the leaves or semi-leaves.
Searching for a key value in a T-tree is relatively straightforward. For every node, a check is made to see if the key value is bounded by the left-most and the right-most key value in the node; if this is the case, then the key value is returned if it is contained in the node (else, the key value is not contained in the tree). Otherwise, if the key value is less than the left-most key value, then the left child node is searched; else the right child node is searched. The process is repeated until either the key is found or the node to be searched is null.
Insertions and deletions into the T-tree are a bit more complicated. For insertions, first a variant of the search described above is used to find the node that bounds the key value to be inserted. If such a node exists, then if there is room in the node, the key value is inserted into the node. If there is no room in the node, then the key value is inserted into the node and the left-most key value in the node is inserted into the left subtree of the node (if the left subtree is empty, then a new node is allocated and the left-most key value is inserted into it). If no bounding node is found then let N be the last node encountered by the failed search and proceed as follows: If N has room, the key value is inserted into N; else, it is inserted into a new node that is either the right or left child of N, depending on the key value and the left-most and right-most key values in N.
Deletion of a key value begins by determining the node containing the key value, and the key value is deleted from the node. If deleting the key value results in an empty leaf node, then the node is deleted. If the deletion results in an internal node or semi-leaf containing fewer than the minimum number of key values, then the deficit is made up by moving the largest key in the left subtree into the node, or by merging the node with its right child.
In both insert and delete, allocation/deallocation of a node may cause the tree to become unbalanced and rotations (RR, RL, LL, LR) may need to be performed. (The heights of subtrees in the following description include the effects of the insert or delete.) In the case of an insert, nodes along the path from the newly allocated node to the root are examined until either
In the case of delete, nodes along the path from the de-allocated node's parent to the root are examined until a node is found whose subtrees' heights now differ by one. Furthermore, every time a node whose subtrees' heights differ by more than one is encountered, a rotation is performed. Note that de-allocation of a node may result in multiple rotations.
There are several test programs for testing classes from POST++ persistent class library, which are included in default make target:
Program | Tested classes |
---|---|
testtree.cxx | AVL-tree, l2-node, hash table |
testtext.cxx | text, string |
testspat.cxx | rectangle, R-tree, hash table |
testperf.cxx | T-tree insert, find and remove operations |
guess
.
Unix specific:
When you will link your Unix application with POST++ library
and persisent objects in application contain virtual functions, please do not
forget to recompile comptime.cxx file and include
it in the linker's list. This file is necessary for POST++ to
provide executable file timestamp, which is placed in the storage and
used to determine when application is changed and reinitialization
of virtual function table pointers in objects is needed.
Attention! This file should be recompiled each time your are
relinking your application. I suggest you to make compiler to call linker for
you and include comptime.cxx
source file in the list of object
files for the target of building executable image
(see makefile).
handle SIGSEGV nostop noprint pass
. If SIGSEGV signal
is not caused by storage page protection violation, but due to a bug
in your program, POST++ exception handler will "understand" that it is
not his exception and send SIGABRT signal to the self process, which
can be normally catched by debugger.
main
or WinMain
function) with structured
exception block. You should always use structured exception handling
with Borland C++, because Unhandled Exception Filter is not correctly called
in Borland. Please use two macros SEN_TRY
and
SEN_ACCESS_VIOLATION_HANDLER()
to enclose body of main
(or WinMain) function:
main() { SEN_TRY { ... } SEN_ACCESS_VIOLATION_HANDLER(); return 0; }Be sure that Debugger behavior for this exception is "Stop if not handled" and not "Stop always" (you can check it in Debug/Exceptions menu). In file testrans.cxx you can find example of using structured exception handling.
Look for new version at my homepage | E-mail me about bugs and problems