The user sends a message to an object and gets a reply. The message may result in the object changing state. The designer of the object decides what messages are allowed and how the object will react and so can test it before the user gets hold of it.
In this series of OO concept talks we shall illustrate some of the concepts by developing a series of track objects. We can make a start on this now, by deciding a few fundamentals about such objects. Clearly one of the properties of a track is its energy. If we have a track called MyTrack, then we could ask it for its energy using the assignment:-
energy = MyTrack.GetEnergy();There is quite a lot to take in here:-
MyTrack.SetEnergy(energy);Now we have to supply additional information, via the argument list, the energy we want to set.
energy = MyTracks[5].GetEnergy();Now we first select the array element, which in C++ uses [..] so as not to confuse it with a function call (in FORTRAN they can look very similar!) and then the selected object is sent the GetEnergy message.
It is unlikely that a fixed length array of objects is going to be satisfactory in most cases. Normally the number of objects is not known until execution time. This is the problem that FORTRAN memory managers such as ZEBRA were written to solve, and the solution in OO is much the same: create the object at execution time by allocating memory and then access the object via some kind of a pointer (in ZEBRA terminology objects are banks and pointers are links). Pointers are discussed in more detail in the later OO concept topic Pointers & References but it is worthwhile introducing a bit of notation now. If, instead of a track object, we have a pointer to it, say MyTrackPtr, then to get its energy:-
energy = MyTrackPtr->GetEnergy();all that changes is that the dot becomes a ->. There is no convention that says pointers have to be called Ptr or something, indeed, pointers are so prolific in C++ code that names rarely indicate that they are, but its something to watch out for when reading code!
Just as FORTRAN memory managers allow banks to be organised into data structures, so in OO there are special objects, called containers whose function it is to form collections of objects. These collections include such organisations as arrays, lists, queues and trees. So the passive central data structure of a FORTRAN program now becomes a dynamic structure of objects. Containers is the subject of the later OO concept talk Containers.
Let's consider this for our track objects. Suppose we have another type of object, a vertex, and further suppose each track can be sent the message GetParentVertex that will return a pointer to the vertex that produced it. We might ask that vertex for its interaction type by:-
interaction = MyTrackPtr->GetParentVertex()->GetInteractionType();The member selection operator -> works left to right, so the above means:-
interaction = ( MyTrackPtr->GetParentVertex() ) -> GetInteractionType();i.e. get the vertex pointer from MyTrack and then ask the vertex for its interaction type. In principle, we could have given the track object the member function GetInteractionType, but that is not the OO way, the user knows that tracks connect to vertices, so its part of the model (the abstraction) so its not the track's job to talk on the vertex on the user's behalf. That would require a track to know everything about a vertex. On the other hand, a track object may privately own objects that are part of what makes it a track. Such objects are would be part of the implementation (i.e. should be encapsulated) and in this case messages would be sent internally and answers returned, rather than returning the objects themselves, The litmus test is always the question: Is this part of the model or part of the implementation?
Continuing with our track object, lets look at a (slightly incomplete) definition of a track class. To flesh out the track model a little more we will add both mass and momentum to the model:-
class Track { Float_t fEnergy; Float_t fMass; Float_t fMomentum; Float_t GetEnergy() { return fEnergy; } void SetEnergy( Float_t energy ) { fEnergy = energy; fMomentum = sqrt( fEnergy*fEnergy - fMass*fMass ); } };We will pick this apart, statement by statement.
class Track {The first line declares that what follows is a class called Track. This is followed by a compound statement, that is to say a series of statements within a pair of curly braces. Convention dictates that the open brace ends the line.
Float_t fEnergy; Float_t fMass; Float_t fMomentum;Next come a list of data members, that is to say the data each object owns. In this case 3 Float_t variables, fEnergy, fMass, fMomentum. Float_t is one of ROOT's Data Types, but its just a bread and butter floating point number like FORTRAN's REAL. Each variable starts with a lowercase f. This is a naming convention and is important here so that, when reading class code, it is at once obvious which are data members, and which are just local variables. Its rather like having a convention in FORTRAN to differentiate between COMMON variables and local variables. The convention used here is the one used by ROOT. The f stands for field, because an object is rather like a record with a set of fields. Another popular convention has a lowercase m for member.
Float_t GetEnergy() { return fEnergy; }Here we see a member function. The function takes no arguments and returns a Float_t. The function itself simply uses the return statement to return the object's energy. "Getters" for the object's other data members could be written in the same manner.
void SetEnergy( Float_t energy ) { fEnergy = energy; fMomentum = sqrt( fEnergy*fEnergy - fMass*fMass ); }The second member function returns void (i.e. nothing). It has a single Float_t argument. This time it takes two statements to realise, the first updates the energy and the second the momentum. Clearly c=1 in the object's units of mass and energy. Note how the momentum has been changed to ensure that the object's internal data remains consistent. The user cannot break the object the model. Of course you should by now be smugly saying what if the user supplies an energy that is less than the particle's rest mass? O.K., so the model does break, when writing this function we should have thought of that. Once again: its down to the class designer to think of everything!
O.K., now we have our object factory, so lets build some objects:-
Track MyTrack;This creates a single track object named MyTrack. The syntax is identical to that for defining a built-in data type. Part of the C++ philosophy is that user data types, which objects are, become an extension of the language and are manipulated using the same syntax.
Track MyTracks[10];This builds 10 objects, it would have been just as easy to make 1,000 or 1,000,000!
Track *MyTrackPtr = new Track;This last example shows the creation of an object dynamically. Despite its appearance, new is an operator that creates an object of the class supplied. The result of the expression is the address of the newly formed object. In the previous two examples the compiler/linker knows about the objects and where they will be in memory. This time the address of the object isn't known until the statement is executed so we need a pointer to a track object to hold it. That is what the * at the start of the name means. So this statement should be read "Define a pointer to Track and initialise it with the address of a new Track object".
If we were to divide out our Track class into two files then Track.h would contain:-
class Track { Float_t fEnergy; Float_t fMass; Float_t fMomentum; Float_t GetEnergy(); void SetEnergy( Float_t energy); };i.e. a declaration of all its data and functions, but no function code. Track.cxx would contain:-
#include "Track.h" Float_t Track::GetEnergy() { return fEnergy; } void Track::SetEnergy( Float_t energy ) { fEnergy = energy; fMomentum = sqrt( fEnergy*fEnergy - fMass*fMass ); }Note that the function names have a preceding Track::. :: is called the scoping operator: Track:: means "is a member of Track". This was implied when the function appears inside the class statement (indeed there Track:: would mean Track::Track:: !), but has to be explicitly stated when outside. Note also that the implementation file has to include the header file as it has to know how the class is defined.
When coding real classes, simply member functions such as these "getters" and "setters" are normally left in the header file for reasons of performance. This will be explained in the OO topic Private & Public