Previous Index Next

VI Dynamic Types

29 Scope

This section is addressed to the users that want to learn the basics aspects and usage of Dynamic Types (DTs for short). You may also find a section devoted to Dynamic Types in the "Developers" part of this document. This latter is aimed to explain how to create new classes that derive from the base DT, and goes into some details regarding their functionality.

30 Why Dynamic Types ?

Though it might be a quite controversial issue, there are three main reasons for the decision of implementing and using DTs in CLAM.

  1. There is a need in some core classes of the library, of working with types with a large number of attributes, i.e.: the descriptors of audio segments, that in some cases only a small subset is needed, and so could represent a waste of space if its memory is always allocated. DT can instantiate and de-instantiate attributes at run-time, and do it in such a way that its interface is the same as if they where C++ normal attributes.
  2. We want support for working with hierarchic or tree structures. That means not only composition of DTs but also aggregates of them (lists, vectors, etc. of DTs). With such compositions of DTs, we can use assignation, and two clone member functions: ShallowCopy () and DeepCopy(), the good thing is that they come free; we don't need to write these members in none of the DT concrete classes.
  3. We obtain introspection of each DT object. That is the ability to know the name and type of each dynamic attribute, to iterate through theses attributes, of having some type specific handlers for each. One clear application of introspection is storage support for loading from, and storing to a file, of a tree of DTs. Of course all this is implemented generically, so appears transparent to the user. At this point we have XML support implemented. Other profits we take from introspection in DT are debugging aids.

31 Where can DT be found within the CLAM library?

All classes deriving from ProcessingData base class are DT. The concrete ProcessingConfig classes as well as the ProcessingDataConfig classes are also DT.

32 Declaring a DT

When we say that dynamic attributes can be instantiated at run time, we mean that we can do so with the previously declared dynamic attributes. Let's see an example. Imagine we want to model a musical note with a DT. We declare it like this:

class Note : public DynamicType
{
public:
DYNAMIC_TYPE (Note, 5)

DYN_ATTRIBUTE (0, public, float, Pitch)
DYN_ATTRIBUTE (1, public, unsigned, NSines)
DYN_ATTRIBUTE (2, public, ADSR, Envolvent)
DYN_CONTAINER_ATTRIBUTE (3, public, std::list<Sine>, Sines,
harmonic)
DYN_ATTRIBUTE (4, private, Audio, Wave)
};

This is a macro-based declaration, right now there are three different macros: DYNAMIC_TYPE for expanding the concrete DT constructors, DYN_ATTRIBUTE for declaring each dynamic attribute and DYN_CONTAINER_ATTRIBUTE for declaring any STL interface compliant container.

We will now explain these three more in depth:

1. DYNAMIC_TYPE this macro expands the default constructor of the concrete DT being declared. The first parameter is the total number of dynamic attributes, and the second one the class name.

If the writer of a DT derived class sees the need of writing a customized default constructor or other constructors it can be done using the initializers. See section 82.

2. DYN_ATTRIBUTE it is used to declare a dynamic attribute. It has four parameters, the first one is the attribute order (needed for technical reasons of the DT implementation), the second one is the accessibility (public, protected or private) the third one is the type: it can be any C++ valid type including typedef definitions but not references (& finished) or pointers (* finished). If you still think you need pointers as dynamic attributes then read the pointers section (section 85).

