Workspace 7.0.2
|
The following are suggested pre-reading for this tutorial:
You can do a lot with the built-in data types and a few simple custom data types of your own. Nevertheless, sometimes your custom data type has more of a hierarchical nature to it. You might want to build up a custom data type from a collection of simpler types, and then use this custom data type as a sub-component of a higher level data type, and so on.
Assembling a custom data type in this way is called composing an object. The main advantage of building up objects in this way is that you can re-use the smaller components to compose a variety of higher level components with relatively little effort. For example, in 3D model applications, a three-element vector will frequently be re-used to specify things like translations, rotations and scale factors. These three things can, in turn, be assembled together to specify a general transformation.
When using custom data types with Workspace, there are some more reasons why re-using objects to compose higher level ones is advantageous. This tutorial shows how to compose objects and explains the advantages of doing so.
The Writing a Workspace Datatype tutorial showed how to create your own custom data type containing two values. We will now see how to do this as an object group and then show how to compose a parent group containing some of these.
Workspace provides a special class called ObjectGroup for composing hierarchical data objects. The best way to understand it is to see it in action, so let's have a look at the new pair class:
There are three key things to notice about this class compared with the equivalent in the previous tutorials. Firstly, there is now base class, namely the ObjectGroup class provided by Workspace. Secondly, instead of holding two integers, the class holds two TypedObject data members. Finally, there is a virtual clone()
function. There are also some more header files included at the top, but we'll cover those shortly. We will also talk about the private setObjects()
function a bit later.
The ObjectGroup class provides a few functions for manipulating the data you add to the group. Generally, you won't need most of them for basic use, so we'll skip over these for now. The important difference is that our class now holds TypedObject's
instead of int's
. For our client code, this means we have to treat the data members as pointers to integers instead of as integers directly. Other than that though, we can do everything with this class that we could do with our earlier versions of the class.
The Workspace/DataExecution/DataObjects/objectgroup.h header defines the ObjectGroup base class for us. It also includes the Workspace/DataExecution/DataObjects/datafactorytraits.h header indirectly, so we don't need to explicitly add that header ourselves. The Workspace/DataExecution/DataObjects/typedobject.h header is needed because our class now holds TypedObject's
instead of int's
.
The implementation file for the MyPair
class starts out like the previous tutorials:
The three constructors, however, are a little different to before:
The copy constructor at (1) uses the default
ObjectGroup
constructor. This is a bit unusual for C++, but because of how the ObjectGroup
class works, it makes sense for us here. Some platforms will issue a compiler warning if you omit this, so it is a good habit to explicitly use the default constructor like this in your ObjectGroup
subclasses for their copy constructors.
For the default constructor, we still assign default values, but now we also call a function called setObjects()
in the body of the constructor. We do a similar thing with the other two constructors, only the valueA
and valueB
variables are set from the parameters passed to the constructor.
The purpose of the setObjects()
function is to inform the ObjectGroup
base class about the two data objects the class defines:
The add()
function takes two parameters. The first is the name to use for the data object being added to the group and the second parameter is the data object to add. The name must not already have been used by another object already added to the group. Similarly, you should not add a data object to a group more than once. Once an object is added to a group, it is not normally removed. It should also be noted that the name cannot be changed, but you can iterate over the objects held by an ObjectGroup and retrieve the names of each one.
The only thing left to implement is the clone()
function and to close off the namespaces:
The clone()
function is almost always implemented this way. It merely creates a copy of the object and returns it. Note that this approach requires you to define a copy constructor, but you can do something totally different if you want. The important thing to remember is that the function is expected to return a complete clone of the object, so you should ensure that the object returned is indeed logically the same as the one making the copy.
The rest of the implementation file looks familiar from previous tutorials:
The advantage of using object groups is that if your group is composed of just the built in data types or of types that are themselves already serializable, then you get serialization for your object group class for free. Workspace uses intelligent code selection based on C++ templates to detect when your class has ObjectGroup
as a base class and defines serialization functions for you automatically without you having to write a single line of code. This is especially useful when your group has a large number of data objects!
One of the more powerful features of object groups is that you can nest them to any level you like. To show an example, we will now define a new class as an object group containing three MyPair
data objects. We will also take the opportunity to show how to define the class in a more modular way (ie with better encapsulation):
Our MyGroup
class uses ObjectGroup
as a base class, just like we did for MyPair
, but there does not appear to be a single data object, only the private pImpl_
object. We've used this pattern once before in the Writing a Simple Workspace Plugin tutorial for our Workspace operation class to hide the details of how the class is constructed, and we use it again here for the same reason. We will hide the actual data objects and only provide access to them with the three set/get functions shown. This generally makes your code more portable and restricts the number of other files that need to be rebuilt any time you change something in your class.
So let's have a look at the implementation file to see just how MyGroup
is defined:
The first thing to notice is that we have moved the Workspace/DataExecution/DataObjects/typedobject.h header here instead of needing to put it in the header file for MyGroup
. Other than that though, the top of this implementation file is more or less the same as that for MyPair
. Next, however, we define the MyGroupImpl
class:
You can see that this implementation class is similar in style to the way we defined MyPair
. It holds three data objects: foo_
, bar_
and gus_
, each of which is a MyPair
object and also an object group. This is how you create hierarchical group structures, by adding data objects for the sub groups rather than sub groups directly (ie use TypedObject<SomeGroup>
instead of just plain SomeGroup
). The definition of the MyGroupImpl
class is very similar to what we did for MyPair:
Just like before, we supply values to the data objects in the constructor and call a common setObjects()
function in the body of each constructor to add the data objects to the group. The setObjects()
function assigns names to each data object we add.
We next define the MyGroup
constructors and destructor:
The constructors essentially just create the private pImpl_
data member of the class and the destructor deletes it again. You can more or less cut and paste these and just change the names for your own classes in most cases. Notice how the copy constructor also explicitly calls the ObjectGroup
default constructor, as we did for the MyPair
class earlier.
The assignment operator comes next:
This rather awkward but short function is simply re-using much of the code from the constructors to create a copy of the source object. This pattern is again usually able to be cut and pasted with the names changed for your own classes.
The clone()
function is just like what we used for the MyPair
class too:
The remaining functions are just set/get functions on the three data objects. Let's look at those for foo_
first:
Because our MyGroupImpl
class holds data objects and not MyPair
objects directly, the setFoo()
function first has to ensure that the data object actually points to a valid MyPair
. We do that by calling foo_'s
ensureHasData()
function. If foo_
already has data, then ensureHasData()
will do nothing, but if foo_
does not yet point to valid data then ensureHasData()
will create the data for it. When the data foo_
points to is an object group, the ensureHasData()
function fully populates any child objects of the group recursively.
Once we know we have valid data in foo_
we assign it the value passed to setFoo()
. For the getFoo()
function, we set a pre-condition that getFoo()
should only be called when it is guaranteed that foo_
has data. We could instead choose to create default data if none is present or even to return a copy of a temporary with a few small modifications. But for the purpose of this tutorial, we choose the simplest option and just use an assert()
to verify the pre-condition.
The code for bar_
and gus_
is entirely analogous (these are the last things we need to define, so we also close off the namespaces and add the usual DEFINE_WORKSPACE_DATA_FACTORY macro):
As you can see, defining a hierarchical group structure is not that much different to defining a simple one-level group. Again, we get all the serialization functionality for free, but we also gain further advantages which we will discuss in the next section.
Using object groups can be a useful way to get automatic serialization of complex data types. This is an advantage from the developer's point of view. There is another big reason for favouring object groups though, and that is because it allows users to build up an object from its components. The user might be defining those components in unusual ways and not all at once. If any of the components require large amounts of memory to store, the user might even be sharing data objects between different object groups. These things can be done very easily with the Workspace editor application. Indeed, Workspace defines a dedicated ComposeGroup operation specifically to allow the user to build up object groups from their subcomponents.
Another great reason for using object groups is that Workspace knows how to present them as a hierarchical tree of widgets in GUI applications. Workspace interrogates the object group about the name and type of each component in the group heirarchy and populates the appropriate items in the tree representing the group. It can do this to any arbitrary level of nesting of subgroups. Thus, using object groups for your data types gives your users instant access to them in the GUI application without you the developer having to write any code for it in most cases.
This tutorial has introduced you to the essentials of composing object groups to create hierarchical custom data types. The main points to remember are the following:
clone()
function to replicate themselves.