On the other hand, to define logical or arithmetic operators between these objects, we have to work with references: We cannot define
inline AndOfTwoCuts& operator&& (const BaseCut *lhs_, const BaseCut *rhs_) // does not work! { return *new AndOfTwoCuts (*lhs_, *rhs_); }
So, if we want not to have to write
// If SFH1F takes only pointers SFH1F *h1 = new SFH1F ("JPsiElasticTT", "#muODS: m_{J/#Psi}: elastic production, (#mu-#mu)", 40, 2., 4., *this, jpsiMass, &(*jpsiElastic&&*jpsiTrackTrack&&*jpsiNoCosmic&&*jpsi2mu));
we would have to use references everywhere, but then we cannot pass NULL pointers anymore, at least not easily (a possible way out would be to use an ugly, dirty, mean trick like *(static_cast<BaseCut *>(0))
).
The logical way out would be to provide constructors with all posible combinations of pointers and references, which would mean increasing the number of constructors by something like a factor 16.
A better possibility is to introduce a class FloatFunPoR (which stands for "FloatFun Pointer Or Reference"). This class has two constructors, taking a pointer or a reference to a FloatFun, and store the pointer (in member FloatFunPoR:pff, meaning "Pointer to FitFun"). These constructors constitute automatic conversion operators, so that if we now call the SFH1F constructor with either a pointer or a reference to a FloatFun object (or to a subclass of it), this is automatically converted to an FloatFunPoR object, which is then used by the SFH1F constructor to extract the pointer to a FloatFun.
Clever, isn't it?
(We haven't seen this trick anywhere else. Maybe we should have it patented? If amazon can get 1-click shopping patented...)
Long answer: Needs to be written...
IDTrackIterator trackIter (ntuple); IpzFun ipzfun (ntuple); PtFun ptfun (ntuple, &trackIter);
IDTrackIterator *trackIter = new IDTrackIterator (ntuple); IpzFun *ipzfun = new IpzFun (ntuple); PtFun *ptfunPtFun = new PtFun (ntuple, trackIter);
IDTrackIterator& trackIter = *new IDTrackIterator (ntuple); IpzFun& ipzfun = *new IpzFun (ntuple); PtFun& ptfunPtFun = *new PtFun (ntuple, &trackIter);
The problem with the first solution, using automatic variables, is this: Typically we (seem to) need all these objects only within one function, e.g. in the constructor of an EventLoop, where we book all histograms.
Automatic variables would be destroyed at the end of this function. However, the self-filling histograms store only pointers to these objects instead of copying them, so these pointers would become invalid.
But the objects are needed later to fill the histograms, so they must not be deleted too early. Allowing the use of automatic variables puts this burden on the shoulder of the user, who would have to make sure the objects live long enough, e.g. by making them data members of her EventLoop subclass.
This is burdensome and error-prone.
If, however, the objects are created with "new", they live until they are explicitly destroyed, which normally happens only when the program terminates.
Another solution would be that the self-filling histograms keep a copy of the objects. This needs an additional "clone()" method, because we need a sort of polymorphic copying. The resulting classes would look like this:
class IpzFun : public FloatFun { public: IpzFun(Ntuple* ntuple) : nt(ntuple) {}; virtual float operator() () const {return nt->ipz;} virtual IpzFun& clone() () const {return new IpzFun(*this);} protected: Ntuple* nt; };
The disadvantage of this method is threefold:
Our first design actually used the clone approach, but these disadvantages convinced us to go the other way.
In our opinion, OOP is about maintainability of code. We think that our toolbox allows you to write code that is better structured, has smaller programming units, and is therefore better to understand and modify.
When you start an analysis, you rarely know where you'll end up. The code tends to grow with time, gets more and more complicated as more and more cuts and control plots are added. This leads to software entropy, which you'll often not notice until it's too late (typically 2 weeks before you have to present your result at a conference, when you are asked to change the order of your cuts or introduce a new cut right at the beginning of your cut chain).
Reordering cuts is a real pain if you nest cuts in if-then-else structures. We find that this is much easier in our framework.
Once you start to bin your J/psi events in 10 t bins times 5 W bins, and want to plots the mass peaks for 5 different cut stages and 5 datasets (data plus 4 Monte Carlos), you'll find that managing these 6250 (count them!) histograms can be much easier by using a two-dimensional array of MatrixOfHistogram objects than hassling with 4-dimansional histogram arrays, 5 levels of if-then-else indentations and all that, 2 levels of loops and all that.
Also, plotting and fitting these histograms can be done quite efficiently in our framework.
First, it's a pain to write the parser, while the compiler already has one.
Second, using objects instead of strings makes it possible to detect many errors already at compile time rather than run time. This is a general idea behind the type-safe system of C++, and one of the main advantages over other languages like Smalltalk.
Since by using C++ we already have all the disadvantages of C++, like clumsy notation, the necessity to care about variable definitions and memory leaks, we think we should also take advantage of C++'s benefits. Otherwise, we'd be much better off using another language in the first place.
Anyway, since multiple inheritance is used a lot in the root source code (by modern standards probably overused), we figured we didn't have to restrain ourselves too much.
Long answer:
In general we tried to adhere to a number of guidelines when we defined constructors:
For classes that are derived from root classes, provide constructors that take the same arguments as in root, put additional arguments (if any) behind the arguments of the root classes. Similarly, for derived classes in general we try to put additional arguments for conistructors at the end.
For SFSetOfHistograms and SFMatrixOfHistograms, we wanted to provide the same constructors as for class SFH1F, plus the BiningFun objects.
At that time, we always passed references to FloatFun and BaseCut objects, so we couldn't have default arguments, and at that time we didn't have FillIterator objects. If we did it again, we'd do it differently, but changing the order of the arguments would break all user code, and we don't think that's worth it.
In short: We did it wrong, but to correct it would mean a lot of work for our users, who are too dear to us to demand that.