As discussed in Section
, FARFALLA handles input and
output by asking the nodes to read and write themselves to disk.
Therefore each class definition must include the definition of a
function that can input or output the node, as well as count the
number of bytes the node will take on disk. This function is a
virtual member function called IOData(). (The keyword virtual is another C++ technicality you will understand after reading
a C++ text.)
IOData() takes three arguments - the first is a source or destination of data (a C++ iostream variable), and the second is a keyword telling the routine to either input the data, output the data, or count the size of the node. (The FARFALLA package defines in, out and count as members of the F_inout enumeration type.) The third argument, an F_length variable, is only used when the routine is counting the length.
IOData() generally accomplishes its task by passing its arguments directly to a series of calls to the FARFALLA function F_IORec(). F_IORec() can handle the translation to and from XDR format, input and output to disk, and counting, for the following data types: char, short, int, long (signed and unsigned versions of all these integer types), float, double and C/C++ strings (char* are assumed to point to null-terminated character strings). char* output is not yet properly implemented. Here is the erpNode IOData() function:
virtual inline void IOData(iostream& file, F_inout choice,
F_length& nodeLength)
{
F_IORec(file,version,choice,nodeLength);
F_IORec(file,boxnum,choice,nodeLength);
F_IORec(file,adc0u,choice,nodeLength);
F_IORec(file,adc1u,choice,nodeLength);
F_IORec(file,tdc0h,choice,nodeLength);
F_IORec(file,tdc1h,choice,nodeLength);
F_IORec(file,adc0a,choice,nodeLength);
F_IORec(file,adc1a,choice,nodeLength);
F_IORec(file,tdc0l,choice,nodeLength);
F_IORec(file,tdc1l,choice,nodeLength);
F_IORec(file,energy,choice,nodeLength);
F_IORec(file,time,choice,nodeLength);
F_IORec(file,posh,choice,nodeLength);
}
You must not pass an array to F_IORec() because it cannot figure out how big the array is; instead, pass it the array members one at a time:
for (i = 0; i<arrSize; i++) F_IORec(file,array[i],choice,nodeLength);
What if in the future we decide to include one more piece of data in the ERP node? We could modify the IOData() function to input and output the new data. However, if the new IOData() is used to input a disk file that was written with the old IOData(), the erpNode input will consume too many bytes from the input stream and FARFALLA will get hopelessly confused. For this reason, any time you design a node, you should consider including versioning. The FARFALLA package does not require or implement versioning, but the node designer may easily do it. In the example we include an unsigned char variable called version, which may eventually handle version numbers between 0 and 255, yet takes up only one byte of disk space on the output file. (In C/C++ , a char may contain any small integer, not just valid ascii characters; it may be thought of as a shortissimo.) The version variable must be the first value input or output in the IOData() routine. For now, when only one version exists, the version variable is ignored on input and output. However, in the future we could extend the IOData() routine by appending lines like the following:
if (version == 0) newdata = UNDEFINED;
else F_IORec(file,newdata,choice,nodeLength);
In this way, the new node definition works properly with old or new data files. Of course the programmer must be careful not to use the newdata variable if a version 0 erpNode has been read off disk. Versioning must be inserted at the time the node type is first defined; by the time you are sure you need it it is too late to add it to existing files. You should be quite liberal in introducing versioning because it does not take much disk space. In a realistic FARFALLA DST when we added versioning to all node types it increased the size of our output files by less than 1%. We are incorporating versioning in all nodes we design for MACRO DSTs.
When the node contains a fixed amount of information, the IOData() function is usually very simple. What if the node contains a variable amount of information? The wireTrackNode definition handles a variable-length hit list in a nice way.
In memory, the wireTrackNode will keep the list of streamer tube hits in dynamic memory. The node on the FARFALLA tree contains only an integer stating how many hits there are, and a pointer to the list in dynamic memory.
... int numCorHits; // Num of ST hits associated with track wireCoorBank *hits; // hits associated with track ...
Here, wireCoorBank is the name of a structure that can describe one streamer tube hit, and hits is a pointer to an array of such structures. The integer is set and the memory is allocated when the programmer calls the wireTrackNode member function setSize():
inline setSize(int num) { numCorHits = num; hits = new wireCoorBank[num];}
When it is time to output the node to disk, IOData() first outputs the integer giving the size of the array; then it outputs each element of the array one at a time. When it is time to read the node in from disk, IOData() reads the number of entries; then it calls setSize() to allocate the memory, and finally it reads in each element of the array one at a time (remember that hits[i] refers to the i-th element of the array which hits points at):
virtual inline void IOData(iostream& file, F_inout choice,
F_length& nodeLength)
{
...
F_IORec(file,numCorHits,choice,length);
if (choice == in) setSize(numCorHits);
for (int i=0; i<numCorHits;i++)
{
F_IORec(file,hits[i].x,choice,length);
F_IORec(file,hits[i].z,choice,length);
F_IORec(file,hits[i].dx,choice,length);
F_IORec(file,hits[i].dz,choice,length);
}
}
This solution is much more elegant than using a fixed-length array inside the node. It is more space-efficient because no unused array entries are written to disk. Also, it is more flexible than a fixed-length array because there is no a priori limit on how big the array can be.
However, you must be careful using this system. When you are finished with the tree containing this node, you must be careful to free up the space that was occupied by the hit list. You have learned that every node has a constructor function that is called the node is created. Nodes also have a destructor function that is called when the node is deleted. The base class F_Node defines a generic destructor that works for fixed-length nodes, so usually you will not have to define a destructor at all; you will inherit the destructor you need. However, if your node allocates space elsewhere you must write a specialized destructor for your node that frees up that space. The name of the destructor function is the name of your class, preceded by ``~ ''. For wireTrackNode the destructor definition appears right under the constructor definition and looks like this:
virtual ~wireTrackNode() {delete [] hits;}
(The ``[]'' tells C++ to delete the entire array that hits points at.)
If your node contains a pointer, not to some array elsewhere in memory,
but to another node on the same FARFALLA tree, see
Section
for information on how to write IOData().