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 // rand(), srand() #include // time_t, time() #include "cl.h" CL_WELL_ENDOWED(int) #define intFile "ints.tmp" main() { time_t t; srand((unsigned) time(&t)); CL 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 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 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 // rand(), srand() #include // 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." 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 #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 #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 #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 #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 #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 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 #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 #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 #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 #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 #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 #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 #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 #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 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 #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 #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 #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 #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 #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 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 #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 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 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 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 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& b) { if (b.Flags(CL_SAVE)) (void) b.put(os); return os; } inline istream& operator>>(istream& is, CL& 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