The forth and last parameter is the attribute name, it is important to begin in upper-case because this name (let's call it XXX) will be used to form the attribute accessors GetXXX() and SetXXX(.), thus the XXX must start in upper-case, following the coding style of the library (See chapter XVI)

Returning to the example above, each DYN_ATTRIBUTE macro will expand a set of usable methods:

float& GetPitch(), void SetPitch(const float&),
void AddPitch(), void RemovePitch(), bool HasPitch()
void StorePitch(Storage&) const, bool LoadPitch(Storage&)

Of course GetPitch and SetPitch are the usual accessors to the data. AddPitch and RemovePitch, combined with UpdateData that will be explained latter on, will instantiate and de-instantiate the attribute. HasPitch returns whether Pitch is instantiated at this moment. Finally StorePitch and LoadPitch are for storage purposes, and will be explained in section 35

3. DYN_CONTAINER_ATTR: The purpose of this macro is to give storage (only XML by now) support to attributes declared as containers of objects. For providing this service, we need that container to fulfill the STL container interface, so all the STL collection of containers is usable. This macro has five parameters, one more that DYN_ATTRIBUTE: the attribute numeration, accessibility, the type, the name of the attribute and finally the new one: the label of each contained element that will be stored.

33 Basic usage

Once, the concrete DT Note has been declared, we can use it like this:

Note myNote; // create an instance of the DT Note

Now myNote, have no attribute instanciate. We can activate attributes this way:

myNote.AddPitch(); myNote.AddNSines(); myNote.AddSines();

Or in the case that we want all of them, it is better to use AddAll. (This method is not macro generated as AddPitch, but is a DT member available in any concrete DT.

)
myNote.AddAll();

As this kind of operations require memory management, we want to update the data, with its possible reallocations only once for every modification of the DT shape or structure (which can imply many individual adds and removes). We'll use the DynamicType UpdateData operation for that purpose:

std::cout << myNote.HasPitch() // writes out: 'false'
myNote.UpdateData();
std::cout << myNote.HasPitch() // writes out: 'true'

And now all the instantiated attributes can be accessed as usual, using the accessors GetXXX and SetXXX. For example:

myNote.SetNSines(10);
myNote.SetPitch(440);

// lets use some std::list operations:
myNote.GetSines().push_back(440).push_back(440*2);
myNote.GetSines().push_back(440*3).push_back(440*4); // etc.

int i=myNote.GetPitch(); // error! GetPitch() returns float
int j=myNote.GetNSines(); // ok.

More about adding, removing and updating attributes:

It is important to learn the exact behaviour of adding and removing DT attributes. It can be summed up to three ideas:

GetXXX and SetXXX operations over non-instantiated attributes will rise an exception ErrDynamicType. So these operations should be protected with an if (HasXXX()) clause in places of the code where there is no safety about the presence of XXX.

• Besides, AddXXX and RemoveXXX can be used safely (don't rise any exception). Basically what they do is set and unset internal flags. For example, if the attribute Pitch exists and we do RemovePitch() it will be marked as "removed" (waiting for the UpdateData() to perform the actual removal). Now, if before updating data we call AddPitch(), then it detects the mark and the effect is just to unset this flag. If we insist with another AddPitch() it will have no effect (and no flag will be set), because it is allready instantiated. The case of adding a non-instantiated attribute is symmetric as the case of removing an instantiated one.

UpdateData() is a safe and efficient operation: with safe what we mean is that it has no effect (and doesn't rise exceptions) in case that the DT needs no memory update, and it is efficient in the sense that the checking for changes in the dynamic shape doesn't involve any traversal of the attributes but only cheking a global flag. Moreover, UpdataData() returns a boolean that says if it has been necessary to update the data. Sometimes it is very useful do things such as :

// here we don't know if Pitch exist.
myNote.AddPitch();
if (myNote.UpdateData()) {
// yes, it existed
// ...
}
else {
// no, it didn't was there till now.
// ...
}

34 Prototypes and copy constructors

It's been said that a the dynamic shape of a DT is the set of instantiated attributes. The description of this shape is stored in a table and can be shared between various DTs. Thus this brings us to the idea of prototypes and creating objects by cloning the dynamic shape of others, this is exactly how the default copy constructor works.

Note n1(myNote), n2(myNote), n3(myNote), n4(myNote);

Now n1, n2, n3 and n4 all share both static information of their type and the dynamic shape information, and we say that myNote is their prototype, or that they all share the same prototype. It is important to notice that this happens even if the prototype (myNote) doesn't have its data updated. If we decide to change the shape of one of these objects, i.e. we want to add new attributes to n4, this operation will automatically create a new prototype and so a new dynamic shape as the next figure shows:

Of course, the copy constructor also copies the data of each instantiated attribute from the source object into the new object. This copy is made using the corresponding copy constructor. This means that the copy constructor makes a so-called "deep copy" (that means recursives copies of its sub-elements) of any composition of DT and aggregates (i.e. using STL containers) of DTs.

More on copy constructors (i.e. with the shareMemory flag) can be learnt directly from the javadoc source documentation.

35 Storing and Loading DTs

This section only explores a feature that is particular to the Dynamic Types: debug-time information. If you should need more details about how to store and load DT from and to XML please refer to chapter IX.

35.1 How to explore a DT at debug time

Since the dynamic data of a DT is not stored as regular C++ attributes, it might be difficult to explore in the usual way from the debugger. Thus we have provided the DT base class with a Debug() method that can be called from the debugger environment and basically does two things:

Note that the XML load and store, including storing the debug file, will only work if the CLAM macro CLAM_USE_XML was set when compiling.

Previous Index Next