home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Monster Media 1994 #1
/
monster.zip
/
monster
/
PROG_C
/
CL187A.ZIP
/
CL187A.TXT
< prev
next >
Wrap
Text File
|
1994-03-15
|
202KB
|
5,311 lines
Container Lite
version 1.87a, 3-15-94
Portable, Persistent,
Hybrid Container Class
for AT&T C++ 2.x & 3.x
Copyright 1994
John W. Small
All rights reserved
PSW / Power SoftWare
P.O. Box 10072
McLean, Virginia 22102 8072
USA
Voice: (703) 759-3838
Software License Agreement
PSW / Power SoftWare licenses the bona fide purchaser of the
Container Lite to use the tool in the development of software without
royalty. The licensee may not publish any portion of the tool kit and
may only make backup copies of software as a means of protecting
the licensee's investment against loss or damage.
Limited Warrantee
PSW / Power SoftWare warrants the Container Lite toolkit to be free
from defects in materials and workmanship for a period of 90 days
from the original purchase date and will replace same if found to be
defective and reported in writing to PSW within this period. The
fitness of the Container Lite toolkit for any particular application is
not implied nor guaranteed. All other liabilities which may be
construed are specifically disclaimed except where and when required
by law.
Technical Support
Have a question, comment, or suggestion? I'm looking forward to
hearing from you. If hard copy mail is too slow you can contact me
directly as indicated below.
John W. Small
Voice: (703) 759-3838 evenings
Acknowledgments
I wish to thank my Lord and Savior, Jesus Christ, who faithfully died
for my sins taking them upon himself along with God's wrath (hell)
meant deservingly for me, so that I might be drawn, albeit
unwillingly, to receive God's unmerited gift of reconciliation and
new and eternal life.
Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .5
Getting Started. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .7
Hybrid Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Elastic Array . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Stack - Queue - Deque . . . . . . . . . . . . . . . . . . . . . . . 16
List. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Sort - Search - Unique. . . . . . . . . . . . . . . . . . . . . . . 21
Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Strong Type Checking . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Additional Functionality. . . . . . . . . . . . . . . . . . . . . . 31
A Well Endowed Data Type. . . . . . . . . . . . . . . . . . . . . . 35
Deficient Data Types. . . . . . . . . . . . . . . . . . . . . . . . 40
Mediator Functions. . . . . . . . . . . . . . . . . . . . . . . . . 43
Protected Scope Virtual Functions. . . . . . . . . . . . . . . . . . . . 47
Polymorphic Nodes. . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Reference. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
Type-less Container . . . . . . . . . . . . . . . . . . . . . . . . 69
Internals. . . . . . . . . . . . . . . . . . . . . . . . . . . 69
Constructors and Destructor. . . . . . . . . . . . . . . . . . 74
Housekeeping . . . . . . . . . . . . . . . . . . . . . . . . . 76
Elastic Array. . . . . . . . . . . . . . . . . . . . . . . . . 78
Stack, Queue, and Deque. . . . . . . . . . . . . . . . . . . . 83
List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Sort, Search, and Unique . . . . . . . . . . . . . . . . . . . 88
FunctionRegistry. . . . . . . . . . . . . . . . . . . . . . . . . . 93
Streamable. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
ClassRegistry . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Mutual. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .100
MutualHoldingPen. . . . . . . . . . . . . . . . . . . . . . . . . .105
Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .107
Introduction
The Container Lite (CL v1.87a) is a portable, persistent, hybrid container
class library compatible with the AT&T C++ 2.x, 3.x standard. Full
source code is provided along with "forms" for compilers not yet supporting
templates. Applications using Container Lite use only a small fraction of the
container code space consumed by other commercial and compiler bundled
container class libraries. Use Container Lite repeatedly throughout all your
applications for even greater savings in design and coding effort as well as
drastically reducing their sizes. Even if you were to incorporate the entire
Container Lite library into your application it would add less than an austere
10K bytes to its code size! Conventional container libraries can't even come
close to a multiple of that figure or capability. Container Lite is ideal for
coding games, rolling your own application frameworks, and any application
requiring user configurations to persist.
Container Lite is based upon a single container class having a hybrid stack-
queue-priority_queue-deque-list-array interface. Included amongst its
members are sort, search, and iterate functions. Traditional container
libraries, on the other hand, typically incorporate these structures and
functionality in a myriad hierarchy. Their "textbook" approaches often
complicate the implementation of real world algorithms, especially those
requiring hybrid structures. For example, Container Lite can be readily
contorted to implement polymorphic trees and graphs. A Container Lite
container can even be saved on a stream and later reloaded while multiple
references to its various elements are automatically resolved, thus allowing
your complex container networks to persist without a second thought. Try
that with any one of those conventional container libraries and you are in for
a formidable exercise in OOP extensibility. That's because those libraries
are inherently inflexible by design with their towering, convoluted
hierarchies and steep learning curves.
Container Lite is implemented as an elastic array of void pointers pointing
to the data you are collecting. Unlike a conventional C++ array that is
statically sized, Container Lite can expand or contract automatically to
accommodate varying numbers of cells. You have full control over the
granularity and hysteresis of this elasticity. Template invocations
additionally define methods for on the fly copy initialization, assignment,
and destruction of your data nodes automatically. Container Lite's "form
template" approach provides the same automatic code generation and strong
type checking for compilers not yet supporting templates.
Chapter 1
Getting Started
Copy the following files to the appropriate directories for the C++ compiler
and operating system you are using.
cl.h Container Lite header
cl.hf Container Lite "form templates"
cl.cpp Container Lite source
You may need to rename the extensions of these files to suit your compiler.
Container Lite source code is AT&T C++ version 2.x, 3.x compatible.
For those of you who don't have smart linker/librarians on your system, the
Container Lite source is broken down into its individual member functions
and grouped together in appropriate files to facilitate the building of
libraries. These files can be found in the "cllib.src" subdirectory on the
distribution diskette. You will find a makefile in this subdirectory. Edit the
macros in the makefile to suit your environment. A Container Lite library
can be quickly built for your compiler using this makefile.
The examples found in the text are grouped together in the "clexamps.cpp"
subdirectory.
Be sure to read any readme.doc files that may appear on the distribution
diskette for last minute revisions. (Please note that a readme.doc file may
not appear.)
cl.h Macros
Container Lite is compatible with AT&T C++ 2.x and 3.x compilers. To
cover such a wide spectrum of capability, the cl.h header file attempts to
dynamically define four macros, i.e.
CL_NO_TEMPLATES CL_NO_EXCEPTIONS
CL_NO_STDARG_H CL_CDECL
describing your compiler's environment by sensing your compiler-specific,
predefined, versioning macro. These macros have already been properly
defined in cl.h for Borland C++ and Microsoft C++ 7.0 and 8.0
compilers.
For other compilers you will need to either edit cl.h as described below or
globally define these macros for your project each time you "make" it. For
an old Unix compiler that doesn't support templates or exceptions and
stdarg.h isn't available the command line compile command would be:
CC -DCL_NO_TEMPLATES -DCL_NO_EXCEPTIONS \
-DCL_NO_STDARG_H yourapp.cpp
assuming the -D switch is the define macro switch for your compiler.
Otherwise the cl.h header file assumes that your compiler supports templates
and exception handling along with stdarg.h.
If your compiler doesn't yet support templates, be sure to define
CL_NO_TEMPLATES in cl.h whenever your compiler's distinguishing
predefined version macro appears. The comments in cl.h around line 28
show how this is done.
Likewise, if your compiler doesn't yet support exceptions, be sure that cl.h
defines CL_NO_EXCEPTIONS around line 56. If your compiler doesn't
support the ANSI C standard header file stdarg.h, e.g. UNIX V CC, you
must define CL_NO_STDARG_H in cl.h around line 113. Without stdarg.h
the cl::forEach() iterator is not available! If however your compiler does
support stdarg.h and at the same time allows for other than the normal C
calling convention you may need to define CL_CDECL in cl.h around line
133.
Exception Handling
Normally, Container Lite member functions catch xalloc exceptions thrown
from within returning a failure indicator instead of terminating the program
without requiring a set_new_handler(0) call! This includes any xalloc
exceptions thrown from within your data's default constructor or copy
initializer constructor whenever they are called by Container Lite member
functions. Even if your application calls set_new_handler(0) it makes no
difference, Container Lite member functions still report heap exhaustion by
returning a failure indicator, i.e. 0. If you want Container Lite member
functions to pass those exceptions on to you application code for processing,
i.e. catch (...), you must comment out the CL_NO_THROW_XALLOC
macro definition found in cl.h around line 74. Alternatively, you can always
globally define the CL_THROW_XALLOC macro for your entire project
each time you "make" it.
All other exceptions thrown from your data's overloaded operators, e.g.
operator==(), operator>(), operator=(), operator<<(), operator>>(),
operator delete(), etc. are passed through to your application code. See
examp101.cpp to get an idea of how you can catch these exceptions.
Suppose your data's overload assignment operator throws the exception, e.g.
xmsg. This exception is not caught by the calling Container Lite member
function. However, Container Lite member functions are written in such a
way that exceptions thrown by your data's overloaded member functions
won't leave the container in an unstable state. In other words, your
exception handler can recover and continue processing instead of always
being forced to terminate.
Container Lite transparently traps any dynamic memory it owns that is left
dangling by a thrown exception, reclaiming it as you continue working with
the container or when the container is finally destructed. To reclaim
dangling memory immediately, your exception handler should call the
catch_reset() member function (see examp101.cpp).
With this in mind, please note that you should code your overloaded
assignment operator so that it will throw all exceptions if possible before
overwriting any target data. This way Container Lite applications can
gracefully recover from exceptions with the container and all its nodes left
intact! For example, if atPutAsg() throws an exception, the node being
assigned to won't be corrupted.
But remember, Container Lite will only catch "xalloc" exceptions thrown
from your default or copy initializer constructors. What happens if your
data's overload assignment operator throws an xalloc exception? Your
application must trap it! Again if you haven't overwritten any target node
data you can assume the container has been left intact just as it was before
its "Asg" method was called. Be careful that you don't leave dynamically
allocated memory dangling that is still "owned" by your overload operator
at the time the exception thrown! If you do, your code has a potential
memory leak.
In streaming operations, i.e. save() or load(), it is assumed that your
compiler switches are set so that auto (local) objects are destructed whenever
an exception is thrown past them. If you elect to turn off the calling of
these destructors, be aware that file handles could be left open. In these
cases, if your handler continues processing instead of terminating, it is
possible to eventually run out of file handles. Moral: if you turn off
destructor calls your exception handler should terminate the application!
Chapter 2
Hybrid Container
Always think of Container Lite in terms of its multi-faceted hybrid behavior.
One moment your container is a stack, the next a priority queue or array,
etcetera. With Container Lite you are never locked into some arcane branch
class of a convoluted, towering hierarchy like you would be with a
conventional "textbook" container implementation.
Elastic Array
Container Lite's elastic array behavior provides the foundation for the rest
of its inherent behaviors. Imagine an array in which you can insert or
extract cells at any location. Container Lite lets you specify the granularity
and hysteresis of this elasticity in constructor and/or housekeeping calls with
a delta parameter. In other words, when a new cell needs to be added how
many additional spare cells should be provided to optimize future expansion
efforts? And how many spare cells should be allowed to accumulate before
compaction? The following list of member functions catalogs this elastic
array behavior.
vector() Limit() setLimit()
pack() Delta() setDelta()
Nodes() MaxNodes() setMaxNodes()
vacancy() vacancyNonElastic()
atIns() atInsNew() atRmv()
allRmv() atDel() atDelAsg()
allDel() atPut() atPutNew()
atPutAsg() atGet() operator[]()
atGetAsg() atXchg() index()
forEach() detect() select()
reject() collect()
If you use Container Lite's template or "form template" to generate a
strongly typed checked container wrapper, you can then assign and clone
your data on the fly. Methods with "Asg" and "New" suffixes assign and
clone your data respectively. Flags are available that signal a container to
delete dynamic nodes upon destruction. Templates can also provide
persistence if your data has overloaded stream insertion and extraction
operators along with a default constructor. If not you still have the option
of defining template mediator functions to adapt to not so well endowed data
types.
Elastic array operation is really summed up by two methods, atIns() and
atRmv(). AtIns() writes a pointer value to a newly inserted cell in the
container's logical array at the index indicated. The cell originally at the
index and all successive cells are pushed back one index location, expanding
the physical array if necessary. AtRmv() performs the inverse function of
atIns(), extracting the indexed cell returning its pointer while pulling all
successive cell indices forward by one. The physical array is shrunk
automatically if excessive space has accumulated.
If you use the Container Lite template to generate a strongly typed check
wrapper class for your data, you can then use the above methods with
"Asg", "New", and "Del" suffixes. These methods allow you to assign,
clone, and delete your data nodes on the fly. Persistence is also provided
which we will see in our first example.
// examp201.cpp -- link with cl.obj
#include <stdlib.h> // rand(), srand()
#include <time.h> // time_t, time()
#include "cl.h"
CL_WELL_ENDOWED(int)
#define intFile "ints.tmp"
main()
{
time_t t;
srand((unsigned) time(&t));
CL<int> ci(CL_ANDS,15);
int i = 0;
while (ci.atInsNew(rand()%(ci.Nodes()+1),&i))
i++;
ci.save(intFile);
ci.allClr();
ci.load(intFile);
cout << "0-14 in random order: " << endl;
while (ci.atDelAsg(0,&i))
cout << i << endl;
return 0;
}
The CL_WELL_ENDOWED() macro defines the requisite inline mediator
functions that enable the CL<int> template invocation to make full use of
the "int" data type's implicit operator>(int&), operator==(int&),
operator=(int&), int(int&) and int() constructors, and
operator<<(ostream&, int&) and operator>>(istream&, int&) functions
(we'll learn more about this in the next chapter). CL<int> in effect
generates a Container Lite wrapper class for integers. Without
CL_WELL_ENDOWED() we wouldn't be able to use the container's
"(A)sg", "(N)ew", "(D)el" and stream (S)ave methods. The CL_ANDS
composite flag enables the container's "(A)sg", "(N)ew", "(D)el" and stream
(S)ave methods. Actually CL_ANDS is defined as the flags expression:
(CL_ASG | CL_NEW | CL_DEL | CL_SAVE). This container is also
limited to a maximum of 15 items.
Next, the numbers zero through fourteen, i.e. the values of the variable i,
are inserted at random locations in the "array." Notice that the first
parameter of the atInsNew() method is the "at" parameter. Cells of the
elastic array are indexed starting at zero just like a conventional C++ array.
Thus the last cell index of the elastic array is Nodes() - 1. The "at" location
selected for insertion is a random number between zero and Nodes(). Any
attempt to insert at a location greater than Nodes()-1 is simply queued onto
the end of the array.
The second parameter of the atInsNew() method is the "insert what"
parameter. The "New" suffix indicates that the item to be inserted must be
cloned on the fly and that this clone is to be bound in the container instead
of the original variable i in this case. To bind means to store the address of
a item in a cell of the container's elastic array. Thus all item parameters of
all Container Lite methods are pointers. Hence we take the address of the
variable i.
Most Container Lite methods return integers or pointers. Such methods
indicate failure by returning zero. In our example atInsNew() returns a
pointer to the newly cloned integer being bound. When maxNodes is
reached, i.e. 15, atInsNew() returns NULL indicating failure and the while
loop terminates.
Since the CL_SAVE flag is set (via CL_ANDS), the save() method is
enabled and our container of integers is saved in the file "ints.tmp." The
complete state of the container is saved in the file, e.g. flags, maximum
node setting, etcetera. If you already have an open stream handle you can
use the stream insertion operator instead of the save() method.
Since the CL_DEL flag is set (via CL_ANDS), the allClr() method calls
allDel() instead of allRmv(). AllRmv() releases all binding addresses,
removing their cells from the elastic array. AllDel does the same thing
except that the nodes are deleted upon removing with the delete operator via
a call to the container's deleteD() protected scope virtual function.
There is no CL_LOAD flag since a container on a stream would never have
the option of having a reset load flag. The load() method calls allClr()
before attempting to load the container from stream. Since the loaded items
are naturally dynamic, the CL_DEL flag is set if it isn't already. The
complete state of the container saved in the stream is restored.
The first parameter of atDelAsg() is the "at" parameter and the second is the
"assign to what" parameter. After assignment is made the original container
node is deleted via a call to the container's deleteD() protected scope virtual
function. AtDelAsg() returns the address of the variable i each time until the
container is empty at which time it returns NULL. Both the CL_DEL and
CL_ASG flags must be set for atDelAsg() to be enabled.
When leaving the scope of main() the container's destructor is automatically
called. If CL_DEL flag is set, allDel() is called instead of allRmv(). This
is irrelevant in our example since all nodes have already been disposed of.
The logical basis for method naming is as follows. The atInsNew() method
performs the same operation as atIns() except the data is cloned on the fly
and the clone is bound in the container instead of the variable i. Again, to
bind means to store the address of a node in a cell of the container's elastic
array. In either case a pointer to the bound node is returned to indicate
success or NULL otherwise. AtDelAsg() basically performs an atGetAsg(),
(As)si(g)ning (copying) the outgoing node to the variable i, before
performing an equivalent atDel() which is the same thing as an atRmv()
except the outgoing node is (Del)eted and not just the cell (R)e(m)o(v)ed
from the array.
If your compiler doesn't support templates you can use the "form
template" approach shown below instead.
// examp202.cpp -- link with cl.obj
#include <stdlib.h> // rand(), srand()
#include <time.h> // time_t, time()
#define ITEM int
#define CL_WELL_ENDOWED
#define CL CL_int
#include "cl.hf"
#define intFile "ints.tmp"
main()
{
time_t t;
srand((unsigned) time(&t));
CL_int ci(CL_ANDS,15);
int i = 0;
while (ci.atInsNew(rand()%(ci.Nodes()+1),&i))
i++;
ci.save(intFile);
ci.allClr();
ci.load(intFile);
cout << "0-14 in random order: " << endl;
while (ci.atDelAsg(0,&i))
cout << i << endl;
return 0;
}
Notice that you must define ITEM as the parameter type that would
normally be passed as the template parameter, i.e. "#define ITEM int" is
the equivalent of passing "int" to "CL<>." Likewise CL is defined as a
new type name so that you use "CL_int" in place of "CL<int>." You
must define CL_WELL_ENDOWED so that the form template is
customized to make fully use of all implicit or explicit overloaded
operators for your data type.
The next example shows a container being used to sort a standard C++
vector of strings. Both the template and form template approaches are
shown combined.
// examp203.cpp - link with cl.obj
// #define CL_NO_TEMPLATES
char *V[] = {
"Vectors of pointers can",
"be exploded into containers",
"for processing. A container",
"can also be imploded into",
"a vector of pointers to",
"its nodes.",
0
};
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM_char
#include "cl.hf"
#endif
#define vectorFile "vector.tmp"
main()
{
CL_char v(V,0,CL_SAVE); // explode constructor
cout << "\n\nExploded, unsorted vector ...\n\n";
for (unsigned i = 0; v[i]; i++)
cout << (char *) v[i] << "\n";
v.save(vectorFile);
v.allClr();
v.load(vectorFile);
v.sort (); // sort
v.vector(V); // implode
cout << "\n\nImploded, sorted vector ... \n\n";
for (i = 0; v[i]; i++)
cout << v[i] << "\n";
return 0;
}
The Container Lite header file, i.e. cl.h, automatically defines
CL_NO_TEMPLATES for compilers not yet supporting templates. If
your compiler doesn't support templates be sure to add its registration to
the header file. Comments in cl.h explain how. In our example we also
allow for the possibility of defining CL_NO_TEMPLATES. This was
done simply so that the form template approach could be tested with a
template generating compiler.
Container Lite can automatically generate a template for a container of
strings, i.e. CL_char. With the form template approach you must signal
the generation of container of strings by defining ITEM_char. Normally
you won't include code support for both the template and form template
approaches. It is shown here and in later examples so that you can
contrast the two methods.
The vector() function will allocation a dynamic vector if one isn't
provided as a parameter. The overloaded subscript operator, i.e. [], is a
convenient notation for calling atGet(). For details about any Container
Lite member function or data be sure to refer to the reference chapter.
Remember, the examples given in this chapter are meant to give you a
feel for Container Lite's different modes of operation, i.e. elastic array,
stack-queue-deque, list, and sort-search-unique. Thus the descriptions
here don't focus on member function details.
You may have noticed by now that neither iostream.h nor iomanip.h is
ever included in our examples. That's because Container Lite header files
already pull them in. And string.h is automatically pulled in also.
Stack - Queue - Deque
A stack provides LIFO (Last In, First Out) storage. Container Lite has a
complete set of stack methods. These functions don't disturb the list
behavior of a container, e.g. current node setting. A queue provides
FIFO (First In, First Out) storage. And the deque (pronounced deck)
provides FOLO (First Out, Last Out) storage. Think of a deque as a
combination stack-queue with the additional capability of being able to be
popped from the rear. The following is a list of the member functions
supporting this stack-queue-deque behavior:
push() pushNew() pop()
popDel() popDelAsg() top()
topAsg() insQ() insQNew()
unQ() unQDel() unQDelAsg()
rear() rearAsg() operator<<()
operator>>()
The example for this section uses a container of floats. Again both the
template and form template approaches are shown.
// examp204.cpp -- link with cl.obj
// #define CL_NO_TEMPLATES
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM float
#define CL_WELL_ENDOWED
#define CL CL_float
#include "cl.hf"
#else
CL_WELL_ENDOWED(float)
#define CL_float CL<float>
#endif
#define floatFile "floats.tmp"
main()
{
CL_float cf(CL_ANDS,10);
for (float i = 1.0; cf.pushNew(&i); i++);
cf.save(floatFile);
cf.allClr();
cf.load(floatFile);
cout << "Count to 10: " << endl;
while (cf.unQDelAsg(&i))
cout << i << endl;
return 0;
}
CL_ANDS (equivalent to the CL_ASG, CL_NEW, CL_DEL, and
CL_SAVE flags) is set in the constructor call this time. Without
CL_NEW being set, pushNew() would have been inhibited and all other
member functions with the "New" suffix. Without CL_ASG and
CL_DEL being set, unQDelAsg() would also have been inhibited along
with all other functions with "Asg" and/or "Del" in their names. Without
CL_SAVE the save() method would also have been inhibited. The
various flags are explained in detail in the reference chapter.
This container is limited to a maximum of 10 items. That's because 10
was passed to the constructor parameter named "maxNodes." This can be
changed at any time with a call to the setMaxNodes() function that you
saw cataloged in the previous section. The unQ() family of member
functions work on the rear of the queue thus providing the deque
behavior.
The container's destructor is automatically called just before "return 0;."
Since the previous while loop has already deleted all nodes there is
nothing left to delete. However, you should take note that if a container's
CL_DEL flag is raised the destructor will attempt to delete any remaining
nodes, otherwise the nodes are simply removed. Thus you should
typically segregate you containers into two groups: those with static nodes
and those with dynamic. You don't want to accidently delete a statically
allocated node that is mingling among the dynamically allocated ones! If
you mix the two types it's safest not to set the CL_DEL flag since a
compiler generated destructor call could then jump out and bite you!
List
Any container can be treated as if it were a doubly linked list thereby
providing sequential storage. In a container's header structure a current
node index is maintained that lets the list methods know where the
insertion or removal is to take place. The elastic array and
stack-queue-list methods don't disturb this current node index unless of
course it points to the cell that's being removed. In that case the current
node becomes undefined just like it was when the container was first
constructed. When a container is saved in a file the current node setting
is saved too. When the container is reloaded its previous current node
setting is restored. Here's a list of the list methods.
CurNode() setCurNode() ins()
insNew() rmv() del()
delAsg() put() putNew()
putAsg() get() getAsg()
next() operator++() nextAsg()
prev() operator--() prevAsg()
Let's look at a little more rigorous, realistic example this time. We'll
create a container of Employees. You'll learn more in the next chapter
about using (form) templates for various data types.
// examp205.cpp -- link with cl.obj
// #define CL_NO_TEMPLATES
#include "cl.h"
class Employee {
char *name;
unsigned salary;
int cmp(const Employee& e) const;
static Employee * THIS;
static ostream& SHOW(ostream& os)
{
return os << "Employee name: "
<< setw(20)
<< (THIS->name? THIS->name : "n/a")
<< "\tsalary: " << THIS->salary;
}
public:
Employee(const char * name = 0,
unsigned salary = 0)
{
this->name = (name? strdup(name) : 0);
this->salary = salary;
}
Employee(const Employee& e)
{
name = (e.name? strdup(e.name) : 0);
salary = e.salary;
}
Employee& operator=(const Employee& e)
{
delete name;
name = (e.name? strdup(e.name) : 0);
salary = e.salary;
return *this;
}
int operator==(const Employee& e) const
{ return !cmp(e); }
int operator>(const Employee& e) const
{ return (cmp(e) > 0); }
~Employee() { delete name; }
ostream& (*show())(ostream&)
{ THIS = this; return SHOW; }
friend ostream& operator<<
(ostream& os, Employee& e)
{ return os << &e.name << endm << e.salary; }
friend istream& operator>>
(istream& is, Employee& e)
{ return is >> &e.name >> nextm >> e.salary; }
};
Employee * Employee::THIS;
int Employee::cmp(const Employee& e) const
{
if (!name)
if (!e.name)
return 0;
else
return -1;
else
if (!e.name)
return 1;
else
return strcmp(name,e.name);
}
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM Employee
#define CL_WELL_ENDOWED
#define CL CL_Employee
#include "cl.hf"
#else
CL_WELL_ENDOWED(Employee)
#define CL_Employee CL<Employee>
#endif
#define EmployeeFile "employs.tmp"
main()
{
CL_Employee cE(CL_ANDS);
cE.ins(new Employee("Doe, John",1000));
Employee E("Small, John",100);
cE.insQNew(&E);
cE.push(new Employee("Morgan, Maria",10000));
cE.save(EmployeeFile);
cE.allClr();
cE.load(EmployeeFile);
cE.sort();
cout << "\nEmployees in alphabetical order: "
<< endl;
while (cE.nextAsg(&E))
cout << E.show() << endl;
return 0;
}
Thus far we have only seen strongly typed checked containers. We could
have just as easily used a type-less container which the next example
demonstrates. However we loose the use of "Asg" and "New" methods
along with persistence. After all, how can a container copy, clone, or
store data that it knows nothing about? Likewise how can it sort and
search un-typed data?
// examp206.cpp -- link with cl.obj
#include "cl.h"
main()
{
cl s;
s.ins("in typeless containers!");
s.ins("Heterogenous data");
s.ins("can be easily bound");
// s.CurNode() == 2
s.insNew("This string won't appear!");
s.sort(CLcmPcast(strcmp,void));
// s.CurNode() == CL_NOTFOUND != 0
while (++s)
cout << (char *)s.get() << endl;
return 0;
}
Notice that "cl" is a typeless container where as CL was used with
templates. The reason the fourth string won't appear is that neither the
CL_NEW flag was set in the constructor call nor was the protected scope
assignD() virtual function overridden during template generation to handle
strings. Notice that the sort() function takes an optional compare function
pointer parameter. The CLcmPcast() macro type casts the standard C
library's strcmp() function pointer to one taking constant void pointer
parameters. Many times when working with non persistent heterogeneous
data it is easiest to play fast and loose with a typeless container.
When the current node is rmv()'ed or del()'ed its predecessor is made the
new current node. For example, if the 5th node is rmv()'ed then
CurNode() returns 4. However, if the current node being 0 is rmv()'ed
then the new current node remains 0 until there are no more nodes and
the current node then becomes undefined, which is not 0! When the
current node is undefined, CurNode() returns the constant
CL_NOTFOUND. The operation of rmv() is designed to perform the
inverse function of ins() which inserts after the current node making the
new node current. An ins() followed by rmv() will leave a list in its
original state.
Sort - Search - Unique
Containers can also be sorted and searched on a default or user supplied
compare function. You don't have to derive a new class for each
different sort order! Only template and "form template" generated
containers have default compare functions (but only if the template
parameter type has implicitly or explicitly defined relational operators or
overrides the template's compare mediator function). If a user defined
compare function is registered it can persist also. Please note that these
methods affect the list's current node setting.
Sorted() unSort() setCmP()
::Register_CmP() CmP() sort()
insSort() insSortNew() insUnique()
insUniqueNew() findFirst() operator[]
findNext() findLast() findPrev()
findAll() tallyAll()
With these methods you can build bags, sets, and even dictionaries which
our next example will demonstrate. One common challenge to any
programmer is to produce an internationalized version of his/her
application. The following string resource suggests one approach. In our
implementation, the first word in a string is the token/key for the
remainder of the string. To change language versions, edit the remainder
of each string. Since all token/key - translation strings are to be located
together the task should be easy.
// examp207.cpp -- link with cl.obj
// #define CL_NO_TEMPLATES
#include <ctype.h>
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM_char
#include "cl.hf"
#endif
class StrReSrc : CL_char {
static unsigned ridx;
static int cmp
(const char * S1, const char * S2);
protected:
virtual voidCmP cmPD(voidCmP cmP)
{
return (cmP? cmP : CLcmPcast(cmp,void));
}
public:
StrReSrc() : CL_char(CL_SAVE) {}
StrReSrc(const char * filename)
: CL_char(defaultConstruct)
{ (void) CL_char::load(filename); }
int load(const char * filename)
{ return CL_char::load(filename); }
int save(const char * filename)
{ return CL_char::save(filename); }
int add(char * S)
{ return (insUnique(S)?1:0); }
const char * operator[](const char * S)
{
return (findFirst(S)?
&(((char *)get())[ridx])
: "not found");
}
CL_char::allClr;
~StrReSrc() { CL_char::destruct(); }
};
unsigned StrReSrc::ridx;
int StrReSrc::cmp(const char * S1, const char * S2)
{
for (ridx = 0; S1[ridx] && S2[ridx]; ridx++)
if (S1[ridx] != S2[ridx])
break;
else if (isspace(S1[ridx]))
// keys match
return 0;
if (!S1[ridx] && S2[ridx] == ' ')
return 0;
return S1[ridx] - S2[ridx];
}
#define StrReSrcFile "spanish.tmp"
main()
{
StrReSrc sr;
sr.add("one uno");
sr.add("two dos");
sr.add("three tres");
sr.add("three wrong");
sr.save(StrReSrcFile);
sr.allClr();
sr.load(StrReSrcFile);
cout << "\nCount to three in Spanish: "
<< endl;
cout << sr["one"] << endl;
cout << sr["two"] << endl;
cout << sr["three"] << endl;
return 0;
}
The (form) template wrapper for strings is used to derive a string
resource. The cmPD() virtual function is then overridden to make our
token/key compare function the default. Notice how the default subscript
operator, i.e. operator[](), is overloaded to allow the token/key to access
the translation/value. Hence we see that a dictionary action is really
defined by the supplied compare function and overloaded access operator.
But recall a dictionary is essentially a set. The add() function defined
here assures us that only unique elements (token/keys) are allowed to
belong to the string resource dictionary (set). To become a bag the add()
function would need to be changed to call insSort() instead of
insUnique(). And to determine the count of a particular bag item one
would call findAll().
Once a container is sorted, it will remain in a sorted state, as far as the
container is concerned, until the compare function is changed or a new
node is added without insSort???() or insUnique???(). However, it is
possible for you to access a node, modifying the key value, without the
container being aware that the sorted order has been spoiled. Be sure to
use UnSort() to let the container know if it is no longer sorted in these
cases.
With other commercial and compiler bundled container libraries, you
would have been forced to derived a new class for each different key.
A rework of examp207.cpp using the ANSI string class can be found in
file examp208.cpp. The file examp209.cpp demonstrates the use of a
Container Lite container to read and modify an MS Windows style
initialization file, e.g. *.ini. A more extensive example that exercises the
find???() family of member functions and alternate compare functions can
be found in examp210.cpp which parses command line parameters.
These file listings are not reproduced here because of space
considerations.
Iterators
You have already seen the Container Lite iterators sprinkled amongst the
previous sections. We'll use this section to list them all in one convenient
place. Notice that Container Lite iterators are integrated methods and
don't involve helper classes.
The following methods utilize a user supplied apply - detect - collect
function, leaving the list's current node setting undisturbed being atomic
in themselves:
forEach() detect() select()
reject() collect()
The following methods affect the list's current node setting:
next() operator++() nextAsg()
prev() operator--() prevAsg()
The following methods affect the list's current node setting, utilizing the
internally stored compare function pointer for matching:
findFirst() operator[] findNext()
findLast() findPrev() findAll()
tallyAll()
Our next example will show off the Smalltalk like iterators. It is a
rework of examp205.cpp so only the pertinent portions are reproduced
here.
// examp211.cpp -- link with cl.obj
// rework of examp205.cpp
// Show off Smalltalk like iterators
...
class Employee {
...
public:
...
Employee * operator()(const char * name)
{
delete this->name;
this->name = (name? strdup(name) : 0);
salary = 0;
return this;
}
static int cmpName(const Employee * E1,
const Employee * E2)
{ return strcmp(E1->name,E2->name); }
static int detectName(Employee * E,
void * name)
{ return (name? !strcmp(E->name,(char*)name) : 0); }
static int detectSalaryGE(Employee * E,
void * salary)
{ return (E->salary >= *(unsigned*)salary); }
static void * collectSalaryGE(Employee *E,
void * salary)
{ return ((E->salary >= (*(unsigned*)salary))?
(void *) new unsigned(E->salary) : 0); }
};
...
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM Employee
#define CL_WELL_ENDOWED
#define CL CL_Employee
#include "cl.hf"
#else
CL_WELL_ENDOWED(Employee)
#define CL_Employee CL<Employee>
#endif
#if defined(CL_NO_TEMPLATES)
#define ITEM unsigned
#define CL_WELL_ENDOWED
#define CL CL_unsigned
#include "cl.hf"
#else
CL_WELL_ENDOWED(unsigned)
#define CL_unsigned CL<unsigned>
#endif
#define EmployeeFile "employs.tmp"
main()
{
CL_Employee cE(CL_ANDS);
cE.ins(new Employee("Doe, John",1000));
Employee E("Mitchell, Allen",100);
cE.insQNew(&E);
cE.push(new Employee("Morgan, Maria",10000));
cE.insQ(new Employee("Zorro", 200));
cE.save(EmployeeFile);
cE.allClr();
cE.load(EmployeeFile);
cE.sort();
cout << "\nEmployees in alphabetical order: "
<< endl;
while (cE.nextAsg(&E))
cout << E.show() << endl;
Employee *Eptr;
unsigned i, salary = 500;
char name[] = "Morgan, Maria";
cout << "\nFind employee: "
<< name << endl;
if (cE.detect(Eptr,Employee::detectName,name))
cout << Eptr->show() << endl;
if (cE.detect(i,Employee::detectName,name))
cout << cE[i]->show() << endl;
cout << "\nFind first employe with salary >= "
<< salary << endl;
if (cE.detect(Eptr,Employee::detectSalaryGE,&salary))
cout << Eptr->show() << endl;
cout << "\nFind all employees with salaries >= "
<< salary << endl;
CL_Employee cE2;
if (cE.select(cE2,Employee::detectSalaryGE,&salary))
while (++cE2)
cout << ((Employee*)cE2)->show() << endl;
cout << "\nList all salaries >= " << salary << endl;
CL_unsigned cs;
if (cE.collect((cl&)cs,Employee::collectSalaryGE,&salary))
while (++cs)
cout << *(unsigned *)cs << endl;
cE2.allClr();
cout << "\nFind all employees with name >= "
<< name << endl;
cE.setCmP(Employee::cmpName);
if (cE.findAll(cE2,E(name),cl::GE))
while (++cE2)
cout << ((Employee*)cE2)->show() << endl;
return 0;
}
Summary
The easiest way to conceptualize Container Lite operations is to classify
its method behaviors into five categories: elastic array, stack-queue-
deque, list, sort-search-unique, and iterator. Every container has an
inherent current node setting associated with its list behavior which is also
modified by its sort-search-unique methods but left undisturbed by the
elastic array and stack-queue-deque methods. Since Container Lite
iterator operations are basically atomic in nature, separate helper classes
are not necessary.
Chapter 3
Strong Type Checking
Container Lite's foundation class is named "cl." The "cl" class used by
itself performs no type checking on the data being bound within. These
typeless containers are useful for quick and dirty handling of
heterogeneous data types. The following code snipet shows a typeless,
i.e. not type checked at compile time, container for your particular data
type.
#include "cl.h"
...
{
cl containerOfYourDataType;
...
However, by using a template the resultant container wrapper class can be
made to impose strong type checking for any type of data at compile
time, thereby restricting the use of that container to a particular data type.
To build a strongly type checked container for your particular data type
you should code something like:
#include "cl.h"
...
{
CL<YourDataType> containerOfYourDataType;
...
Even if your compiler doesn't support templates, strong type checking can
still be achieved with Container Lite's "form templates."
#include "cl.h"
...
#define ITEM YourDataType
#define CL CL_YourDataType
...
{
CL_YourDataType containerOfYourDataType;
...
You have already seen all three approaches in the examples of the
previous chapter. In some of those examples both the template and
"form" template approaches were combined for the sake of contrasting
them.
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM YourDataType
#define CL CL_YourDataType
#include "cl.hf"
#else
#define CL_YourDataType CL<YourDataType>
#endif
...
{
CL_YourDataType containerOfYourDataType;
...
Recalling examp211.cpp we observe that "cl.hf" can be included a
multitude of times to facilitate the "form" template generation of various
wrappers, e.g. CL_Employee and CL_unsigned.
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM Employee
#define CL_WELL_ENDOWED
#define CL CL_Employee
#include "cl.hf"
#else
CL_WELL_ENDOWED(Employee)
#define CL_Employee CL<Employee>
#endif
#if defined(CL_NO_TEMPLATES)
#define ITEM unsigned
#define CL_WELL_ENDOWED
#define CL CL_unsigned
#include "cl.hf"
#else
CL_WELL_ENDOWED(unsigned)
#define CL_unsigned CL<unsigned>
#endif
The typeless container we saw at the beginning of the chapter can not call
a destructor for a node before deleting it because the node's data type
isn't known. This is something you should keep in mind before setting
the CL_DEL flag that enables the "Del" methods and by the way forces
cl::~cl() to call allDel() on any nodes remaining in the container! Both
the template and "form" template container wrapper classes do call a
node's destructor before deleting it since the data type is known.
Additional Functionality
A generic type checked container can not use the "Asg", "New", and
save() methods unless more information about the data type being bound
is known. The methods are simply "turned off" in the generic wrapper
returning a failure condition regardless of the settings of the CL_ASG,
CL_NEW, and CL_SAVE flags.
For example, a container wrapper can be generated that automatically
compares your data within the insUnique(), sort(), and find???() methods
without a user supplied compare function. How can these methods do
this? Only if your data type has an overloaded equality operator, i.e.
int ITEM::operator==(const ITEM&) const;
or
friend int operator==(const ITEM&, const ITEM&);
and an overloaded greater than operator, i.e.
int ITEM::operator>(const ITEM&) const;
or
friend int operator>(const ITEM&, const ITEM&);
and only if the template or form is notified before the wrapper is
generated. For example, if you code:
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM YourDataType
#define CL_EQ_GT_OPS
#define CL CL_YourDataType
#include "cl.hf"
#else
CL_EQ_GT_OPS(YourDataType)
#define CL_YourDataType CL<YourDataType>
#endif
...
{
CL_YourDataType containerOfYourDataType;
...
containerOfYourDataType.sort();
the sort() method can now be successfully called without specifying a
compare function. Of course you can still change the container's internal
compare function at will to facilitate sorting or searching on various keys
but a default compare function is now available when required. This
default compare function is in effect defined by YourDataType's
operator==() and operator>() overloaded functions.
A container wrapper can also be generated so that your data can be
copied on the fly. For example, the nextAsg(&targetObject) iterator will
advance the list's current node pointer to the next node and assign that
node to the target object specified. Your data type must have an
overloaded assignment operator however, i.e.
ITEM& ITEM::operator=(const ITEM&);
You could then code something like:
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM YourDataType
#define CL_ASSIGN_OP
#define CL CL_YourDataType
#include "cl.hf"
#else
CL_ASSIGN_OP(YourDataType)
#define CL_YourDataType CL<YourDataType>
#endif
...
{
CL_YourDataType containerOfYourDataType(CL_ASG);
...
YourDataType targetObject;
for (containerOfYourDataType.setCurNode();
containerOfYourDataType.nextAsg(targetObject);
/* no reinit */ )
targetObject.DoSomething();
...
Notice that the CL_ASG flag must still be set in the constructor call to
enable the "Asg" methods. You can of course call the setFlags(CL_ASG)
member function instead. In the example, the setCurNode() function sets
the list's current node to be undefined so that the first call to nextAsg()
returns a pointer to the container's first node and also copies the first
node's contents to "targetObject" via the overloaded assignment operator.
When nextAsg() falls off the end of the list it returns NULL.
Likewise a container wrapper can be generated so that your data can be
cloned on the fly allowing you to bind the clone instead of the original.
But your data type must have a copy initializer constructor, i.e.
ITEM::ITEM(const ITEM&);
Since the "unsigned" data type has an implicit copy initializer we could
code the following:
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM unsigned
#define CL_COPYINIT
#define CL CL_unsigned
#include "cl.hf"
#else
CL_COPYINIT(unsigned)
#define CL_unsigned CL<unsigned>
#endif
...
{
CL_unsigned containerOfUnsigned(CL_NEW,10);
...
for (unsigned i = 0;
containerOfUnsigned.insQNew(&i);
i++);
...
Now you can use the container's "New" methods provided of course the
CL_NEW flag is set. The insQNew() method first clones the unsigned
variable "i" and then inserts this clone into the queue, binding its address.
The for loop terminates after ten nodes have been queued because the
container is limited to a maximum of ten nodes in the constructor call.
A strongly type checked container can also be made to persist, i.e. stored
on a stream as a file, provided it has
an overloaded stream insertion operator, i.e.
friend ostream& operator<<(ostream&,ITEM&);
an overloaded stream extraction operator, i.e.
friend istream& operator>>(istream&,ITEM&);
and a default constructor, i.e.
ITEM::ITEM();
In this case you could now code the following:
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM YourDataType
#define CL_STRM_OPS
#define CL CL_YourDataType
#include "cl.hf"
#else
CL_STRM_OPS(YourDataType)
#define CL_YourDataType CL<YourDataType>
#endif
...
{
CL_YourDataType containerOfYourDataType(CL_SAVE);
...
// 1.
containerOfYourDataType.save("pathname");
containerOfYourDataType.allClr();
if (containerOfYourDataType.load("pathname"))
// container same state as in 1. above
...
Of course the CL_SAVE flag must be set in order to save the container in
a file. If you have an already opened stream handle you can use the
stream insertion and extraction operators with the container itself instead
of the save() and load() methods. Please don't confuse these operators,
which are automatically generated by the template or form, with those
which you must supply for your data type. If a container has a user
defined compare function set and it has been registered before the
container is saved, it will be automatically restored when the container is
reloaded from the file. Any default compare function is always restored.
Likewise the list's current node setting will persist also.
A Well Endowed Data Type
If your data type has all the overloaded operators and constructors, etc.
outlined above then your data type is well endowed as far as Container
Lite is concerned. A fully featured container, i.e. one with the "Asg",
"New", and save() methods operational along with a default compare
function for the sort-search-unique methods, can then be generated as
shown in the following code.
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM YourDataType
#define CL_WELL_ENDOWED
#define CL CL_YourDataType
#include "cl.hf"
#else
CL_WELL_ENDOWED(YourDataType)
#define CL_YourDataType CL<YourDataType>
#endif
...
{
CL_YourDataType containerOfYourDataType(CL_ANDS);
...
The CL_ANDS flag is actually a combination of flags being defined as
(CL_ASG | CL_NEW | CL_DEL | CL_SAVE) thus the container
constructed above has all its available methods enabled. By the way,
please don't forget if you are always going to have templates available to
you with the compilers you'll be using you can simply code:
CL_WELL_ENDOWED(YourDataType)
...
{
CL<YourDataType> containerOfYourDataType(CL_ANDS);
...
You can see how this works when you consider that the template version
of the CL_WELL_ENDOWED() macro is defined as:
#define CL_WELL_ENDOWED(ITEM) \
CL_EQ_GT_OPS(ITEM) \
CL_ASSIGN_OP(ITEM) \
CL_COPYINIT(ITEM) \
CL_STRM_OPS(ITEM)
The "form" template is a bit more complicated but works essentially the
same way.
Let's review the requisite overloaded operators, etc. for a data type to be
considered well endowed. It must have:
1. an overloaded equality operator, i.e.
int ITEM::operator==(const ITEM&) const;
or
friend int operator==(const ITEM&, const ITEM&);
and an overloaded greater than operator, i.e.
int ITEM::operator>(const ITEM&) const;
or
friend int operator>(const ITEM&, const ITEM&);
2. an overloaded assignment operator, i.e.
ITEM& ITEM::operator=(const ITEM&);
3. a copy initializer constructor, i.e.
ITEM::ITEM(const ITEM&);
4. an overloaded stream insertion operator, i.e.
friend ostream& operator<<(ostream&,ITEM&);
5. an overloaded stream extraction operator, i.e.
friend istream& operator>>(istream&,ITEM&);
and a default constructor, i.e.
ITEM::ITEM();
and
6. a destructor if one is required, i.e.
ITEM::~ITEM();
Now let's look at a well endowed string class example.
// examp301.cpp -- link with cl.obj
// #define CL_NO_TEMPLATES
#include "cl.h"
class String {
char *str;
size_t len;
int cmp(const String& cs) const;
public:
String(const char * s = (const char *)0)
{
str = (s? len = strlen(s), strdup(s)
: (char *)(len = 0));
}
String(const String& s)
{
str = (((len = s.len) != 0)?
strdup(s.str) : (char *)0);
}
~String() { delete str; }
String& operator=(const String& s)
{
delete str;
str = (((len = s.len) != 0)?
strdup(s.str) : (char *)0);
return *this;
}
inline int operator==(const String& cs) const
{ return !cmp(cs); }
inline int operator> (const String& cs) const
{ return (cmp(cs) > 0); }
operator const char *() { return str; }
friend ostream& operator<<
(ostream& os, String& s)
{ return os << &s.str; }
friend istream& operator>>
(istream& is, String& s)
{
is >> &s.str;
s.len = (s.str? strlen(s.str) : 0);
return is;
}
};
int String::cmp(const String& cs) const
{
if (!str)
if (!cs.str)
return 0;
else
return -1;
else
if (!cs.str)
return 1;
else
return strcmp(str,cs.str);
}
#ifdef CL_NO_TEMPLATES
#define ITEM String
#define CL_WELL_ENDOWED
#define CL CL_String
#include "cl.hf"
#else
CL_WELL_ENDOWED(String)
#define CL_String CL<String>
#endif
#define StrFile "strings.tmp"
main()
{
CL_String cS(CL_ANDS);
String s("can be");
cS.insNew(&s);
cS.ins(new String("tamed!"));
cS.insQ(new String("Now strings"));
cS.sort();
cS.save(StrFile);
cS.allClr();
cS.load(StrFile);
while (cS.popDelAsg(&s))
cout << (const char *)s << endl;
return 0;
}
For traditional C/C++ strings, i.e. zero terminated char arrays, there is
a special wrapper already prepared for your use. To use it simply code:
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM_char
#include "cl.hf"
#endif
...
{
CL_char strings;
...
You saw this approach used in examp203.cpp and examp207.cpp. If you
are only using a template supporting compiler the code would be simply:
#include "cl.h"
...
{
CL_char strings;
...
This C/C++ string wrapper does not, however, enable you to use the
"Asg" methods.
Deficient Data Types
What do you do if your data is deficient in one or more of the six
requisites necessary to be considered well endowed yet you need the
additional functionality of the "Asg", "New", and save() methods? If you
don't have the source code, i.e. the class belongs to some commercial
library, you can't just go back and modify the source. The best way is to
derive a new class with the necessary functionality. However, there are
other ways to modify the generated wrapper. If your data is of a fixed
size with no embedded pointers, virtual functions or virtual bases, then
performing bytewise operations can provide a quick and dirty fix. The
next example reworks examp205.cpp to allow bytewise operations to be
meaningful - notice that what was previously a string pointer is now a
fixed size char array.
// examp302.cpp -- link with cl.obj
// #define CL_NO_TEMPLATES
#include "cl.h"
class Employee {
char name[20];
unsigned salary;
static Employee * THIS;
static ostream& SHOW(ostream& os)
{
return os << "Employee name: "
<< setw(20)
<< (THIS->name? THIS->name : "n/a")
<< "\tsalary: " << THIS->salary;
}
public:
Employee(const char * name = 0,
unsigned salary = 0)
{
if (name)
strncpy(this->name,name,
sizeof(Employee::name));
else
this->name[0] = '\0';
this->salary = salary;
}
ostream& (*show())(ostream&)
{ THIS = this; return SHOW; }
};
Employee * Employee::THIS;
#define EmployeeFile "employs.tmp"
#ifdef CL_NO_TEMPLATES
#define ITEM Employee
#define CL_CMP_BYTES
#define CL_ASSIGN_BYTES
#define CL_CLONE_BYTES
#define CL_STRM_BYTES
#define CL CL_Employee
#include "cl.hf"
#else
CL_CMP_BYTES(Employee)
CL_ASSIGN_BYTES(Employee)
CL_CLONE_BYTES(Employee)
CL_STRM_BYTES(Employee)
#define CL_Employee CL<Employee>
#endif
main()
{
CL_Employee cE(CL_ANDS);
cE.ins(new Employee("Doe, John",1000));
Employee E("Small, John",100);
cE.insQNew(&E);
cE.push(new Employee("Morgan, Maria",10000));
cE.save(EmployeeFile);
cE.allClr();
cE.load(EmployeeFile);
cE.sort();
cout << "\nEmployees in alphabetical order: "
<< endl;
while (cE.nextAsg(&E))
cout << E.show() << endl;
return 0;
}
The CL_CMP_BYTES macro causes the template to generate a default
compare function that performs a byte by byte compare of two Employee
objects. If an Employee object had embedded data pointers, the data
pointed to wouldn't be compared, just the pointer values.
The CL_ASSIGN_BYTES macro causes the "Asg" methods to copy
sizeof(Employee) bytes. Again if an Employee object had embedded data
pointers problems would arise, i.e. the target object wouldn't delete those
pointers before overwriting them. After the copying is completed the
target object would end up pointing to the source object's data instead of
a cloned copy. If Employee had a virtual base class, the internal pointer
of the target object would end up pointing to the virtual base of the source
object. Overwriting the virtual function table pointer may or may not
cause complications depending on your compiler's implementation and
options setting (dangerous ground to tread on).
The CL_CLONE_BYTES macro enables the "New" methods to also copy
sizeof(Employee) bytes. Any suballocated data pointed to by the source
object wouldn't get cloned, but rather instead the cloned target and
original source would both end up pointing to the same data again.
The CL_STRM_BYTES macro allows the save() method to write
sizeof(Employee) bytes on a stream. If Employee has any embedded
pointers, i.e. data, virtual base, or virtual function table you are going to
have problems when reloading.
Except for the most simple classes it's best not to use the bytewise
approach. If these restrictions are met and the situation dictates, the
bytewise approach provides an easy fix. You can of course mix the
previous macros, e.g. CL_EQ_GT_OPS, CL_ASSIGN_OP, etc., with any
of the bytewise macros in an unambiguous fashion. For example,
suppose your data type did not have an overloaded assignment operator
but it did have a copy initializer. You could then code something like:
#include "cl.h"
#if defined(CL_NO_TEMPLATES)
#define ITEM YourDataType
#define CL_ASSIGN_BYTES
#define CL_COPYINIT
#define CL CL_YourDataType
#include "cl.hf"
#else
CL_ASSIGN_BYTES(YourDataType)
CL_COPYINIT(YourDataType)
#define CL_YourDataType CL<YourDataType>
#endif
...
{
CL_YourDataType containerOfYourDataType;
YourDataType ydt(???);
...
containerOfYourDataType.pushNew(&ydt);
...
containerOfYourDataType.topAsg(&ydt);
...
There is a safer way to make up for your data's deficiencies which we'll
take a look at next.
Mediator Functions
Container Lite uses inline mediator functions to configure a template or
form to generate a wrapper class with additional functionality, i.e.
meaningful "Asg", "New", "Del", and save() methods, etc.. The
template or form automatically generates inline mediator function
definitions which in turn are used to configure the generated wrapper for
your data type. (The inline mediator functions are specifically used to
define the overloaded virtual functions of the generated wrapper which
you'll see more of in the next chapter.) The macros outlined in the
previous sections cause the appropriate inline mediator functions to be
defined. Instead of using these macros you can also manually override
the mediator functions directly to mitigate the deficiencies of your data
type.
To see one reason you may need to take this approach consider a class
that has overloaded stream operators that were designed to be used for
console display purposes. These would most likely be totally unsuitable
for file operations. If you want a container of these objects to persist you
will have to derive a new class with stream operators designed for file
operations. Again there is a quick and dirty solution if the need arises.
Let's see a rework for a seriously deficient Employee structure.
// examp303.cpp -- link with cl.obj
// #define CL_NO_TEMPLATES
#include "cl.h"
struct Employee {
char *name;
unsigned salary;
Employee(const char * name,
unsigned salary = 0)
{
this->name = (name? strdup(name) : 0);
this->salary = salary;
}
~Employee() { delete name; }
friend ostream& operator<<
(ostream& os, Employee& e)
{
return os << "Employee name: "
<< setw(20)
<< (e.name? e.name : "n/a")
<< "\tsalary: " << e.salary;
}
};
int CL_cmpD(const Employee * E1, const Employee * E2)
{
if (!E1->name)
if (!E2->name)
return 0;
else
return -1;
else
if (!E2->name)
return 1;
else
return strcmp(E1->name,E2->name);
}
inline void * CL_assignD
(Employee * D, const Employee * S,
unsigned /* flags */)
{
delete D->name;
D->name = (S->name?
strdup(S->name) : 0);
D->salary = S->salary;
return D;
}
inline void * CL_newD(const Employee * E)
{
Employee * D = new Employee(0);
if (!D) return 0;
D->name = (E->name? strdup(E->name) : 0);
D->salary = E->salary;
return D;
}
inline void CL_deleteD(Employee * E)
{ delete E->name; E->name = 0; delete E; }
inline ostream& CL_putD(ostream& os, Employee * E)
{
return os << &((*E).name) << endm
<< (*E).salary << endm;
}
inline istream& CL_getD(istream& is, Employee *& E)
{
if (E) delete E;
if ((E = new Employee(0)) != (Employee*)0)
is >> &((*E).name) >> nextm
>> (*E).salary >> nextm;
return is;
}
#if defined(CL_NO_TEMPLATES)
#define ITEM Employee
#define CL_ALL_DEF
// #define CL_CMP_DEF
// #define CL_ASSIGN_DEF
// #define CL_NEW_DEF
// #define CL_DELETE_DEF
// #define CL_STRM_INSERT_DEF
// #define CL_STRM_EXTRACT_DEF
#define CL CL_Employee
#include "cl.hf"
#else
CL_ALL_DEF(Employee)
// CL_CMP_DEF(Employee)
#define CL_Employee CL<Employee>
#endif
#define EmployeeFile "employs.tmp"
main()
{
CL_Employee cE(CL_ANDS);
cE.ins(new Employee("Doe, John",1000));
Employee E("Small, John",100);
cE.insQNew(&E);
cE.push(new Employee("Morgan, Maria",10000));
cE.save(EmployeeFile);
cE.allClr();
cE.load(EmployeeFile);
cE.sort();
cout << "\nEmployees in alphabetical order: "
<< endl;
while (cE.nextAsg(&E))
cout << E << endl;
return 0;
}
Instead of using the macro: we defined the function: and
macro:
CL_EQ_GT_OPS CL_cmpD() CL_CMP_DEF
CL_ASSIGN_OP CL_assignD() CL_ASSIGN_DEF
CL_COPYINIT CL_newD() CL_NEW_DEF
CL_deleteD() CL_DELETE_DEF
CL_STRM_OPS CL_putD() CL_STRM_INSERT_DEF
CL_getD() CL_STRM_EXTRACT_DEF
Notice that CL_cmpD() isn't inlined since we need a proper compare
function to point to. The rest are inlined since they will only be
expanded one time in the overridden virtual functions of the generated
container wrapper. CL_deleteD() isn't really necessary since the
Employee structure already has a destructor. Notice that these overriding
mediator function definitions have to appear before the template
generation! The CL_ALL_DEF macro is equivalent to all of the
commented out CL_???_DEF macros which allow you when used
individually to fine tune a generated wrapper. The template version
(versus the form) only requires the use of the CL_CMP_DEF() macro.
Again, you can mix and match any of the approaches outlined in this
chapter.
Summary
Typeless containers used by themselves perform no type checking and
thus can not be made to persist since nothing is known about the data
bound within. Likewise a typeless container's "Asg" and "New"
methods are non functional. But typeless containers are nevertheless
useful for holding heterogeneous data types in quick and dirty
implementations.
Templates are used to create containers with strong type checking. If
your compiler doesn't yet support templates, "form templates" can be
used in a manner similar to templates. To gain additional container
functionality, i.e. default compare function and meaningful "Asg",
"New", and save() methods you must signal the template or form via the
configuration macros or mediator functions to generate the appropriate
wrapper. Deficiencies in you data types can be accommodated many
times without deriving new classes by simply invoking special bytewise
macros or manually overriding mediator functions.
Strongly typed check containers are used typically to bind homogeneous
data. However, heterogeneous data residing in a polymorphic cluster can
be bound by strongly type checking for a public polymorphic root class.
To learn how to add additional functionality to containers of polymorphic
clusters, see the chapter on polymorphic nodes.
Chapter 4
Protected Scope Virtual
Functions
In the last chapter on strong type checking you saw the ability of
Container Lite templates and forms to adapt a container to various data
types via mediator functions. These inline mediators are automatically
generated by the template or form process unless you directly defined
them yourself. It is also possible, however, to configure a container for
any data type by overriding Container Lite's protected scope virtual
functions in a derived class which is exactly what the template or form
does with the mediators.
Let's rework examp303.cpp this time without mediators or templates,
deriving our own class directly. The CL_Employee class was created by
copying the CL class declaration from cl.hf intact and globally replacing
"CL " with CL_Employee and ITEM with Employee. Let's look at it
piece by piece interspersed with my explanation of what's going on.
(Source is found in examp401.cpp).
class CL_Employee : cl {
protected:
CL_Employee (defaultConstructor) : cl(defaultConstruct) {}
void assign(const CL_Employee& b) { cl::assign(b); }
cl:: destruct;
The default constructor is used by the stream extraction mechanism for
containers. It really isn't a default constructor since it has an unnamed
parameter of type "defaultConstructor." But this prevents any ambiguity
that may arise in a user derived class that supplies it's own real default
constructor. The assign() function is called by the CL_Employee's copy
initializer and overloaded assignment operator. It in turn calls cl::assign.
The importance of this arrangement may not be apparent to you just yet.
Likewise the destruct() function is called by the CL_Employee's
destructor. Destruct() either deletes any remaining nodes in the container
if the CL_DEL flag is set or simply releases them otherwise. Nothing
has been edited thus far.
Now we come to the important stuff!
virtual voidCmP cmPD(voidCmP cmP)
{ return (cmP? cmP : CLcmPcast(Employee::cmp,void)); }
virtual void * assignD(void * D, const void * S)
{
delete ((Employee *)D)->name;
((Employee *)D)->name = (((Employee*)S)->name?
strdup(((Employee *)S)->name) : 0);
((Employee *)D)->salary = ((Employee*)S)->salary;
return D;
}
virtual void * newD(const void * D)
{
Employee * E = new Employee(0);
if (!E) return 0;
E->name = (((Employee *)D)->name?
strdup(((Employee *)D)->name) : 0);
E->salary = ((Employee *)D)->salary;
return E;
}
virtual void deleteD(void * D)
{
delete ((Employee *)D)->name;
((Employee *)D)->name = (char *)0;
delete (Employee *)D;
}
The cmPD() function is coded to return the compare function pointer it is
passed or a pointer to a default compare function if the passed in compare
function pointer is NULL. The CLcmPcast() macro (defined in cl.h) type
casts the cmp function pointer to one that takes void pointer parameters.
In the original cl class everything is a pointer to void until we wrap it in a
template or form generated class or code it ourselves as we are doing
here. Recall those examples in earlier chapters which performed sorts
without a compare function being assigned. They were able to because
the template or form generated an overloaded cmPD() that returned a
default compare function that was in turn generated using the data type's
implicit or explicit operator==() const and operator>() const functions.
The assignD() function in our case performs the assignment of one
Employee to the other. Container Lite methods like nextAsg() or
popAsg() are allowed to call assignD if the CL_ASG flag is set.
Normally templates and forms override assignD() expanding an inline
mediator that calls your data type's overloaded assignment operator.
Recall that you did this by defining your own mediator or (defining)
invoking CL_ASSIGN_OP macro before the (form) template instance was
generated. If you want to inhibit the "Asg" methods simply overload
assignD() to always return NULL. Then regardless of the CL_ASG flag
setting, "Asg" methods are inhibited.
The newD() function is called by Container Lite methods like insQNew(),
but only if the CL_NEW flag is set! If newD() returns NULL the calling
method likewise returns failure.
The deleteD() function is straight forward and is overloaded to call the
correct destructor for the node's data type. The deletion of the name
field isn't really necessary since the Employee's destructor will delete it
for you.
virtual int attachD(void * D)
{ return 1; }
virtual void detachD(void * D)
{ return; }
Actually attachD() and detachD() are used in more sophisticated situations
where nodes may be attached to more than one container at the same time
and need to be signaled to whom or how many they are attached to.
AttachD() must return true (non zero) before a node is allowed to be
bound within a container. This allows your nodes to reject binding on the
fly. Your node has nothing to say about becoming detached however.
virtual int putD(ostream& os, void * D)
{
return ((os << &(((Employee *)D)->name) << endm
<< ((Employee *)D)->salary << endm)? 1 : 0);
}
virtual void * getD(istream& is)
{
Employee * E = new Employee(0);
if (E)
is >> &(E->name) >> nextm
>> E->salary >> nextm;
return E;
}
The putD() and getD() virtual functions are responsible for storing and
recalling your data from stream.
virtual int put(ostream& os)
{ return cl::put(os); }
virtual int get(istream& is)
{ return cl::get(is); }
If the CL_Employee derived class had itself introduced new data
members, these put() and get() functions would be responsible for their
streaming. But they would still call cl::put() and cl::get() to do the actual
streaming of the base class members. If you decide to write the new data
members to the stream before calling cl::put() then you must read these
new members from the stream before calling cl::get()!
Now let's turn our attention to the CL_Employee copy initializer
constructor, assignment operator, and destructor.
CL_Employee (const CL_Employee& b) : cl(defaultConstruct)
{ assign(b); }
CL_Employee& operator=(CL_Employee& b)
{ assign(b); return *this; }
virtual ~CL_Employee() { destruct(); }
Notice that the assign() function is called after the cl and CL_Employee
virtual function tables have been initialized. If we had relied on the cl
copy initializer constructor to copy the nodes it would have done so with
cl::assignD() instead of CL_Employee::assignD(). Likewise, if we let the
cl destructor destroy the nodes then cl::deleteD() would have been called
instead of the CL_Employee::deleteD() that we overloaded. The base
class is always constructed first and destructed last. During construction-
destruction, the virtual functions are always initialized to the class level
defaults of the currently executing constructor-destructor. Now you can
see that having the assign() and destruct() functions provides for easier
coding of the copy constructor and virtual destructor.
The main() function of examp401.cpp is identical to examp303.cpp thus
when you run this example you will see it produce exactly the same
results as examp303.cpp.
Summary
In the previous chapters you were exposed to container methods with
"Asg", "New", and "Del" suffixes and "del" within their names. Recall
that these methods performed on the fly assigning, cloning, and deleting
of your data types, respectively, but otherwise they worked essentially the
same as their namesake counterparts. Likewise you saw sort and search
operations as well as persistence. These container methods are able to
adapt to different types of data via the protected scope virtual function
hooks that either (form) template generation overrides via inline mediator
functions and/or modifying macros or you as the programmer override
directly in your own class derived from a Container Lite base class.
Chapter 5
Polymorphic Nodes
Thus far we have only considered binding homogeneous data. While
there is no reason why we can't bind heterogeneous data, it remains
difficult to provide the full spectrum of container services, i.e. "additional
functionality", owing to the complexities of having to provide a common
assignment operator, copy initializer, stream operators, etcetera. Of
course if this additional functionality isn't required then a generic type
checked, template or form generated, wrapper class suffices nicely.
In order for you to more readily construct polymorphic clusters of
heterogeneous data the pitem.h file declares two classes, Streamable and
Mutual, either of which you can use as the polymorphic root for your
family cluster of classes. Streamable forms the foundation of any
persistent cluster with run time typing, compatible assignment, cloning,
and stream processing of the various cluster members. Mutual is itself
derived from Streamable and additionally provides for multiple reference
arbitration in RAM as well as automatic reference resolution during
streaming operations. For example if an object with multiple references
is saved to stream only one copy is saved. Upon reloading the multiple
links are automatically reconstructed.
You can use either Streamable or Mutual in conjunction with a container
to form polymorphic tree and graph structures. Use the pitem.cbk file to
cookbook your derived classes. It is fully commented with step by step
instructions.
The remainder of this chapter will be concerned with using pitem.cbk to
build a polymorphic persistent cluster of geometric shapes. A file entitled
gconsole.cpp is used in an attempt to provide a device independent
graphical interface to the examples. It has been set up to work with
Borland's BGI and the Microsoft graphical run time library as back ends.
It shouldn't prove too difficult to add your own back end to gconsole.cpp
if a different one is required. (I don't mean to endorse any platform or
product. These were the only two environments available to me as I
wrote the examples for this chapter.)
We'll start off with a Shape class that provides X and Y coordinates. We
will want to eventually derive Circle, Rectangle, and Segment (a
container of Shapes itself). We'll proceed this first time in the standard
fashion without pitem.cbk as follows.
// examp501.cpp -- link with cl.obj and graphics.lib.
//#define CL_NO_TEMPLATES
#include "cl.h" // endm, nextm
#define BGI_PATHNAME "\\bc4\\bgi"
#include "gconsole.cpp"
class Shape {
int x, y;
friend ostream& operator<<(ostream&,Shape&);
friend istream& operator>>(istream&,Shape&);
public:
// default constructor required for stream
// extraction
Shape (const Shape& s)
{ x = s.x; y = s.y; }
Shape (int x = GETRANDX(),
int y = GETRANDY())
{ this->x = x; this->y = y; }
int getx() { return x; }
int gety() { return y; }
Shape& setxy(int x = GETRANDX(),
int y = GETRANDY())
{ this->x = x; this->y = y;
return *this; }
virtual void show(int color = GETRANDCOLOR(),
int xxpose = 0, int yxpose = 0,
int scale = 1)
{ PUTPIX(x,y,color); }
virtual char * name()
{ return "shape (pixel)"; }
virtual ~Shape() {}
};
// Shape stream insertion/extraction operators
inline ostream& operator<<(ostream& os, Shape& s)
{ return os << s.x << endm << s.y; }
inline istream& operator>>(istream& is, Shape& s)
{ return is >> s.x >> nextm >> s.y; }
#ifdef CL_NO_TEMPLATES
#define ITEM Shape
#define CL_CLONE_BYTES
#define CL_STRM_OPS
#define CL Shapes
#include "cl.hf"
#else
CL_CLONE_BYTES(Shape)
CL_STRM_OPS(Shape)
#define Shapes CL<Shape>
#endif
#define ShapesFile "shapes.tmp"
main()
{
openGraphics();
Shapes sb(CL_NEW | CL_DEL |
CL_SAVE,1000);
Shape s;
while (sb.insQNew(&s.setxy()));
sb.save(ShapesFile);
sb.allDel();
sb.load(ShapesFile);
while (++sb) sb.get()->show();
cout << "Press <enter> to exit" << endl;
(void) cin.get();
closeGraphics();
return 0;
}
Plain Shapes appear as pixels. But how can we add Circle to our
polymorphic cluster of Shapes? How are the persistence virtual functions,
putD() and getD(), going to differentiate between a simple Shape and a
Circle? We could devise a way for the Shape to have a virtual put(). But
how do you code a virtual get() for an object that isn't yet got!
These problems have already been solved for you as will be demonstrated
in the next two examples. In the first of these next two examples we start
over again with Shape derived from Streamable. Remember this isn't as
difficult as it may at first appear since this new Shape was coded by
cloning the pitem.cbk file and following the step by step instructions.
Essentially pitem.cbk and Streamable does what you would eventually
discover for yourself needed to be done to make a polymorphic cluster
persistent. As you gain more experience be sure to get into the pitem.cpp
code to see what's going on. Let's see the remake of examp501 now.
// examp502.cpp
// compile: bcc -DCL_NEW_EXCEPTION_MASK
// examp502 cl pitem graphics.lib
#include "pitem.h"
#define BGI_PATHNAME "\\bc4\\bgi"
#include "gconsole.cpp"
#define ID_Shape 1U
class Shape : public Streamable {
int x, y;
void init(
// Shape level initializers with
// defaults provided for each
int x = GETRANDX(),
int y = GETRANDY()
)
{ this->x = x; this->y = y; }
protected:
void assign(const Shape& s)
{ Streamable::assign
(*(const Streamable *)&s);
x = s.x; y = s.y; }
Shape(defaultConstructor)
: Streamable(defaultConstruct)
{ init(); }
virtual int put(ostream& os);
int get(istream& is);
static StreamablE extract(istream& is);
public:
static int register_Class()
{ return clasSv.regClass
(Shape::extract,ID_Shape); }
Shape(const Shape& s) : Streamable(defaultConstruct)
{ init(); assign(s); }
Shape(int x = GETRANDX(),
int y = GETRANDY())
: Streamable()
{ init(x,y); }
virtual int operator=(const Streamable& s);
virtual StreamablE clone() const
{ return (StreamablE) new Shape(*this); }
virtual unsigned ID() const
{ return ID_Shape; }
int getx() { return x; }
int gety() { return y; }
Shape& setxy(int x = GETRANDX(),
int y = GETRANDY())
{ this->x = x; this->y = y;
return *this;}
virtual void show(int color = GETRANDCOLOR(),
int xxpose = 0, int yxpose = 0,
int scale = 1)
{ PUTPIX(x,y,color); }
virtual char * name()
{ return "shape (pixel)"; }
virtual ~Shape() {}
}; /* class Shape */
int Shape::put(ostream& os)
{
// if (Streamable::put(os)) {
os << x << endm << y << endm;
if (os)
return 1; // success
// }
return 0;
}
int Shape::get(istream& is)
{
// if (Streamable::get(is)) {
is >> x >> nextm >> y >> nextm;
if (is)
return 1;
// }
return 0;
}
StreamablE Shape::extract(istream& is)
{
Shape* S;
S = new Shape(defaultConstruct);
if (S) if (S->get(is))
return (StreamablE) S;
else
delete S;
return StreamablE0;
}
int Shape::operator=(const Streamable& s)
{
if (this->ID() == s.ID()) {
assign(*(const Shape *)&s);
return 1;
}
return 0;
}
#if defined(CL_NO_TEMPLATES)
#define CL_ALL_DEF
CL_PITEM(Shape,Streamable)
#define ITEM Shape
#define CL Shapes
#include "cl.hf"
#else
CL_PITEM(Shape,Streamable)
#define Shapes CL<Shape>
#endif
#define ShapesFile "shapes.tmp"
main()
{
openGraphics();
Shapes sb(CL_NEW | CL_DEL |
CL_SAVE,1000);
Shape s;
while (sb.insQNew(&s.setxy()));
Register_Class(Shape);
sb.save(ShapesFile);
sb.allDel();
sb.load(ShapesFile);
while (++sb) sb.get()->show();
cout << "Press <enter> to exit" << endl;
(void) cin.get();
closeGraphics();
return 0;
}
Notice that Shape has a virtual ID() function. This id is used among
other things to distinguish a Shape object from let's say a Circle or
Rectangle. Shape also has a virtual put() function so that a Circle or
Rectangle can override it to save any pertinent data particular to its class.
Each overridden put() function is only responsible for its class level
members and calls its base class put() to deal with base class members.
Likewise get() only deals with the current level and relies on its base class
get() to process the base level. Perhaps the most important aspect of the
overall design is the extract() function. Each class derived from a
Streamable hierarchy must provide a static extract() function that acts as a
virtual initialize-from-stream constructor. Each time a Streamable derived
object is encountered on stream, its unique id is extracted and used to
look up the object's extract() function that must have been previously
registered with the Class Registry. This extractor will default construct
the appropriate object as required chaining to the base default constructors
and then start the ball rolling by calling the object's top level get()
function. Depending on the order that put() stores data on a stream, get()
reverses the process loading base class data before or after its own, which
ever is appropriate.
Study this example carefully. Notice how templates and forms handle
classes derived from Streamable. The CL_PITEM() macro configures the
template or form via mediator functions that handle Streamable or
Mutual instances, e.g. CL_assignD() invokes the virtual function int
operator=() and tests to see if the assignment succeeded, while
CL_newD() invokes the virtual function StreamablE clone(). Notice the
call to Register_Class() before streaming. Now we are ready to extend
our example to incorporate circles and rectangles. Though both Circle
and Rectangle are derived from Shape, pitem.cbk can still be used to
generate their code. The global replacement of PITEM_STRM_BASE in
the cloned pitem.cbk file proceeds with Shape this time instead of
Streamable. Only the highlights of examp503.cpp are shown here.
// examp503.cpp
// link with cl.obj, pitem.obj and graphics.lib.
...
#define ID_Circle 2U
class Circle : public Shape {
protected:
int radius;
private:
void init(int radius = GETRANDY()/8+1)
{ this->radius = radius; }
protected:
void assign(const Circle& c)
{ Shape::assign(*(const Shape *)&c);
radius = c.radius; }
Circle(defaultConstructor)
: Shape(defaultConstruct)
{ init(); }
virtual int put(ostream& os);
int get(istream& is);
static StreamablE extract(istream& is);
public:
static int register_Class()
{ return clasSv.regClass
(Circle::extract,ID_Circle); }
Circle(const Circle& s) : Shape(defaultConstruct)
{ init(); assign(s); }
Circle(int radius = GETRANDY()/8+1,
int x = GETRANDX(),
int y = GETRANDY())
: Shape(x,y)
{ init(radius); }
virtual unsigned ID() const
{ return ID_Circle; }
virtual void show(int color, int xxpose,
int yxpose, int scale)
{
SETCOLOR(color);
CIRCLE(getx()+xxpose,
gety()+yxpose,
(radius*scale));
}
virtual char * name() { return "circle"; }
virtual ~Circle() {}
}; /* class Circle */
int Circle::put(ostream& os)
{
if (Shape::put(os)) {
os << radius << endm;
if (os)
return 1; // success
}
return 0;
}
int Circle::get(istream& is)
{
if (Shape::get(is)) {
is >> radius >> nextm;
if (is)
return 1;
}
return 0;
}
StreamablE Circle::extract(istream& is)
{
Circle* S;
S = new Circle(defaultConstruct);
if (S) if (S->get(is))
return (StreamablE) S;
else
delete S;
return StreamablE0;
}
...
// Shape polymorphic cluster processing
// remains outside compiled cluster library
// to allow cluster extensibility.
Shape * Shape::newShape()
{
int ID = random(ID_Rectangle-ID_Shape+1)
+ ID_Shape;
switch (ID) {
case ID_Shape: return new Shape();
case ID_Circle: return new Circle();
case ID_Rectangle: return new Rectangle();
// add new cluster members here:
default: return (Shape *)0;
}
}
void Shape::TallyShowApply(Shape * S, va_list args)
{
int xxpose = va_arg(args,int);
int yxpose = va_arg(args,int);
int scale = va_arg(args,int);
int * shapes = va_arg(args,int *);
int * circles = va_arg(args,int *);
int * rectangles = va_arg(args,int *);
// add new cluster members here:
switch (S->ID()) {
case ID_Shape: ++*shapes; break;
case ID_Circle: ++*circles; break;
case ID_Rectangle: ++*rectangles;break;
// and here:
}
S->show(GETRANDCOLOR(),xxpose,yxpose,scale);
}
#define ShapesFile "shapes.tmp"
main()
{
openGraphics();
Shapes sb(CL_ANDS,100);
Shape *S;
while (sb.insQ(S = Shape::newShape()));
delete S;
Register_Class(Shape);
Register_Class(Circle);
Register_Class(Rectangle);
sb.save(ShapesFile);
sb.allDel();
sb.load(ShapesFile);
int shapes = 0, circles = 0, rectangles = 0;
sb.forEach(Shape::TallyShowApply,0,0,1,
&shapes,&circles,&rectangles);
cout << "Shapes (pixels): " << shapes
<< " cirlces: " << circles
<< " rectangles: " << rectangles
<< endl;
S = Shape::newShape();
unsigned i = sb.tallyAll(S);
cout << "A total of " << i << " "
<< S->name() << "(s)"
<< " were found"
<< endl;
delete S;
cout << "Press <enter> to exit" << endl;
(void) cin.get();
closeGraphics();
return 0;
}
Okay, we have succeeded with a container of polymorphic Shapes that's
persistent! Let's push on to a tree structure, namely adding the Segment
shape. A Segment is itself a Shape and a container of Shapes. Pitem.cbk
again comes to the rescue, generating sublists. Excerpts of examp504.cpp
are shown next.
// examp504.cpp
...
#define ID_Segment 4U
#define SGM_MAXSHAPES 20
class Segment : public Shape
, public Shapes // container of Shape
{
void init() {}
protected:
void assign(const Segment& i)
{ Shape::assign(*(const Shape *)&i);
Shapes::assign(*(const Shapes *)&i);
}
Segment(defaultConstructor)
: Shape(defaultConstruct)
, Shapes(defaultConstruct)
{ init(); }
virtual int put(ostream& os);
int get(istream& is);
static StreamablE extract(istream& is);
public:
static int register_Class()
{ return clasSv.regClass
(Segment::extract,ID_Segment); }
Segment(const Segment& s)
: Shape(defaultConstruct)
, Shapes(defaultConstruct)
{ init(); assign(s); }
Segment (int x = 0, int y = 0,
unsigned flags = CL_ANDS,
unsigned maxNodes = SGM_MAXSHAPES,
unsigned limit = CL_LIMIT,
unsigned delta = CL_DELTA
) : Shape (x,y)
, Shapes (flags,maxNodes,limit,delta)
{ init(); }
virtual unsigned restream();
virtual unsigned ID() const
{ return ID_Segment; }
virtual void show(int color, int xxpose,
int yxpose, int scale);
virtual char * name() { return "segment"; }
virtual ~Segment() {}
}; /* class Segment */
int Segment::put(ostream& os)
{
if (Shape::put(os))
return Shapes::put(os);
return 0;
}
int Segment::get(istream& is)
{
if (Shape::get(is))
return Shapes::get(is);
return 0;
}
StreamablE Segment::extract(istream& is)
{
Segment * S;
S = new Segment(defaultConstruct);
if (S) if (S->get(is))
return (StreamablE) S;
else
delete S;
return StreamablE0;
}
unsigned Segment::restream()
{
unsigned underFlow = 0;
for (unsigned i = 0; i < Shapes::Nodes(); i++)
underFlow += (atGet(i)->restream());
return (underFlow + Shape::restream());
}
#pragma argsused
void Segment::show(int color, int xxpose, int yxpose,
int scale)
{
for (unsigned i = 0; i < Nodes(); i++)
atGet(i)->show(color,getx()+xxpose,
gety()+yxpose,scale);
}
// Shape polymorphic cluster processing
// remains outside compiled cluster library
// to allow cluster extensibility.
Shape * Shape::newSimpleShape(int ID)
{
switch (ID) {
case ID_Shape: return new Shape();
case ID_Circle: return new Circle();
case ID_Rectangle: return new Rectangle();
// add new cluster members here:
default: return (Shape *)0;
}
}
Shape * Shape::newShape()
{
int ID = random(ID_Segment-ID_Shape+1) + ID_Shape;
if (ID == ID_Segment) {
Segment * S = new Segment();
if (S)
while (S->insQ(Shape::
newSimpleShape(random
(ID_Rectangle-ID_Shape+1)+ID_Shape)));
return S;
}
return newSimpleShape(ID);
}
void Shape::TallyShowApply(Shape * S, va_list args)
{
int color = va_arg(args,int);
int xxpose = va_arg(args,int);
int yxpose = va_arg(args,int);
int scale = va_arg(args,int);
int * shapes = va_arg(args,int *);
int * circles = va_arg(args,int *);
int * rectangles = va_arg(args,int *);
int * segments = va_arg(args,int *);
// add new cluster members here:
switch (S->ID()) {
case ID_Shape: ++*shapes; break;
case ID_Circle: ++*circles; break;
case ID_Rectangle: ++*rectangles;break;
case ID_Segment: ++*segments; break;
// and here:
}
if (S->ID() == ID_Segment)
((Segment *)S)->forEach(Shape::
TallyShowApply,GETRANDCOLOR(),
S->getx(),S->gety(),scale,
shapes,circles,
rectangles,segments);
else
S->show(color,xxpose,yxpose,scale);
}
#define ShapesFile "shapes.txt"
main()
{
openGraphics();
Shapes sb(CL_ANDS,20);
Shape *S;
while (sb.insQ(S = Shape::newShape()));
delete S;
Register_Class(Shape);
Register_Class(Circle);
Register_Class(Rectangle);
Register_Class(Segment);
sb.save(ShapesFile);
sb.allDel();
sb.load(ShapesFile);
int shapes = 0, circles = 0, rectangles = 0;
int segments = 0;
sb.forEach(Shape::TallyShowApply,
GETRANDCOLOR(),0,0,1,
&shapes,&circles,
&rectangles,&segments);
cout << "Shapes (pixels): " << shapes
<< " circles: " << circles
<< " rectangles: " << rectangles
<< " segments: " << segments
<< endl;
S = Shape::newShape();
unsigned i = sb.findAll(S);
cout << "A total of " << i << " "
<< S->name() << "(s)"
<< " were found at the first level"
<< endl;
delete S;
cout << "Press <enter> to exit" << endl;
(void) cin.get();
closeGraphics();
return 0;
}
Besides Shapes (pixels), Circles, and Rectangles, we are able to display
Segments of Shapes, Circles, and Rectangles. The TallyShowApply()
function used in conjunction with the container's forEach() iterator
provides an excellent example of how to pass variable argument lists to
multiple sub-levels.
The power of a real graphics subsystem lies in the ability of graphic
segments to be multiply referenced while citing different scales and
transpositions. The next example is a remake of examp504 rooting Shape
in a Mutual base instead of a Streamable one. This leads us to create yet
an additional new Shape called SegRef for Segment Reference. I
generated examp505.cpp by modifying examp504.cpp so that all
Streamable references were replaced with Mutual. I could have used
pitem.cbk as well. Such is the case with SegRef and PieSlice, yet another
Shape. Here is a snippet of examp505.cpp.
// examp505.cpp
...
#define ID_SegRef 6U
class SegRef : public Shape
{
Segment * S;
void discardS();
void init(Segment * S = 0);
protected:
void assign(const SegRef& i);
SegRef(defaultConstructor)
: Shape(defaultConstruct)
{ init(); }
virtual int put(ostream& os);
int get(istream& is);
static MutuaL extract(istream& is);
public:
static int register_Class()
{ return clasSv.regClass
(SegRef::extract,ID_SegRef); }
SegRef(const SegRef& s) : Shape(defaultConstruct)
{ init(); assign(s); }
SegRef(Segment * S = 0,
int x = GETRANDX(),
int y = GETRANDY())
: Shape(x,y)
{ init(S); }
virtual unsigned restream();
virtual StreamablE clone() const
{ return (StreamablE)
new SegRef(*this); }
virtual unsigned ID() const
{ return ID_SegRef; }
virtual void show(int color = GETRANDCOLOR(),
int xxpose = 0, int yxpose = 0,
int scale = 1)
{ if (S) S->show(color,getx()+xxpose,
gety()+yxpose,scale); }
virtual char * name()
{ return "segment reference"; }
virtual ~SegRef() { discardS(); }
}; /* class SegRef */
void SegRef::discardS()
{
if (S) {
S->unlink(this);
if (!S->RefCount())
delete S;
S = 0;
}
}
void SegRef::init(Segment * S)
{
this->S = 0;
if (S) if (S->link(this))
this->S = S;
}
void SegRef::assign(const SegRef& i)
{
Shape::assign(*(const Shape *)&i);
discardS();
init(i.S);
}
int SegRef::put(ostream& os)
{
if (Shape::put(os)) {
int Sok;
os << (Sok = (S != 0)) << endm;
if (Sok)
os << *(MutuaL)S << endm;
if (os)
return 1;
}
return 0;
}
int SegRef::get(istream& is)
{
if (Shape::get(is)) {
int Sok;
is >> Sok >> nextm;
if (Sok) {
MutuaL M;
is >> M >> nextm;
S = (Segment *)M;
if (S)
S->link(this);
}
if (is)
return 1;
}
return 0;
}
MutuaL SegRef::extract(istream& is)
{
SegRef * S;
S = new SegRef(defaultConstruct);
if (S) if (S->get(is))
return (MutuaL) S;
else
delete S;
return MutuaL0;
}
unsigned SegRef::restream()
{
unsigned underFlow = 0U;
if (S) if (S->StreamPos())
underFlow = S->restream();
return (underFlow + Shape::restream());
}
#define ShapesFile "shapes.txt"
main()
{
openGraphics();
Shapes sb(CL_ANDS,5);
Segment * S = new Segment();
if (!S) {
closeGraphics();
return 1;
}
*S
<< new PieSlice(60,300,
GETMAXY()/16,0,0)
<< new Circle(GETMAXY()/8,0,0)
<< new Rectangle(GETMAXX()/8,
GETMAXY()/8,0,0);
SegRef * SR = new SegRef(S);
if (!SR) {
delete S;
closeGraphics();
return 1;
}
sb << SR;
while (sb.insQ(SR = new SegRef(S)));
delete SR;
Register_Class(Shape);
Register_Class(Circle);
Register_Class(Rectangle);
Register_Class(PieSlice);
Register_Class(Segment);
Register_Class(SegRef);
sb.save(ShapesFile);
for (sb.setCurNode(); ++sb; sb.get()->restream());
sb.allDel();
sb.load(ShapesFile);
Restream();
while (++sb)
sb.get()->show(GETRANDCOLOR());
cout << "Press <enter> to exit" << endl;
(void) cin.get();
closeGraphics();
return 0;
}
You'll notice that the example code didn't have to worry about multiple
reference resolution or reconstruction. It was all done transparently
behind the scenes in pitem.cpp. But I did have to insure that SegReg
signals its Segment each time it was linked or unlinked from its SegRef
(see discardS(), init(), and get() above). Segment's inherited Shapes
container performs un/linking on all its nodes (Shapes) automatically. If
you want to save a Mutual network again on stream you must call
restream() for each Mutual derived object. The for loop immediately
following sb.save() demonstrates this though it isn't necessary in our
example since we don't save the shapes more than once. The restream()
function zeros the streamPos and streamCount members of a Mutual
derived object. If pitem.cpp was compiled with MUTUAL_DEBUG
defined then any discrepancies between refCount and the number of times
the object was "streamed", i.e. streamCount, is reported on cerr. In any
case this underflow tally is return by restream(). Take a moment to
review SegRef::restream() since it is slightly different than the pitem.cbk
format suggests. Remember that more than one SegRef object can be
linked to a Segment object. Once restream() is called for the Segment its
streamPos is zeroed. SegRef::restream() only calls Segment::restream() if
it hasn't been called already. The call to Restream() in main() is a
slightly different matter. It is done after loading to insure the Mutual
holding pen data base is cleared. This Restream() is a macro that calls
MutualHoldingPen::restream() for you. If pitem.cpp is compiled with
MUTUAL_DEBUG defined then calling Restream() reports any multiple
reference reconstruction discrepancies on cerr.
Summary
You'll most likely never use the Container Lite tool at this level of
complexity, but it's there if you ever need it. Remember pitem.cpp
processes polymorphic persistent nodes for you. Use the Streamable class
for basic polymorphic clusters and Mutual if you need multiple reference
handling. Use pitem.cbk as a boiler plate template to generate your
derived classes by following the step by step instructions in its comments.
Chapter 6
Reference
Type-less Container
A container is an elastic array of void pointers having the added behaviors
of stack, queue, deque, list, etcetera. To achieve strong type checking
use templates or forms (see chapter 3). Strongly typed prototypes read
the same as shown here for a typeless container except that void pointers,
i.e. void *, are replaced with pointers to your specified data types.
Internals
Functions, private:
void init(unsigned flags = 0U,
unsigned maxNodes = 0U,
unsigned limit = 0U,
unsigned delta = 0U);
The init() initializer is called by all the container constructors thus
guaranteeing consistency.
Members, protected:
unsigned lowLimit, lowThreshold, first;
void *V linkS;
unsigned limit, delta, nodes;
unsigned maxNodes, curNode, flags;
CLcmP cmP;
The value of "limit" is the current size of the array pointed to by
"linkS." The value of "delta" is how much the array will grow
upon overflow. "Nodes" is the number of occupied cells in the
array. "MaxNodes" is the upper bound on the growth of limit.
"LowTheshold" is the trigger for contraction. When the number
of logically occupied cells in the container becomes equal to
"lowThreshold" the physical array is compacted to a new limit of
"lowLimit." "First" maps the container's logical cell zero into a
starting cell position in the physical array pointed to by "linkS."
"CurNode" is the current node associated with the container's list
behavior. When no nodes are present "linkS" remains NULL.
"Flags" has the following bit definitions.
#define CL_SORTED 0x0001U
#define CL_BIND_ONLY 0x0000U
#define CL_ASG 0x0002U
#define CL_READ 0x0004U
#define CL_READ_DEL 0x0008U
#define CL_NEW 0x0010U
#define CL_NEWED 0x0020U
#define CL_DEL 0x0040U
#define CL_SAVE 0x0080U
#define CL_ALL_FLAGS 0x00FFU
#define CL_ANDS (CL_ASG|CL_NEW|\
CL_DEL|CL_SAVE)
The CL_SORTED flag indicates whether the container is still
sorted since the last sort operation. CL_BIND_ONLY is the
absence of CL_ASG, CL_NEW, CL_DEL, and CL_SAVE flags.
CL_ASG enables methods ending with the "Asg" suffix. The
CL_READ flag is set during an "Asg" operation that copies data
from a node to an external location. An overloaded assignD()
virtual function or the CL_assignD() mediator function can test
this flag if required. Likewise the CL_READ_DEL flag is set
instead of the CL_READ flag during a "DelAsg" operation.
CL_NEW enables the methods ending with the "New" suffix.
CL_NEWED is set if ever newD() is invoked by a "New" method.
Use it to detect memory leaks, e.g. a non self deleting container.
CL_DEL enables the methods with "Del" or "del" in their names.
It also causes the destructor to call allDel() instead of allRmv().
CL_SAVE enables save() and the overloaded stream insertion
operator.
The cmP variable holds the current compare function pointer.
Functions, protected:
cl (defaultConstructor) { init(); }
This pseudo default constructor provides a mechanism for
stream loader functions to construct an object's hierarchy
without initializing it. The parameter is a dummy to
present a style for latter extensibility that will insure that an
unambiguous default constructor is available.
void assign(const cl& b);
Assigns the state (maxNodes, flags, cmP, etc.) of container
b to the current container after disposing of any nodes.
The nodes of container b are cloned and these clones are
bound within the current container. Assign() is meant to be
called by a derived class' assign(). In this manner a copy
initializer constructor of the derived class calls its container
base class default constructor and calls its own assign()
which in turn calls cl::assign(). By taking this approach we
insure that DerivedFromCL:: assignD() and
DerivedFromCL:: attachD() are called in the copy
initializer constructor instead of cl:: assignD() and cl::
attachD() which would have been the case if the derived
class called a container copy initializer constructor. See the
copy constructor definition in cl.hpt/hpf for proper use.
Also see notes on destruct() to further your understanding
of this phenomenon.
void destruct();
You must override the container destructor in any
descendant that overrides deleteD() or detachD(), i.e.
virtual ~DerivedFromCL()
{ cl::destruct(); }
This is because the base destructors are called after virtual
function tables are reset to their default values for the base
level. This means that cl::~cl() calls cl::detachD() and
perhaps cl::deleteD() instead of the intended
DerivedFromCL:: detachD() or DerivedFromCL::
deleteD(). By performing the actual destruction in the
destruct() function the overridden destructor can be simply
coded inline as shown.
virtual voidCmP cmPD(voidCmP cmP) { return cmP; }
This function is designed to return cmP or a default
compare function pointer if cmP is NULL. The template
and form instances override cmPD() in such a manner.
You can also overload it yourself (see examp207.cpp). The
mediator function CL_cmpD() can also be user defined to
customize template generation of an overloaded version of
this function for data types lacking relational operators (see
examp303.cpp).
virtual void * assignD(void * D, const void * S)
{ return (void *) 0; }
Called by atPutAsg(), atGetAsg(), popAsg(), etcetera, to
copy data between a node and an external object. Template
and form instances override assignD() redefining it to call
the operator=() function for your data type. It is
guaranteed that assignD() is never called unless the
CL_ASG flag is set and the parameters are not NULL!
The first parameter is the destination while the second is
the source. If assignD() returns NULL the calling method
gracefully fails leaving the container in an unaltered state.
If assignD() is being called from a reading method such as
atGetAsg() versus atPutAsg(), the CL_READ flag is set. If
assignD() is being called from a reading deletion method
such as atDelAsg() then CL_READ_DEL flag is set
instead. This allows your overloaded version to known the
exact conditions of the assignment should that become
necessary to your application. For example, if
CL_READ_DEL is set you may want to copy just the
pointers to suballocated memory from the node about to be
deleted instead of making a duplicate and then turn around
and let the container destroy the originals.
A quick look at cl.h/hf reveals an overloaded assignD() that
calls the mediator function CL_assignD(). CL_assignD() is
automatically (form) template inline generated unless
manually overridden (see examp303.cpp).
virtual void * newD(const void *) { return (void *) 0; }
Called by atInsNew(), pushNew(), atPutNew(), etcetera, to
clone the data given as the parameter. Template and form
instances override newD() redefining it to call the copy
initializer constructor for your data type. It is guaranteed
that newD() is never called unless the CL_NEW flag is set
and the parameter is not NULL! If newD() returns NULL
the calling method gracefully fails leaving the container in
an unaltered state.
A quick look at cl.h/hf reveals an overloaded newD() that
calls the mediator function CL_newD(). CL_newD() is
automatically (form) template inline generated unless
manually overridden (see examp303.cpp).
virtual void deleteD(void * D) { delete (void*) D; }
Called by atDel(), allDel(), popDel(), del(), etcetera, to
delete container nodes as they are removed from the
container. Template and form instances override deleteD()
redefining it to call the appropriate destructor for your data
type via the automatically inline generated CL_deleteD().
It is guaranteed that deleteD() is never called unless the
CL_DEL flag is set and the D parameter is not NULL. As
a side note, containers of Streamable/Mutual nodes carry
out the deletion of nodes only when their refCount's are
zero.
virtual int attachD(void *) { return 1; }
Called by any container method that attempts to bind data,
e.g. atIns(), push(), etcetera. This allows the data to
become aware of the binding process by overriding
attachD(). The void * parameter is a pointer to the data
about to be bound and is guaranteed never to be NULL! If
and only if the data refuses to attach itself to the container
should zero be returned. Any overriding function should
only link itself to the container and should not use the
implicit "this" pointer to access the container within the
overriding function. This restriction applies because
container data may be in a transition state when attachD() is
called. It is however permissible to store the "this" pointer
within the data being bound for later use in accessing
container members. Template and form instances do not
override this function except for Streamable/Mutual derived
data types which make use of this function allowing a
container to signal node linkage.
virtual void detachD(void *) { return; }
detachD() is called by any container method that attempts
to unbind data, e.g. atDel(), pop(), etcetera. The void *
parameter is never NULL! Once called, the node must
consider itself detached from the container! Template and
form instances do not override this function except for
Streamable/Mutual derived data types which make use of
this function allowing a container to signal released nodes.
virtual int putD(ostream&, void *) { return 0; }
putD() is called for each node after the container's header
is stored on the stream. The void * parameter is
guaranteed never to be NULL. Template and form
instances override this function redefining it to invoke the
overloaded stream insertion operator for your data type. If
your overridden putD() is successful it should return a non
zero value.
virtual void * getD(istream&) { return (void*)0; }
getD() is called for each node on a stream after the
container's header is loaded. The container binds what
getD returns. Template and form instances override this
function redefining it to allocate a new variable for your
data type with its default constructor, on which it then
invokes the overloaded stream extraction operator.
virtual int put(ostream& os);
Called by the save() function and the container's stream
insertion operator defined by the template and form
instances but only if the CL_SAVE flag is set. The
container's header is saved first followed by its nodes. The
nodes are saved via putD(). Template and form instances
do not override this put() function. Instead they override
putD(). The purpose of overriding the put() function would
be to store additional header information declared in a class
derived from the container.
virtual int get(istream& is);
Called by the load() function and the container's stream
extraction operator defined by the template and form
instances. The container's header data is loaded first
followed by its nodes. The nodes are retrieved via getD().
Template and form instances do not override this get()
function. Instead they override getD(). The purpose of
overriding the get() function would be to load additional
header information declared in a class derived from the
container.
int load(const char * filename);
Any nodes in the container are first cleared then the file
stream is opened and get() is called to load the saved
container and its nodes. If successful a non zero value is
returned. The complete state of the saved container is
restored, i.e. maxNodes, curNode, flags, etc. and cmP (if
user defined, the function must be registered prior to
saving, see Register_CmP() under FunctionRegistry).
Template and form instances promote this function to
public scope because only when the data type is known can
the container persist, i.e. getD() is meaningfully
overridden.
int save(const char * filename);
Opens a file stream and calls put() to save the container
state (maxNodes, curNode, flags, etcetera) and its nodes.
Template and form instances promote this function to
public scope because only when the data type is known can
the container persist, i.e. putD() is meaningfully
overridden.
inline ostream& operator<<(ostream& os,CL<ITEM>& b)
{ if (b.Flags(CL_SAVE)) (void) b.put(os); return os; }
inline istream& operator>>(istream& is, CL<ITEM>& b)
{ (void) b.get(is); return is; }
These friendly, public, stream operators are defined by
(form) template wrappers. ITEM is replaced with the data
type being checking for.
void vforEach(voidApplY B, va_list args);
See forEach() in public scope.
Constructors and Destructor
Methods, public:
cl (unsigned flags = CL_BIND_ONLY,
unsigned maxNodes = CL_MAXNODES,
unsigned limit = CL_LIMIT,
unsigned delta = CL_DELTA)
{ init(flags,maxNodes,limit,delta); }
This constructor defaults to initializing a container so that
node assigning, cloning, and deleting methods are inhibited
(CL_BIND_ONLY), e.g. atGetAsg(), atInsNew(), atDel(),
etcetera, see cl::flags. Thus the destructor will call
allRmv() instead of allDel(). Don't leave dynamically
allocated nodes in the container upon destruction unless the
CL_DEL is set or they will be left dangling, i.e. a memory
leak! Likewise don't bind statically allocated nodes in a
container that has its CL_DEL flag set or the destructor
will try to de-allocate them!
The container is limited to "maxNodes" starting off with an
internal physical array big enough to hold "limit" nodes
which will grow or shrink at "delta" rate. CL_LIMIT is
defined as 20 and CL_DELTA as 10 while
CL_MAXNODES is defined as ((unsigned) (UINT_MAX /
sizeof(void *))).
cl (void ** argv, unsigned argc = 0U,
unsigned flags = CL_BIND_ONLY);
This constructor takes "argv", a vector of void pointers,
and builds a container by copying each void pointer into a
cell of the container's array. If "argc" is zero then "argv"
is expected to be NULL terminated. This constructor calls
init() to do the actual container initialization then proceeds
to loop through the vector queuing the void pointers.
(Form) template instances strongly type check "argv."
cl& operator=(const cl& b) { return *this; }
The container assignment operator is overloaded in the
(form) template wrapper class and thus allows the
assignment of one container to another. Both containers
have to be of the same type and the "New" methods must
be enabled in the target container, i.e. CL_NEW flag set.
The nodes are cleared from the target container and the
nodes are cloned from the source container.
int operator==(const cl& b) const;
The container equality operator isn't functional for a type-
less container unless a user defined compare function has
been set with setCmP(). This operator compares node by
node. It is possible to recursively compare containers of
containers with this operator.
int operator> (const cl& b) const;
The container greater than operator isn't functional for a
type-less container unless a user defined compare function
has been set with setCmP(). This operator compares node
by node. It is possible to recursively compare containers of
containers with this operator.
void ** vector(void ** argv = (void **) 0,
unsigned argc = 0U);
Returns a dynamically allocated, NULL terminated, vector
of void pointers to the nodes currently in the container if
argv is NULL otherwise the supplied vector is used and
returned. If you supply argv and argc is zero, it is
assumed that argv is big enough. Only if argc is non zero
is any remaining space in argv that is not filled by node
pointers, NULL filled. However dynamic vectors are
always NULL terminated. The order of nodes in the
container dictates the order of pointers in the vector.
Remember to delete the returned vector when you are done
with it if you allowed vector() to allocate it! The nodes of
the container are not alerted to the fact that they are pointed
to by this vector, i.e. attachD() is not called - attachD() is
only called to "attach" data to a container. Thus the
Streamable/Mutual nodes in a container are not linked in
any way to the returned vector!
virtual ~cl() { destruct(); }
See the entry for destruct() near the beginning of the
protected section.
Housekeeping
Methods public:
unsigned Limit() { return limit; }
Returns the number of cells (in use plus available) in the
container's internal physical array.
unsigned setLimit(unsigned newLimit);
Adjusts the size of the container's internal physical array if
possible returning the new limit or zero otherwise. It's not
possible to set the new limit below the number of nodes
currently in the container or below delta or above
maxNodes.
unsigned pack() { return setLimit(nodes); }
Shrink the container's internal physical array to be just big
enough to hold the nodes. Returns the number of nodes in
the container which is also the size of the container's
internal physical array if successful, otherwise zero is
returned. The logical array is always contiguous within the
physical array, i.e. no holes. The container may map the
logical array's index 0 to any physical index, wrapping the
logical array as necessary. However, after a successful
pack(), the logical and physical arrays are identical, i.e. no
wrap.
unsigned Delta() { return delta; }
Returns the size that the container will grow or shrink upon
overflow or underflow respectively.
unsigned setDelta(unsigned newDelta = CL_DELTA);
Set the granularity of growth/shrinkage for the container's
physical array. CL_DELTA is defined as 10. Returns the
value of the new delta if successful otherwise zero is
returned.
unsigned Nodes() { return nodes; }
Returns the number of nodes currently bound within the
container, i.e. size of the logical array.
unsigned MaxNodes() { return maxNodes; }
Returns the maximum number of nodes the container is
ever allowed to hold.
unsigned setMaxNodes(unsigned newMaxNodes = CL_MAXNODES);
Set the maximum number of nodes a container is allowed
to hold returning this new value. NewMaxNodes must not
be below "limit" (current size of the container's internal
physical array) or above CL_MAXNODES in order to be
valid. Zero is returned if newMaxNodes is invalid.
CL_MAXNODES is defined as ((unsigned) (UINT_MAX /
sizeof(void *))).
unsigned vacancy() { return maxNodes - nodes; }
Returns the number of nodes that can yet be added to the
container.
unsigned vacancyNonElastic() { return limit - nodes; }
Returns the number of nodes that can still be added to the
container without undergoing an expansion of the
container's internal physical array.
unsigned Flags(unsigned flags = CL_ALL_FLAGS)
{ return (this->flags & flags); }
Returns the specified flag(s). CL_ALL_FLAGS masks all
flags. For flag definitions see "flags" in the protected
scope section.
unsigned setFlags(unsigned flags)
{ return (this->flags |= flags); }
Allows you to raise any container flag(s). Be sure you
don't set the CL_DEL flag if the container has statically
allocated nodes. It is sometimes useful to sort the container
using one compare function then scan it with a slightly
different "filter" compare function. Upon setting this new
compare function the CL_SORTED flag is automatically
reset but you can set it again with setFlags()!
unsigned resetFlags(unsigned flags)
{ return (this->flags &= ~flags); }
Allows you to reset any container flag(s). Be sure you
don't reset the CL_DEL flag if you are counting on the
container's deleting any remaining nodes. The container's
destructor calls allRmv() unless the CL_DEL flag is set in
which case it calls allDel().
cl& operator<<(cl& (*manipulator)(cl&))
{ return (manipulator?
(*manipulator)(*this)
: *this); }
This overloaded operator allows user defined manipulators
to be applied to a container in the same fashion iomanip.h
allows manipulators for streams. For example:
// examp601.cpp - link with cl.obj
#include "cl.hpp"
cl& clr(cl& b)
{ b.allClr(); return b; }
char *v[] = {"line one ", "line two", 0 };
main()
{
cl b ((void **) v);
b << clr;
b.atIns(0,"only line");
while (++b)
cout << (char *) b.get() << "\n";
return 0;
}
Elastic Array
Methods, public:
void * atIns(unsigned n, void * D);
Inserts a new cell at index n pushing back all cells starting
at n possibly expanding the container's internal physical
array if necessary. The specified void pointer is copied to
the new nth cell and is also returned. If the operation fails
the container is left unchanged and NULL is returned. If
D is NULL than a cell is not added.
void * atInsNew(unsigned n, const void * D);
Same as atIns() except the data pointed to by D is cloned
via newD() and the pointer to the clone is inserted instead
of the original. The inserted pointer is returned if all goes
well otherwise NULL is returned. The CL_NEW flag
must be set to enable this method. The default newD() for
a typeless container always returns NULL causing this
method to fail even if the CL_NEW flag is set.
void * atRmv(unsigned n);
Return the node pointer stored in the nth cell destroying
this position pulling all cells starting at n + 1 forward
perhaps collapsing the container's internal physical array if
the remaining number of cells falls below an internally
calculated threshold (lowThreshold). If the operation fails,
e.g. n is out of range, NULL is returned.
void allRmv();
Destroys all the cell positions discarding the node pointers
without deleting the nodes.
int atDel(unsigned n);
The CL_DEL flag must be set to enable this method.
Performs the same function as atRmv() except the discarded
node is deleted via the deleteD() protected virtual function.
A non zero value is returned to indicate success.
void * atDelAsg(unsigned n, void * D);
The CL_DEL and CL_ASG flags must both be set to
enable this method. Performs the same function as atDel()
except the contents of the node is copied to the D
parameter location before the node is deleted. The copy
operation is performed by the assignD() protected virtual
function. If the D parameter is NULL, or the assignD()
function fails, or for any other reason, e.g. n is out of
range, etc., the method fails and NULL is returned.
Otherwise the value of the D parameter is returned. The
default assignD() for a typeless container always returns a
failure indication.
int allDel();
Repeatedly calls atDel(0) if the CL_DEL flag is set. If the
CL_DEL flag is not set, zero is returned to indicate failure.
void allClr();
If the CL_DEL flag is set allDel() is called, otherwise
allRmv() is called.
void * atPut(unsigned n, void * D);
Overwrites the nth cell's pointer with D, the new node
pointer, returning D. If D is NULL or n is out of range
then nothing is overwritten and NULL is returned. Before
the old pointer is overwritten, it is detached via detachD()
and deleted via deleteD if the CL_DEL flag is set. The
new node is of course attached via attachD().
void * atPutNew(unsigned n, const void * D);
The CL_NEW flag must be set to enable this method.
Performs the same function as atPut() except the data
pointed to by D is cloned via newD() and the pointer to the
clone is atPut()'ed. If successful the clone's pointer is
returned, otherwise NULL is returned.
void * atPutAsg(unsigned n, const void * D);
The CL_ASG flag must be set to enable this method.
Copies the data pointed to by the D parameter to the node
bound at index n. The actual copying is performed by the
assignD() protected virtual function. If successful, the
pointer to the bound node is returned. The default
assignD() for a typeless container always returns a failure
indication.
void * atGet(unsigned n);
Returns the node pointer stored in the nth cell.
void * operator[](unsigned n) { return atGet(n); }
As shown by its inline definition, the overloaded
subscripting operator provides a convenient notation for
calling atGet().
void * atGetAsg(unsigned n, void * D);
Provides the same functionality as atGet(), except the
contents of the node bound at index n is copied via
assignD() to the location pointed to by the D parameter. If
successful the value of the D parameter is returned. The
CL_ASG flag must be set to enable this method. Also, the
default assignD() for a typeless container always returns a
failure indication.
void * atXchg(unsigned n, void * D);
Returns the node pointer stored in the nth cell if D is not
NULL and n is in range otherwise NULL is returned. D is
written to the nth cell.
unsigned index(const void * D);
Find the index of the cell containing the pointer with the
same value as D. If D is not bound in the container then
index() returns CL_NOTFOUND which is defined as
CL_MAXNODES which is in turn defined as ((unsigned)
(UINT_MAX / sizeof(void *))). Since the cells of a
container are indexed from 0 to nodes - 1,
CL_NOTFOUND will never be an index of a valid
container cell.
void forEach(voidApplY B, ...);
This container iterator applies the function specified by the
function pointed to by B to each of its nodes. The function
pointer type is defined as:
typedef void (*voidApplY)(void * D,
va_list args);
Template and form instances automatically type the D
parameter to the correct data type so there is no need to
type cast your function pointer to type voidApplY if it is of
the following form:
void (*B)(ITEM * D, va_list args);
int detect(void *& D, voidDetecT B, void * V = 0);
This container iterator applies the function pointed to by B
to each of its nodes. The first node "detected", i.e. the
function (*B)(...) returns a non zero value, causes the
detect() iterator to return immediately with a non zero
value. The D pointer points to the detected node in this
case, otherwise it is set to NULL. The function pointer
type is defined as:
typedef int (*voidDetecT)(void * D, void * V);
Template and form instances automatically type the D
parameter to the correct data type so there is no need to
type cast your function pointer to type voidDetecT if it is
of the following form:
int (*B)(ITEM * D, void * V);
The V parameter allows an additional user defined
parameter to be passed to the detecting function.
int detect(unsigned& i, voidDetecT B, void * V = 0);
This container iterator applies the function pointed to by B
to each of its nodes. The first node "detected", i.e. the
function (*B)(...) returns a non zero value, causes the
detect() iterator to return immediately with a non zero
value. The i integer is set to the index of the detected node
in this case, otherwise it is set to CL_NOTFOUND. The
function pointer type is defined as:
typedef int (*voidDetecT)(void * D, void * V);
Template and form instances automatically type the D
parameter to the correct data type so there is no need to
type cast your function pointer to type voidDetecT if it is
of the following form:
int (*B)(ITEM * D, void * V);
The V parameter allows an additional user defined
parameter to be passed to the detecting function.
unsigned select(cl& c, voidDetecT B, void * V = 0);
This container iterator applies the function pointed to by B
to each of its nodes. Whenever a node is "detected", i.e.
the function (*B)(...) returns a non zero value, that node is
queued into the "c" container. The "c" container is in
effect a list of the nodes "detected." This iterator returns
zero if no nodes are detected, otherwise the number of
nodes detected is returned. Thus the return value of
select() can be considered as a boolean value with true
indicating that one or more nodes have been selected. The
function pointer type is defined as:
typedef int (*voidDetecT)(void * D, void * V);
Template and form instances automatically type the D
parameter to the correct data type so there is no need to
type cast your function pointer to type voidDetecT if it is
of the following form:
int (*B)(ITEM * D, void * V);
The V parameter allows an additional user defined
parameter to be passed to the detecting function.
unsigned reject(cl& c, voidDetecT B, void * V = 0);
This container iterator applies the function pointed to by B
to each of its nodes. Whenever a node is not "detected",
i.e. the function (*B)(...) returns a zero value, that node is
queued into the "c" container. The "c" container is in
effect a list of the nodes "detected." This iterator returns
zero if no nodes are detected, otherwise the number of
nodes detected is returned. Thus the return value of
select() can be considered as a boolean value with true
indicating that one or more nodes have been selected. The
function pointer type is defined as:
typedef int (*voidDetecT)(void * D, void * V);
Template and form instances automatically type the D
parameter to the correct data type so there is no need to
type cast your function pointer to type voidDetecT if it is
of the following form:
int (*B)(ITEM * D, void * V);
The V parameter allows an additional user defined
parameter to be passed to the detecting function.
unsigned collect(cl& c, voidCollecT B, void * V = 0);
This container iterator applies the function pointed to by B
to each of its nodes. All the non NULL pointers returned
by (*B)(...) are collected, i.e. queued, into the "c"
container. This iterator returns zero if no pointers are
collected, otherwise the number of pointers collected is
returned. Thus the return value of collect() can be
considered as a boolean value with true indicating that one
or more pointers have been selected. The function pointer
type is defined as:
typedef void * (*voidCollecT)(void * D, void * V);
Template and form instances automatically type the D
parameter to the correct data type so there is no need to
type cast your function pointer to type voidCollecT if it is
of the following form:
void * (*B)(ITEM * D, void * V);
The V parameter allows an additional user defined
parameter to be passed to the collecting function.
Stack, Queue, and Deque
Methods, public:
void * push(void * D) { return atIns(0,D); }
Pushes the node pointer onto the stack returning that
pointer if successful otherwise it returns the NULL pointer.
void * pushNew(const void * D) { return atInsNew(0,D); }
Same as push() except the data pointed to by D is cloned
and a pointer to the clone is instead pushed.
void * pop() { return atRmv(0); }
Pop the stack returning the node pointer or NULL if no
nodes are present.
cl& operator>>(void *& D) { D = atRmv(0); return *this; }
Provides a convenient way to call pop().
int popDel() { return atDel(0); }
Same as pop() except the popped pointer is deleted via the
deleteD() function. The CL_DEL flag must be set to
enable this method.
void * popDelAsg(void * D) { return atDelAsg(0,D); }
Same as popDel() except the contents of the node being
delete is copied via assignD() to the location pointed to by
D. If successful D is returned otherwise NULL is. The
CL_ASG and CL_DEL flags both must be set to enable this
method.
void * top() { return atGet(0); }
Returns the pointer to the node at the front of the container
or NULL if there are no nodes. The front of the container
is considered to be the top of the stack or the front of the
deque-queue.
void * topAsg(void * D) { return atGetAsg(0,D); }
Same as top() except the contents of the top node is copied
via the assignD() virtual function to the location pointed to
by D. If successful D is returned. The CL_ASG flag must
be set to enable this method.
void * insQ(void * D) { return atIns(nodes,D); }
Inserts the D pointer at the rear of the queue returning this
pointer if successful or NULL otherwise.
cl& operator<<(void * D) { atIns(nodes,D); return *this; }
This overloaded operator provides a convenient notation for
inserting into the queue.
void * insQNew(const void * D) { return atInsNew(nodes,D); }
Same as insQ() except what D points to is cloned via the
newD() protected virtual function and the pointer to the
clone is queued instead of D. If successful the pointer to
the clone is returned. The CL_NEW flag must be set to
enable this method.
void * unQ() { return atRmv(nodes-1); }
Pops the rear of the queue returning its pointer.
int unQDel() { return atDel(nodes-1); }
Same as unQ() except the popped node is deleted via the
deleteD() virtual function. Zero is returned to indicate
failure. The CL_DEL flag must be set to enable this
method.
void * unQDelAsg(void * D) { return atDelAsg(nodes-1,D); }
Same as unQDel() except before deleting the popped node
its contents are copied via the assignD() virtual function to
the location pointed to by D. If successful D is returned.
Both the CL_ASG and CL_DEL flags have to be set to
enable this method.
void * rear() { return atGet(nodes-1); }
Returns a pointer to the rear node of the queue.
void * rearAsg(void * D) { return atGetAsg(nodes-1,D); }
Same as rear() except the contents of the rear node is
copied via the assignD() virtual function to the location
pointed to by D. If successful D is returned otherwise
NULL is returned. The CL_ASG flag must be set to
enable this method.
List
Methods, public:
unsigned CurNode();
Returns the index to the list's current node if there is one
or CL_NOTFOUND if the current node is undefined.
When a container is first constructed, its current node is
undefined.
int setCurNode(unsigned n = CL_NOTFOUND);
Resets the list's internal current node index to n if within
range, i.e. 0 to nodes-1, or to undefined if not.
CL_NOTFOUND is never in range so the current node is
left as undefined if setCurNode() is called without a
parameter. Returns false only if current node is left
undefined.
void * ins(void * D);
Inserts D into the container after the list's current node
making the inserted pointer, in the newly created cell, the
new current node. Returns D if successful, otherwise it
returns NULL. If at the outset, the current node is
undefined then the node is inserted at the rear of the list
and made the new current node. Remember, a list may or
may not have a current node defined.
void * insNew(const void * D);
Same as ins() except what D points to is cloned via the
newD() virtual function and the pointer to the clone is
inserted. If successful the pointer to the clone is returned
otherwise the NULL pointer is returned. The CL_NEW
flag must be set to enable this method.
void * rmv();
Destroys the current node cell returning its pointer. The
current node setting is decremented making the node ahead
of the node just extracted current. If the first node of the
list is extracted the current node index is left unchanged. If
the first node is also the last node the current node becomes
undefined. If the operation fails for any reason, e.g.
current node undefined at the outset or no nodes in the list,
NULL is returned.
int del();
Same as rmv() except the removed node is deleted via the
deleteD() protected virtual function. Zero is returned to
indicate failure. The CL_DEL flag must be set to enable
this method.
void * delAsg(void * D);
Same as del() except the contents of the node is copied via
the assignD() protected virtual function to the location
pointed to by D before the node is itself is deleted. If
successful D is returned. Both the CL_ASG and CL_DEL
flags must be set to enable this method.
void * put(void * D) { return atPut(curNode,D); }
Overwrites the current node's pointer with D, the new node
pointer, returning D. If D is NULL or the current node
index is undefined then nothing is overwritten and NULL is
returned. Before the old pointer is overwritten, it is
deleted via deleteD() if the CL_DEL flag is set.
void * putNew(const void * D) { return atPutNew(curNode,D); }
The CL_NEW flag must be set to enable this method.
Performs the same function as put() except the data pointed
to by D is cloned via newD() and the pointer to the clone is
put. If successful the clone's pointer is returned, otherwise
NULL is returned.
void * putAsg(const void * D) { return atPutAsg(curNode,D); }
The CL_ASG flag must be set to enable this method.
Copies the data pointed to by the D parameter to the
current node. The actual copying is performed by the
assignD() protected virtual function. If successful, the
pointer to the bound node is returned.
void * get() { return atGet(curNode); }
Returns the pointer to the list's current node or NULL if no
node is current or there are no nodes.
operator TYPE *() { return atGet(curNode); }
The subscript operator is also overloaded as shown here as
a convenient method for calling atGet() but only for (form)
template container wrappers. TYPE is replaced with the
data type being checked for.
void * getAsg(void * D) { return atGetAsg(curNode,D); }
Same as get() except the current node is copied via the
assignD() protected virtual function to the location pointed
to by D. If successful D is returned. The CL_ASG flag
must be set to enable this method.
void * next();
Advances the list's current node index returning the pointer
to the next node. Upon reaching the end of the list next()
returns NULL. A subsequent call to next() causes the
current node index to wrap around to zero, the index of the
first node in the list, and the process of walking the list can
begin anew.
void * operator++() { return next(); }
This overloaded prefix operator provides a convenient
notation for calling next().
void * nextAsg(void * D);
Same as next() except the contents of the new current node
is copied via the assignD() protected virtual function to the
location pointed to by D. If successful D is returned. The
CL_ASG flag must be set to enable this method.
void * prev();
Decrements the list's current node index returning the
pointer to the new current node. After reaching the front
of the list, the next call to prev() returns NULL. A
subsequent call to prev() causes the current node index to
wrap around positioning itself on the last node of the list
and the process of walking backwards across the list can
begin anew.
void * operator--() { return prev(); }
This overloaded prefix operator provides a convenient
notation for calling prev().
void * prevAsg(void * D);
Same as prev() except the contents of the current node is
copied via the assignD() protected virtual function to the
location pointed to by D. If successful D is returned. The
CL_ASG flag must be set to enable this method.
Sort, Search, and Unique
Methods, public:
unsigned Sorted() { return (flags & CL_SORTED); }
Returns true if the container is still sorted since its last sort.
For example, if a container was sorted and then nodes
popped from it, it would still be sorted so Sorted() would
return true. But if it had been pushed since the last sort,
the sorted order couldn't still be guaranteed so Sorted()
would return false. It is possible to disturb a container's
sorted order without the container being able to detect it,
e.g. using the pointer returned from atGet() to modify a
key field of a node while it is still bound within the
container. In such cases you should call unSort() to let the
container know that it can no longer guarantee a sorted
order.
void unSort() { flags &= ~CL_SORTED; }
See explanation of Sorted().
void setCmP(voidCmP cmP = voidCmP0)
{ this->cmP = cmPD(cmP); flags &= ~CL_SORTED; }
Sets the container's compare function which is used by
sort(), insSort(), insUnique(), findFirst(), etc.. The
compare function must return zero to indicate a match and
a value greater than zero if its first parameter is greater
than its second. If cmP is NULL cmPD() will return a
default compare function for template and form instances.
Your compare function should be of the form:
int cmp(const TYPE *, const TYPE *);
voidCmP CmP() { return cmP; }
Returns the compare function pointer stored in the
container's header.
int sort(voidCmP cmP = voidCmP0);
Uses a binary sort to sort the list in conjunction with the
user supplied compare function. If the compare function
pointer is NULL then the previously set compare function
pointer is used. If the previous function pointer is NULL
and cmPD() is unable to provide a default then the list
obviously can't be sorted and false is returned.
void * insSort(void * D);
Uses the previously set compare function pointer to insert
the D pointer into the container in a sorted order according
to the key contained in the node pointed to by D. What is
or isn't a key is defined by the compare function. If the
list isn't sorted, it is first sorted and then the insertion sort
of D takes place. In either case the newly insert node
becomes the list's new current node. If either the compare
function pointer (no default) or D is NULL then the
method fails and NULL is returned and the current node is
left undefined. Otherwise D is returned.
void * insSortNew(const void * D);
Same as insSort() except what D points to is cloned via the
newD() protected virtual function and the clone is inserted
in sorted order. If successful a pointer to the clone is
returned. The CL_NEW flag must be set to enable this
method.
void * insUnique(void * D);
Calls findFirst() to see if there is a match for D. If not D
is inserted into the container via insSort().
void * insUniqueNew(const void * D);
Same as insUnique() except D is cloned via newD() and the
cloned inserted in sorted order. If successful the pointer to
the clone is returned otherwise NULL is returned. The
CL_NEW flag must be set to enable this method.
void * findFirst(const void * K, cl::RelCmp rc = EQ);
Uses the previously set compare function (see setCmP()) to
find a node matching K. If the compare function hasn't
been established a default one is provided if available from
a template or form definition. Since the compare function
is user definable any field in the node can be used as a key.
The compare function is called with the node being tested
as its second parameter and K as its first.
If the list is sorted then findFirst() uses a binary search to
find the first match, otherwise it uses a linear search. If all
goes well the pointer to the matching node is returned
otherwise NULL is returned. The current node index is set
to that of the matching node or to undefined if no match is
found.
The compare function must always be defined so that it
returns zero to indicate equality and a value greater than
zero if its first parameter is greater than its second. The rc
parameter allows the sense of the match to be modified.
For example, if rc is set to GT then findFirst() will match
the first node greater than K. RelCmp is an enumerated
type defined as:
enum RelCmp { EQ, NE, GT, GE, LT, LE };
ITEM * operator[](const ITEM * K) { return findFirst(K); }
As you can see from its inline definition, this overloaded
subscript operator is a convenient way to call findfirst(). It
is only defined for (form) template wrappers where ITEM
is the data type being type checked.
void * findNext(const void * K, cl::RelCmp rc = EQ);
If the list is sorted the next node past the list's current node
is tested for a match with K via the previously set user
defined compare function. If it is a match the current node
index is advanced and a pointer to that node is returned. If
it isn't, findNext() returns the NULL pointer, after setting
the current node index to undefined.
If the list isn't sorted at the outset then the list is scanned
starting with the first node past the current node and
continues looking until the end of the list is reached. If a
match is found that node becomes the new current node and
its pointer returned, otherwise the current node is set to
undefined and the NULL pointer, is returned.
While it is possible to call findNext() after calling
findLast() and findPrev() several times and come up with a
match in a sorted list, be aware that for sorted lists the
current node must be immediately ahead or within the
contingent of matching nodes for findNext() to find them as
expected! In other words for sorted lists be sure to call
findFirst() first!
The compare function must always be defined so that it
returns zero to indicate equality and a value greater than
zero if its first parameter is greater than its second. The rc
parameter allows the sense of the match to be modified.
For example, if rc is set to LE then findNext() matches the
next node that is less than or equal to K. RelCmp is an
enumerated type defined as:
enum RelCmp { EQ, NE, GT, GE, LT, LE };
void * findLast(const void * K, cl::RelCmp rc = EQ);
Where as findFirst() searches for its first match closest to
the front of the list, findLast() searches for its first match
closest to the rear of the list. Likewise, if the list is sorted
a binary search is made and if not a linear search is made.
The current node index is set to the matching node and the
node's pointer returned. If there are no matching nodes
then the current node index is set to undefined and NULL
is returned.
void * findPrev(const void * K, cl::RelCmp rc = EQ);
If the list is sorted the previous node in the list is tested for
a match with K via the user defined compare function
previously set. If it is a match the current node index is
decremented and a pointer to that node is returned. If it
isn't, findPrev() returns NULL after setting the current
node index to undefined.
If the list isn't sorted then the list is walked backwards
from the current node while looking for a match or until
dropping off the front of the list. If a match is found that
node becomes the new current node and its pointer returned
otherwise the current node is set to undefined and NULL is
returned.
Be sure to read the explanation for findNext().
unsigned findAll(cl& b, const void * K, cl::RelCmp rc = EQ);
Using the previously user defined compare function, each
node is checked against K and the matches collected into
the "b" container. The current node is left undefined and
the number of nodes collected is returned.
The compare function must always be defined so that it
returns zero to indicate equality and a value greater than
zero if its first parameter is greater than its second. The rc
parameter allows the sense of the match to be modified.
For example, if rc is set to NE then findAll() matches all
nodes that are not equal to K. RelCmp is an enumerated
type defined as:
enum RelCmp { EQ, NE, GT, GE, LT, LE };
unsigned tallyAll(const void * K, cl::RelCmp rc = EQ);
Same as findAll() except the matched nodes are not
collected.
FunctionRegistry
The FunctionRegistry is a class that provides function pointer
registration so that function pointers can be streamed. A
container's default compare function does not need to be registered
since it can be recovered automatically. However, your own
defined compare functions and functions whose pointers appear in
nodes need to be registered before streaming. A unique id is
associated with each function pointer and that id is what actually
gets stream. Once registered, a compare function that is set for a
container is automatically streamed with that container without
programmer intervention. The macros below are provided so that
you may deal with other function pointers appearing in nodes
yourself.
#define Register_CmP(cmP,id) Register_Function(cmP,id)
For user defined compare functions call Register_CmP()
with a unique id.
#define Register_Function(fnC,id) \
fnCv.regFnC((GenericFnC)fnC,id)
For other functions that need to be streamed call
Register_Function() with a unique id. Be sure that you id
doesn't conflict with any other function including compare
functions!
#define Forget_Functions() fnCv.forget()
When your application has reached a phase where no more
function pointers will be streamed you may call
Forget_Functions() to free up space held by the
FunctionRegistry.
#define fnC2ID(fnC) fnCv.fnC_2_ID((GenericFnC)fnC)
Once registered, use this macro to convert a function
pointer to its id.
#define ID2fnC(id) fnCv.ID_2_fnC(id)
Once registered, use this macro to convert an id to its
function pointer. Don't forget any type cast that may be
required to get you back to the correct pointer type!
The following example demonstrates the function pointer macros.
// examp602.cpp - link with cl.obj
#include "cl.hpp"
#define ID_test 5
char * test(char * s1, char *s2)
{ cout << s1; return s2; }
main() {
Register_Function(test,ID_test);
cout << (*(char *(*)(char *, char*))
ID2fnC(fnC2ID(test)))
("\nGoodbye persistence headaches!\n",
"Hello streamable functions!\n");
return 0;
}
First the function pointer is registered. Then the function pointer is
converted to an id which is converted back to a function pointer which is
invoked with the two string parameters returning a string to stream to
cout. You can use the fnC2ID() macro to save a function pointer's id
inside your overriding cl::putD() function. Inside getD(), the extracted id
is converted back to a function pointer with the ID2fnC() macro.
Streamable
Streamable is a conceptually abstract class that provides a basis for a
persistable polymorphic cluster of classes that is container compatible.
The Streamable class works in concord with the ClassRegistry class.
Members, private:
void * parenT;
The first object to "link()" to the instance with a non
NULL parent pointer while the instance's parenT is NULL
will automatically establish a back link to that object.
When the back link parent unlink()'s, parenT is reset to
NULL (see ParenT()). This back link makes it easy for
Streamable/Mutual nodes of a container to be able to access
container member functions.
void init() { parenT = (void *)0; }
Initializes the parenT pointer.
Members, protected:
void assign(const Streamable& s) { parenT = s.parenT; }
Used to form an assignment chain in descendants for use in
copy initializer constructors and assignment operators. (see
pitem.cbk)
Streamable(defaultConstructor) { init(); }
Insures an unambiguous default constructor for hierarchy
chains. Used when reloading from stream.
virtual int put(ostream&) { return 1; }
Called by insert() to store the Streamable derived object on
stream. All descendants override this function. Each level
put() calls its base class put to store the base level
members. The parenT pointer is never saved on stream but
instead is reconstructed in the rebuilding linking that takes
place upon reloading.
int get(istream&) { return 1; }
Called by extract() if current level is the instance level
otherwise it is called from a descendant's get().
ostream& insert(ostream& os);
Inserts id and then calls put().
static StreamablE extract(istream& is);
Acts as a virtual initialize-from-stream constructor.
Streamable::extract() extracts id from stream and looks up
the instance level extract() in the class registry. The
instance level extract() is called to allocate and default
construct the instance and then calls the instance level get().
Each derived class overloads extract() to allocate and
default construct and to call its respective get(). It is the
instance level extract() that is registered with a call to
Register_Class().
Members, public:
inline ostream& operator<<(ostream& os, Streamable& s)
{ return s.insert(os); }
inline istream& operator>>(istream& is, StreamablE& S)
{ S = Streamable::extract(is); return is; }
These stream operators are actually just friends of
Streamable. Notice how they call insert() and extract().
Any object derived from a Streamable hierarchy uses these
operators for streaming. Recall from examp502.cpp, a
Shape overloads put() which is called from insert() above.
When reloading a Shape, Streamable::extract() extracts
Shape's id which it then uses to look-up Shape::extract().
Shape::extract() then allocates and default constructs Shape
and then calls Shape::get(). In examp503.cpp, recall that a
Circle is derived from Shape. In this case
Streamable::extract extracts Circle's id and looks up
Circle::extract(). Circle::extract() allocates and default
constructs Circle which in turn default constructs Shape.
Then Circle::extract() calls Circle::get() which calls
Shape::get() which extracts X and Y coordinates and
returns to Circle::get() which then extracts the radius which
returns to Circle::extract() which returns the pointer to the
newly constructed Circle.
Streamable() { init(); }
Standard default constructor for descendants.
virtual unsigned restream() { return 0U; }
Restreaming takes on meaning with multiple referenced
nodes. It is declared here so that container template and
forms can handle either Streamable or Mutual in a
consistent manner.
virtual int operator=(const Streamable&) { return 0; }
Polymorphic cluster nodes may be assigned to one another
if they have identical ID()'s. (See pitem.cbk to see how
this is done.) Then the container's assignD() virtual
function only has to call Streamable::operator=().
virtual StreamablE clone() const { return StreamablE0; }
Polymorphic descendants of Streamable override this
function to return a clone of themselves. This is
accomplished at each level by applying the new operator to
that level's copy initializer constructor. The container
newD() virtual function simply calls Streamable::clone().
Thus clone() acts like a virtual copy initializer constructor
for a polymorphic cluster.
virtual unsigned ID() const { return 0U; }
Polymorphic descendants of Streamable override this
function to provide a unique id. This may be replaced in
subsequent versions of Container Lite if C++ gets run
time typing.
operator unsigned() const { return ID(); }
A convenient method for invoking ID().
virtual unsigned level() { return 0U; }
This function is fully user definable and is not used by
either the container or stream registry, etc..
void * ParenT() { return parenT; }
Linking automatically maintains a back link pointer to the
first linking source. This is provided simply as a
convenience to your application if use can be made of it.
void unlink(void * P = (void *)0)
{ if (parenT == P) parenT = (void*)0; }
Called by cl::detachD() to signal the node it is no longer
attached to the container. Mutual overloads this function to
provide multi-reference resolution.
int link(void * P = (void *)0)
{ if (!parenT) parenT = P; return 1; }
Called by cl::attachD() to signal the node it is attached to
the container. In this manner a polymorphic cluster node
can determine which container it its attached to. Mutual
overloads this function to provide multi-reference
resolution.
unsigned RefCount() { return 0U; }
Provided for consistency with Mutual's overloaded
RefCount().
virtual ~Streamable() {}
Standard destructor, virtualized for polymorphic cluster
processing.
ClassRegistry
The ClassRegistry class, derived from FunctionRegistry, provides
the mechanism for allowing instances of classes derived from
Streamable and Mutual to be written out to a stream and later read
back into memory from the stream. There is only one instance of
ClassRegistry, namely clasSv. Each class derived from
Streamable/Mutual defines its own register_Class() static member
function which registers itself with clasSv. The following macros
make this mechanism transparent to the programmer.
#define Register_Class(classType) \
classType::register_Class()
You must register your Streamable and Mutual derived
classes before they can be streamed. If you followed the
format of pitem.cbk, simply call Register_Class() with your
derived class' name as a parameter to register that class.
#define Forget_Classes() clasSv.forget()
If you are completely finished with streaming operations for
both Streamable and Mutual derived classes you can call
Forget_Classes() to free up space held by the
ClassRegistry.
Mutual
Mutual, derived from Streamable, is a conceptually abstract class that
additionally provides for mutual ownership behavior in its descendants.
Mutual objects are able to record multiple reference counts which can be
used to avoid accidental deletion. Even if a Mutual object is multiply
referenced, any attempt to store the objects referencing it will result in
only one copy of the Mutual object being stored. When reloaded from a
stream, only one copy of the Mutual instance will be reconstructed and
the multiple links will be automatically reestablished. The Mutual class
works in concord with both the ClassRegistry and MutualHoldingPen
classes.
Members, private:
unsigned refCount;
Records the number of "link()s" made to instance.
RefCount is incremented by link() and decremented by
unlink().
unsigned streamCount;
The number of times the instance has been written to the
stream during the current streaming operation. Note that
only the first time is the instance written and any additional
attempts to write only writes a multiple reference mark.
StreamCount must be less than refCount in order for an
instance to be streamed.
long streamPos;
Once an instance is written to a stream its position on the
stream is recorded internally to facilitate the writing of the
multiple reference marks on the stream.
The following chart shows what happens during a streaming
operation. Notice that if an instance is referenced only once then
it will only be streamed once. The parent pointer is not streamed
so in order for this link to persist you must insure that the
dominate link, the first link that reaches the instance when storing,
is the parent link so that it will be automatically reconstructed
when "re-link()ing" the loaded instance. StreamCount isn't
streamed since a reloaded instance's streamCount is always zero.
The user's data resides in descendants of Mutual. Only on the
first encounter with a instance during a streaming operation is it
necessary to store the user's data. The next time only a multiple
reference marker is written to the stream.
Stream contents:
1st & Only 1st of Many MultiRef
ID() x x 0
refCount 1 > 1
streamCount
streamPos x
user's data x x
When the stream is subsequently reloaded, the first time an
instance is encountered, the stream registry will load its id. If its
id is non zero then refCount is read and the id is used to look up
the Mutual class' descendant's static extract() function, which is
called to load the descendant's data. Register_Class() registered
this extract() function with the registry. The descendant's extract()
function will call its default constructor which in turn calls
Mutual(defaultConstruct) which initializes parenT to NULL and
refCount, streamCount, and streamPos to zero. If the refCount
read from the stream is greater than one then the pointer to the
loaded Mutual instance returned from the descendant's extract()
function is also saved in a holding pen along with the reloaded
refCount and the instance's position on the stream. The pointer to
the instance is returned by the overloaded stream extraction
operator, i.e. operator>>(istream&,MutuaL&) as usual.
If the id is zero then the instance has already been loaded and its
pointer is being held in the holding pen of the stream registry. In
this case streamPos is read from the stream's multiple reference
mark and used to uniquely identify the instance being held in the
holding pen. Instead of calling the descendant's extract() function,
the pointer retrieved from the holding pen is returned by the
stream extraction operator. When the last multiple reference link
to a instance held in the holding pen is reconstructed, the holding
pen automatically releases the instance. The holding pen is simply
a container.
#ifdef MUTUAL_DEBUG
static void mut_error(const char *msg,
unsigned id,
unsigned refCount,
unsigned streamCount,
long streamPos);
#else
#define mut_error(msg,id,refCount,streamCount,streamPos)
#endif
If you compile pitem.cpp with MUTUAL_DEBUG defined
then an error reporting mechanism is put in place on cerr.
void init()
{ refCount = streamCount = 0U; streamPos = 0L; }
Called by the Mutual constructors.
Members, protected:
void assign(const Mutual& m)
{ Streamable::assign(*(const Streamable*)&m);
streamCount = 0U; streamPos = 0L; }
Assign() allows for a assign() chain for copy initializer
constructors and overloaded assignment operators. (see
Streamable::assign().)
Mutual(defaultConstructor) : Streamable(defaultConstruct)
{ init(); }
Insures an unambiguous default constructor for hierarchy
chains. Used when reloading from stream.
ostream& insert(ostream& os);
Inserts id with refCount or multi-reference mark with
streamPos and then calls the inherited Streamable::put()
virtual function to store the descendants data if it isn't a
multi-reference mark.
static MutuaL extract(istream& is);
Acts as a virtual initialize-from-stream constructor.
Mutual::extract() extracts id with refCount from stream if
this is the first encounter with the object or the multi-
reference mark with streamPos if this is an additional
encounter. On the first encounter, the id is used to look-up
the descendant's extract() function which is then called to
load the object. The descendant's extract() first allocates
and default constructs the descendant, then calls the
descendant's get() function to perform the actual loading of
the object. If refCount is greater than one, a pointer to the
reloaded object along with its streamPos is recorded in the
holding pen. When an additional encounter is made on the
stream, the streamPos is used to look-up the already loaded
objects pointer in the holding pen. The holding pen also
maintains a tally of reconstructed references. The pointer
to the object in the holding pen is returned by
Mutual::extract() instead of calling the descendant's
extract() again. When all references to a object in the
holding pen have been reconstructed the holding pen
destroys the pointer record. ::Restream() iterates across the
holding pen list to determine discrepancies in reconstructing
multiple references.
Members, public:
inline ostream& operator<<(ostream& os, Mutual& m)
{ return m.insert(os); }
inline istream& operator>>(istream& is, MutuaL& M)
{ M = Mutual::extract(is); return is; }
These stream operators are actually just friends of Mutual.
Notice how they call insert() and extract(). Any object
derived from a Mutual hierarchy uses these operators for
streaming.
Mutual() : Streamable() { init(); }
Standard default constructor.
virtual unsigned restream();
After a network of Mutual nodes has been saved on stream,
restream() must be called before another attempt at saving
is made. This is because streamCount and streamPos must
be zeroed. StreamCount and streamPos are used by the
automatic multiple storage mechanism. StreamCount
doesn't allow the instance to be streamed out more times
than it has been notified that it is referenced, i.e. refCount
via link(). StreamCount also tells the stream registry if this
is the first time the instance is being streamed. If so the
descendant's data is streamed out via Streamable::put()
after the stream registry outputs the id and refCount. If not
the first time, the multiple reference mark with streamPos
is streamed out instead of the id with refCount recording
the position on the stream of the actual instance. After
streaming a network of Mutual instances, streamCount
should equal refCount for each instance owing to the fact
that refCount should be the number of ways an instance can
be reached over the network, i.e. the number of link()s.
Mutual::restream() calculates the difference between
refCount and streamCount before resetting streamCount and
streamPos whenever streamPos is non zero. If pitem.cpp
was compiled with MUTUAL_DEBUG defined the
discrepancy is reported on cerr. If streamPos is zero,
restream() returns zero since no streaming has taken place
since the last restream() or constructor call. (See also
Mutual::extract() above.)
void unlink(void * P = (void *)0);
Decrements refCount if non zero, returning its value. If
pitem.cpp was compiled with MUTUAL_DEBUG defined
any underflow is reported on cerr. If P is equal to parenT
then parenT is reset to NULL. Unlink() is called by cl::
detachD() which in turn is called by container unbinding
methods, e.g. atDel(), atPut(), and atXchg(), for automatic
unlinking of Mutual nodes.
int link(void * P = (void *)0);
Increments refCount if less than UINT_MAX to indicate
additional references in memory to the instance. Overflow
errors are reported on cerr if pitem.cpp was compiled with
MUTUAL_DEBUG defined. If P is not NULL and parenT
is, P is assigned to parenT. Link() is called by cl::
attachD() which in turn is called by container binding
methods, e.g. atIns(), atGet(), and atXchg(), for automatic
linking of Mutual nodes.
unsigned RefCount() { return refCount; }
Called by cl:: deleteD() to see if a Mutual node can be
safely deleted. Only when zero is returned is it okay to
delete a Mutual node. See link() and unlink().
long StreamPos() { return streamPos; }
Provided for building Mutual object file indexes. After
streaming out and before restream() is called, call
StreamPos() to determine the position of the Mutual object
in the file.
virtual ~Mutual() {}
Provided so that descendants can be virtually destroyed.
MutualHoldingPen
The MutualHoldingPen class provides the mechanism for allowing
instances of classes derived from Mutual to be read back into
memory from the stream automatically reconstructing the multiple
references. There is only one instance of MutualHoldingPen,
namely instancEv. The following macro makes this mechanism
transparent to the programmer.
#define Restream() instancEv.restream()
The Restream() macro expands into a call to the restream()
member function without the need for a instance name.
When Mutual instances are reloaded from a stream back
into memory, instances with multiple references are loaded
only once while pointers to these instances are held in a
holding pen within the instancEv so that the multiple
references can be reconstructed as they are encountered.
Once the stream is fully loaded all instances should have
already been automatically released from the holding pen.
Restream() releases any stragglers, reporting the relinking
discrepancies on cerr if pitem.cpp was compiled with
MUTUAL_DEBUG defined. You should call Restream()
after every reloading operation to insure that holding pen is
clear. Restream() returns the number of objects with
discrepancies.
Index
~cl() 76
~Mutual() 104
~Streamable() 98
A
allClr() 80
allDel() 70, 80
allRmv() 70, 79
ANSI C 8
ANSI string class 24
Asg suffix 70
Assign 12
assign() 47, 50, 70, 95, 102
assignD() 48, 50, 70, 71
Assignment operator 50
atDel() 79
atDelAsg() 79
atGet() 80
atGetAsg() 80
atIns() 79
atInsNew() 79
atPut() 80
atPutAsg() 80
atPutNew() 80
atRmv() 79
attachD() 49, 72, 97, 104
atXchg() 81
B
Bags 22
C
catch_reset() 9
Circle 53
cl (void ** argv 75
cl() 74
cl(defaultConstructor) 70
CL_ALL_DEF 45
CL_ASG 17, 33, 48, 70
CL_ASSIGN_BYTES 41
CL_ASSIGN_DEF 45
CL_ASSIGN_OP 32, 48
CL_assignD() 45, 56, 70
CL_BIND_ONLY 70
CL_CDECL 8
CL_char 39
CL_CLONE_BYTES 41
CL_CMP_BYTES 41
CL_CMP_DEF 45
CL_cmpD() 45
CL_COPYINIT 33
CL_DEL 17, 70
CL_DELETE_DEF 46
CL_deleteD() 46
CL_DELTA 75
CL_EQ_GT_OPS 32
CL_getD() 46
CL_LIMIT 75
CL_MAXNODES 75
CL_NEW 17, 34, 48, 70
CL_NEW_DEF 45
CL_newD() 45, 56
CL_NEWED 70
CL_NO_EXCEPTIONS 8
CL_NO_STDARG_H 8
CL_NO_TEMPLATES 8
CL_NO_THROW_XALLOC 8
CL_NOTFOUND 21, 86
CL_PITEM() 56
CL_putD() 46
CL_READ 70
CL_READ_DEL 70
CL_SAVE 17, 35, 70
CL_SORTED 70
CL_STRM_BYTES 41
CL_STRM_EXTRACT_DEF 46
CL_STRM_INSERT_DEF 46
CL_STRM_OPS 34
CL_THROW_XALLOC 8
CL_WELL_ENDOWED 35
Class level defaults 50
Class level members 56
ClassRegistry 99
CLcmPcast() 21, 48
clexamps.cpp 7
cllib.src 7
Clone 12
clone() 56, 97
cmP 70
cmPD() 48, 71
Compare function 21
Copy initializer 33, 37, 50
curNode 69
CurNode() 86
Current node 18
D
Default constructor 34, 37
defaultConstructor 47
Del suffix 70
del() 86
DelAsg suffix 70
delAsg() 86
Delete 12
deleteD() 49, 50, 72, 104
delta 69
Delta() 77
Deque 16
destruct() 47, 50, 71
Destructor 37, 50
detachD() 49, 73, 97, 103
Dictionaries 22
Doubly linked list 18
Dynamic nodes 18
E
Elastic array 11
examp101.cpp 9
examp201.cpp 12
examp202.cpp 14
examp203.cpp 15
examp204.cpp 17
examp205.cpp 18
examp206.cpp 20
examp207.cpp 22
examp208.cpp 24
examp209.cpp 24
examp210.cpp 24
examp211.cpp 25
examp301.cpp 37
examp302.cpp 40
examp303.cpp 43
examp401.cpp 47
examp501.cpp 51
examp502.cpp 53
examp503.cpp 57
examp504.cpp 60
examp505.cpp 64
examp601.cpp 78
examp602.cpp 93
Examples subdirectory 7
extract() 56, 96, 102
F
FIFO 16
findAll() 92
findLast() 91
findNext() 91
findPrev() 91
first 69
flags 70
Flags() 77
fnC2ID() 93
FOLO 16
forEach() 64, 81
Forget_Classes() 99
Forget_Functions() 93
Form templates 29
FunctionRegistry 93
G
gconsole.cpp 51
Geometric shapes 51
get() 49, 73, 87, 95
getD() 49, 53, 73
Granularity 11
Graphic segments 64
H
Heterogeneous data 51
Hysteresis 11
I
ID() 56, 97
ID2fnC() 93
index() 81
init() 69, 95, 102
ins() 86
insert() 96, 102
insNew() 86
insQ() 84
insQNew() 85
Installation 7
iomanip.h 16
iostream.h 16
ITEM_char 39
L
level() 97
Library building 7
LIFO 16
limit 69
Limit() 76
link() 97, 103
linkS 69
List 18
load() 74
Logical array 69
lowLimit 69
lowTheshold 69
M
makefile 7
maxNodes 69
MaxNodes() 77
Mediator functions 43
MS Windows 24
Multi-faceted hybrid behavior 11
Multiple reference arbitration 51
Multiple reference resolution 68
Mutual 51, 64, 100
Mutual holding pen 68
Mutual() 103
Mutual(defaultConstructor) 102
MUTUAL_DEBUG 68, 101
MutualHoldingPen 105
N
Network 68
New suffix 70
newD() 48, 70, 72
nodes 69
Nodes() 77
O
Object file indices 104
operator TYPE *() 87
operator unsigned() 97
operator<<() 34, 37, 78, 85, 96, 103
operator=() 32, 37, 75, 97
operator==() 31, 36, 75
operator>() 31, 36, 75
operator>>() 34, 37, 84, 96, 103
operator[]() 80
P
pack() 76
parenT 95
ParenT() 97
Physical array 69
PieSlice 64
pitem.cbk 51
PITEM_STRM_BASE 57
Polymorphic clusters 51
pop() 84
popDel() 84
popDelAsg() 84
Protected scope virtual functions 47
push() 84
pushNew() 84
put() 49, 73, 87, 95
putAsg() 87
putD() 49, 53, 73
putNew() 87
Q
Queue 16
R
rear() 85
rearAsg() 85
Rectangle 56
refCount 100
RefCount() 98, 104
Register_Class() 56, 99
Register_CmP() 93
Register_Function() 93
resetFlags() 78
restream() 68, 96, 103, 105
rmv() 86
S
save() 70, 74
Search 21
SegRef 64
setCurNode() 86
setDelta() 77
setFlags() 77
setLimit() 76
setMaxNodes() 77
Sets 22
Shapes 53
Sort 21
Stack 16
Static nodes 18
stdarg.h 8
Stream contents 101
Streamable 51
Streamable() 96
Streamable(defaultConstructor) 95
streamCount 68, 100
streamPos 68, 100
StreamPos() 104
string.h 16
T
top() 84
topAsg() 84
Typeless container 20, 29
U
UNIX V 8
unlink() 97, 103
unQ() 85
unQDel() 85
unQDelAsg() 85
V
vacancy() 77
vacancyNonElastic() 77
Variable argument lists 64
vector() 76
vforEach() 74
Virtual destructor 50
Virtual initialize-from-stream constructor
56
Virtual put() 56
voidApplY 81
W
Wrapper class 12