Introduction
This article is the natural evolution of the "comparing coding approach" series, and can be an answer to the corresponding article of Dave_H, with the same subject.
Disclaimer (or ... some opinion about)
This article doesn't want to be a part in a religion battle: I already explained - I hope - that I'm not interested in this kind of religious disputes. My interest is to evaluate the possible alternatives.
What Dave presented is a pretty good and elegant solution fully based on the generic programming techniques, but with the need of some RTTI features to handle polymorphism. I also used those techniques, mainly in code like the one I presented to create W32 reference counting wrappers. Here, I just want to show the "other face of the medal" using mainly RTTI and OOP, with some template decoration. Whenever using one or the other ... mostly depend on the need of the application you are deploying, and ... why not, on your personal opinions.
All those "template based" solution have in common the so called "curiously recurring template pattern": a template class takes as a template parameter a class that is supposed to be derived from the template class itself. This allows the base to call functions that are supposed to be implemented in the derived by casting the base "this
" into a derived*
. See the cited articles for all the samples. Briefly:
template <class TDerived>
class CBase
{
void basefn()
{ static_cast<TDerived*>(this)->derivedfn(); }
};
class CDerived: public CBase<CDerived>
{
void derivedfn() { }
};
Another approach, use template derived instead of template bases, like this other article. Briefly:
class CBase
{
void basefunction() { }
};
template<class TBase>
class CDecorator: public TBase
{
void decorate() { basefunction(); }
};
The limit of both this approaches is the impossibility to handle runtime polymorphism: objects of different types cooperating in the same data structure.
This situation (as stated in the cited article) requires necessarily RTTI and dynamic_cast
.
But here comes the point: stated that you cannot do without V-tables and RTTI descriptors, do you still need CRTP?
After all, it allows to call functions in the derived from a base. But ... this can be done by virtual functions.
So, here I propose a way to implement that paradigm using generic code as a minimum, and most of the code using traditional OOP.
The result is an implementation that is translatable by the compiler as a set of classes. Not code that is re-injected every time a new class requires it.
This makes me able to separate the code of the implementations (to be placed into a library) from the code of the application (to be placed in their respective projects). It will be the linker to bind (various) OBJs into EXE, not the compiler to generate all the code into a single (or few) OBJs.
The main design rules
There are people who consider insane multiple inheritance, but consider "good things" templates with many parameters. I don't see any particular difference between the two: they are perfectly dual. So, I'll use multiple inheritance and multiple parameter templates, mainly referring the "multiple partial implementation" methodology described in the "comparing" article.
The general scheme
In this implementation, I'll follow this general scheme:
I don't want to worry about true or assumed inefficiency of inheritance, virtual bases, or multiple inheritance, rather than template injections. I just want to have a code that can be independently translated as possible, using all the available features to keep the design enough elegant and modular. Performance is not the "key factor", although I will pay an attention to that also.
Object ownership
Since we're going to implement collection of polymorphic objects, we will need to refer them in terms of "pointer to common base". And this leads to the old problem "who owns that object?" (That is: who will have the right to destroy it?)
Although it is not a universal solution, reference counting is a good compromise. In this case, however, I'll not go through more or less complicated policy driven smart pointers, but through a "template and interface" mixing.
Composites - visitors
Composites are objects containing other objects. Their natural aggregation is a tree, but not a sort of std_like::tree<T>
(where T
elements are unaware of being part of the tree), but a tree composed by tree nodes, where the object themselves are the nodes, and are polymorphic.
Visitors are sort if object iterators, able to walk through the structure and dereference into the hold objects.
Data cast-ers
If the structures are built up in terms of common base pointers, accessing the object components (inside the derived classes) requires a dynamic_cast
from the common base into the required components. This (binary) code is clearly different from component to component, so this is a good point where templates have a real advantage: they can generate the code from a common source.
Collections
Collections are containers of homogeneous types, whose activities do not depend specifically on the hosted types, and where the hosted objects have no knowledge about the fact they are collected. So, collections can have a common interface, but they cannot have a common binary code, since this code depends on the hosted object size. So, templates are really effective as the implementation for that interface. Stated this point, STL collections play their right role, even in OOP applications!
Polymorphic collections, instead, can be implemented in terms of homogeneous collection of pointers to a common base. This consideration can lead to the idea of homogeneous collections as a particular case where a polymorphic collection is "accidentally" populated by objects of the same type. But there is a case when this cannot be done: arrays. Arrays require the homogeneous objects to be stored contiguously. (That is: "this+1" must be the next object, and this cannot be done by storing pointers.) And this is somewhat what makes templates always preferable for those tasks.
Now that all the arguments have been introduced, let's go to describe the proposed implementation.
Data Cast-er and cast-able visitors
Since all composites will live on the heap, a very basic data caster will be a small object having a pointer semantics that inject dynamic type conversion and perform the appropriate assignment by decorating (inheriting) a "castable" class.
struct
Ptr
, is - so - a "template decorator" for a generic TBase
that can be inherited by Ptr
, providing it some functions:
template<class T, class TBase>
struct Ptr: public TBase
{
typedef typename TBase::_TRefType _TRefBaseType;
typedef typename T _TPtrType;
...
};
Ptr
implements all assignment, copy, conversion, subscripting, dereferencing operators (all the classic operators that can take pointers as parameters) auto-converting _TBaseRefType
into _Type
by a dynamic_cast
mechanism.
In particular:
private:
friend struct Ptr;
T* _pCached;
public:
typedef typename TBase::_TRefType _TRefBaseType;
typedef typename T _TPtrType;
struct exception: public XException
{};
_TPtrType* _convert() const { return dynamic_cast<_TPtrType*>(_access()); }
Ptr() {_pCached = NULL;}
Ptr(const Ptr& p) { _assign(p._access()); _pCached = p._pCached; }
template<class A, class B>
Ptr(const Ptr<A,B>& p) { _assign(p._access()); _pCached = _convert(); }
Ptr(_TRefBaseType* p) { _assign(p); _pCached = _convert();}
Ptr& operator=(const Ptr& p)
{ _assign(p._access()); _pCached = p._pCached; return *this; }
Ptr& operator=(_TRefBaseType* p)
{ _assign(p); _pCached = _convert(); return *this; }
template<class A, class B>
Ptr& operator=(const Ptr<A,B>& p)
{ _assign(p._access()); _pCached = _convert(); return *this;}
operator _TPtrType*() const { return _pCached; }
_TPtrType* operator->() const
{if(!_pCached) ThrowExcp<exception>(); return _pCached; }
_TPtrType& operator*() const
{if(!_pCached) ThrowExcp<exception>(); return *_pCached; }
bool operator!() const { return !_pCached; }
bool operator~() const { return !_access(); }
void New() { _assign(new T); _pCached = _convert(); }
template<class I> void New(const I& i)
{ _assign(new T(i)); _pCached = _convert();}
Ptr& operator++() { _inc(); _pCached = _convert(); return *this; }
Ptr& operator--() { _dec(); _pCached = _convert(); return *this; }
Ptr operator++(int) { Ptr r(*this); _inc(); _pCached = _convert(); return r; }
Ptr operator--(int) { Ptr r(*this); _dec(); _pCached = _convert(); return r; }
_TPtrType& operator[](int i) const
{ _TPtrType* p = _convert(_subscript(i));
if(!p) ThrowExcp<exception>(); return *p; }
Practically, a pure "decorator", referring to the previous scheme. Note that _convert
is a dynamic cast from the type provided by the base and the type on that Ptr
operates, that is supposed to be somewhat derived from the one provided by the base.
The implementation of Ptr
keeps a T* _pChache
private value, and performs the type conversion during assignments or copy. All other "read" operators (like *
, ->
etc.) return the cached value. This contributes to limit the number of casts performed in a normal execution.
Note, also, that - although Ptr
is a template - no recursion has been done in its definition.
Reference countable objects
In perfect OOP style, let's start from an interface
.
First: interface
, for me, is nothing more than an alias for struct
. The use of this keyword is, as a consequence, pure suggestion. Although completely empty, it has a v-table, so it's always better to provide them with a virtual destructor. Does nothing ... but it's virtual. Is what is required to let delete
to properly work if called with an interface pointer!
A reference countable object can implement IReferable
providing a meaning to its abstract functions:
interface IReferable
{
private:
virtual void AddRef()=0;
virtual void Release()=0;
virtual void Delete()=0;
protected:
virtual ~IReferable() {}
}
They're all private, since no-one that is not explicit friend should call them directly.
And to have a smart pointer to it, we need a "castable visitor" that translates _assign
into Addref
and Release
.
Since this is specific for IReferable
, I defined it as Ireferable::XPtr
(internal struct
), and made it a friend of IReferable
(so that it can call the IReferable
function overrides).
struct XPtr
{
protected:
void _assign(IReferable* pR);
IReferable* _access() const;
public:
typedef IReferable _TRefType;
XPtr();
~XPtr();
private:
IReferable* _pRef;
};
where _assign
is coded as ...
void IReferable::XPtr::_assign(IReferable* pR)
{
if(pR) pR->AddRef();
if(_pRef) _pRef->Release();
_pRef = pR;
}
Since we are talking about a stand-alone object, it makes no sense to implement _inc
, _dec
and _subscript
.
Note that AddRef
must be called before Release
, to be sure that, in case pR == _pRef
, the pointed object is not destroyed after the Release
.
A smart pointer to IReferable
can be obtained as typedef Ptr<IReferable, IReferable::XPtr> PReferable
.
In general, given a CSomthingReferable
that inherits IReferable
, it is possible to define a Ptr<CSomethingReferable, IReferable::XPtr>
, whose *
and ->
operators will automatically cast the held object into a CSomethingReferable
.
IReferable implementation: EReferable
To avoid re-implementing the interface on every object, referable object can inherit an implemented IReferable
, like EReferable
.
It handles a reference counter and implements AddRef
and Release
by incrementing and decrementing it.
Since we cannot know how complex the inheritance graph for a compound object can be, but since this interface must be only once per instance, virtual inheritance is required.
AddRef
and Release
are implemented also by calling other virtual "hook" functions, declared as "do nothing", but overridable in the inherited objects: OnAddRef
, OnFirstRef
, OnRelease
, and OnLastRelease
.
Now, it is possible to define a referable class by virtually deriving from EReferable
and by defining its pointer as Ptr<yourclass, yourclass::XPtr>
.
A generic decorator for EReferable: CReferable<U>
For classes or simple types that cannot inherit from EReferable
, CReferable
does it by inheriting EReferable
, hosting an U
member, providing U
value semantics, and typedef
-ining a smart Ptr
.
template <class U>
class CReferable: public virtual EReferable
{
private:
U _u;
public:
friend class CReferable;
CReferable(const CReferable& c) { _u = c._u; }
CReferable(const U& u) { _u = u; }
CReferable& operator=(const CReferable& c) { _u = c._u; return *this; }
CReferable& operator=(const U& u) { _u = u; return *this; }
operator U&() {return _u;}
operator const U&() const {return _u;}
typedef NTypes::Ptr<CReferable, XPtr> Ptr;
};
Testing unit
Really simple:
using namespace GE_;
typedef NTypes::CReferable<int> CInt;
typedef NTypes::CReferable<double> CDouble;
int _tmain(int argc, _TCHAR* argv[])
{
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
try
{
STRACE(trc,1,("Try Block\n"))
CInt::Ptr pi;
pi.New(5);
int r = *pi;
trc("r = %d\n", r);
CDouble::Ptr pd = pi;
double d = *pd;
trc("will never reach this message");
}
catch(NTypes::XException* pe)
{
STRACE(trc,1,("Catch Block\n"))
pe;
}
trc("End Program\n");
return 0;
}
//// DEBUG OUTPUT ///
( 1).Begin test
( 1)..Try Block
( 1)..r = 5
( 0)...First reference for 002F0FF0
First-chance exception at 0x796be592 in CompVisit.exe: Microsoft C++ exception:
GE_::NTypes::Ptr<GE_::NTypes::CReferable<double>,
GE_::NTypes::IReferable::XPtr>::exception @ 0x0012fbc0.
( 0)...Last release for 002F0FF0
( 1)..Catch Block
( 1).End Program
Il programma "[2100] CompVisit.exe: Nativo" � terminato con il codice 0 (0x0).
Note that the expression pd = pi
is legal, since CInt::Ptr
and CDouble::Ptr
rely on IReferable::XPtr
, and both CInt
and CDouble
are EReferable
s.
What cannot be done is defeference pd
(with something like d = *pd
) when the sustained value is not a CDouble
: this - by design - throws the exception.
This is clearly a runtime type casting error, not a static casting as with templates. The compiler cannot detect it, but we can support polymorphism, like in this case:
CInt::Ptr pi, pj;
CDouble::Ptr pd;
pi.New(7);
pd = pi;
pj = pd;
int r = *pj;
pd.New(*pi);
double s = *pd;
Note that pj
receives from pd
what it received from pi
that created a CInt
. What makes pj
able to dereference, it is the nature of the sustained IReferable
. Not who passes the value.
Composites - Visitors
The behavior of a composite object can be described by one interface: ITreeNode
.
interface ITreeNode:
public virtual IReferable
{
virtual ITreeNode* GetFirstChild()=0;
virtual ITreeNode* GetLastChild()=0;
virtual int GetChildrenCount()=0;
virtual ITreeNode* GetParent()=0;
virtual ITreeNode* GetPrev()=0;
virtual ITreeNode* GetNext()=0;
virtual bool SetParent(ITreeNode* pParent,
ITreeNode* pPrevious=NULL)=0;
virtual ~ITreeNode() {}
typedef NTypes::Ptr<ITreeNode, IReferable::XPtr> Ptr;
struct XShallowIterate
{
...
};
typedef NTypes::Ptr<ITreeNode, XShallowIterate> ItShallow;
struct XDeepIterate
{
...
};
typedef NTypes::Ptr<ITreeNode, XDeepIterate> ItDeep;
};
It defines the functions necessary to navigate between parent, children, and siblings, and to setup this kind of structure.
It also defines its own pointer, the iterator for shallow iteration (iterate between siblings) and deep iteration (iterate into the children).
The difference is in the way the _inc
and _dec
functions are implemented in XShallowIterate
and XDeepIterate
.
The _subscript
function is also implemented as a repetitive iteration.
All that gives the ItShallow
and ItDeep
iterators, perfectly working ++
, --
and []
operators.
Functions for the implementors
Although the supplied functions are enough from an external point of view, they are not sufficient for an implementor: If I want to implement SetParent
, I must set my references to my new parent and siblings, but I also have to tell the parent and the siblings to set their references to myself.
To do this, there are two choices:
- Consider this problem as internal to the implementation: this means that an implemntor must access the data structures for the implementor of the parent and siblings. This can be done using protected members in a
class
or struct
, but this means that all the objects inside the same tree must, use, or derive, from the same implementation.
- Consider it as external to the implementation, by putting some more functions to the interface. But such functions should not be accessible from other objects than ... the implementors.
The first is probably the "quick and dirty way". The second is the "right way". But there is a problem: how should those functions be declared? They cannot be public
, since they must not be called by anyone. But they must be called by other instances of the derived classes. And even the protected
way is not good: it makes those functions inaccessible.
Thus, we need a proxy object (XProxy
), declared as protected
by the interface, but friend of the interface itself, having public functions: only derived classes can get such proxy, and its functions can call the interface protected
functions, that will be overridden by the derived implementations.
Note that the model of having empty interfaces defining abstract functions and manipulators, with partial implementations (the Exxx
classes) that can be inherited, is redundant: it allows different implementations of the same interface; but if you don't have such needs, you can avoid to define the Ixxx
interface, and start your deployment from the Exxx
classes directly.
This gives you one level less of flexibility, but makes you able to define functions in terms of Exxx*
(rather than Ixxx*)
and can avoid the need of proxies.
An ITreeNode implementation: ETreeNode
Since ITreeNode
requires IReferable
, ETreeNode
may inherit ... EReferable
, so that its implementation can dominate the IReferable
virtual base.
class ETreeNode:
public virtual ITreeNode,
public virtual EReferable
{
public:
virtual ITreeNode* GetFirstChild();
virtual ITreeNode* GetLastChild();
virtual ITreeNode* GetParent();
virtual ITreeNode* GetPrev();
virtual ITreeNode* GetNext();
virtual bool SetParent(ITreeNode* pParent,
ITreeNode* pPrevious=NULL);
protected:
virtual void SetFirstChild(ITreeNode* pN);
virtual void SetLastChild(ITreeNode* pN);
virtual void SetPrev(ITreeNode* pN);
virtual void SetNext(ITreeNode* pN);
protected:
virtual bool OnSettingParent(ITreeNode* pParent,
ITreeNode* pPrevious) {return true;}
virtual void OnSettedParent() {}
private:
ITreeNode::Ptr _pChildFirst, _pChildLast, _pNext;
ITreeNode *_pParent, *_pPrev;
protected:
ETreeNode();
virtual ~ETreeNode();
};
Note the difference in the ERrferable
design with respect to ETreeNode
:
IReferable
has all private functions with its private XPtr
as a friend and a public Ptr
decorating XPtr
. This is because I wanted IReferable::Ptr
to be the only object that can manipulate the IReferable
functions.
ITreeNode
has some public functions because I wanted those functions to be accessible from any situation. But it also has some private functions, with a protected proxy as a friend. This is to let only any descendants to be able to get the proxy and to call, through it, its functions.
SetParent
is the only function that can modify the structure. It's implemented through two hooks: OnSettingParent
and OnSettedParent
.
The first returns a bool
: if it returns false, the setting of the parent is refused. This gives a chance to implement sort of "compatibility checking" between objects that candidate to be related.
Testing unit and Sample
Let's imagine some shapes, each having a bounding rectangle. A shape can contain other shapes. We can then have various shapes: in particular: rectangle, circles, and groups.
A sample implementation is in "shapes.h".
Each shape has its own properties, and - by default - inherits the properties from the group it belongs.
interface IShape:
public virtual NTypes::ITreeNode
{
typedef NTypes::Ptr<IShape, XPtr> Ptr;
typedef NTypes::Ptr<IShape, XShallowIterate> ItShallow;
typedef NTypes::Ptr<IShape, XDeepIterate> ItDeep;
enum e_shapeattributes { attr_linecolor,
attr_fillcolor, attr_textcolor, attr__count };
virtual NTypes::UINT GetAttribute(e_shapeattributes a) =0;
virtual NTypes::UINT GetDeepAttribute(e_shapeattributes a) =0;
virtual void SetAttribute(e_shapeattributes attr, NTypes::UINT value)=0;
virtual void GetBound(SRect& r)=0;
virtual void SetBound(const SRect& r)=0;
virtual void Invalidate()=0;
};
GetDeepAttribute
should retrieve the required attribute, and - if it finds it is == -1
- get it from the parent group.
IShape
is implemented by EShape
, that inherits ETreeNode
also (to implement ITreeNode
).
From EShape
derives EGraphicShape
, that also inherits IDrawable
and overrides the OnSettingParent
hook by inhibiting a shape to be set as a child of a non-CGroup
object.
Finally, from EGraphicShape
, derives CCircle
, CRectangle
, and CGroup
. Each with its own Draw
function.
This gives, globally, this inheritance graph:
Note: I didn't depict the Ptr<...>
decorators: any class may have its own, redecorating XPtr
, XIt
... etc.
I also introduced IDrawer
and CDrawer
as do-nothing referrables (just to have something that emulates an HDC on an eventual Windows program).
The test code
I didn't deploy a full Windows application to draw picture, since the scope of this article is only to illustrate coding techniques. But a console main
can be the following:
int _tmain(int argc, _TCHAR* argv[])
{
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
STRACE(trc, 1, ("Begin test\n"))
NApp::CGroup::Ptr pDrawing;
pDrawing.New();
new NApp::CCircle(pDrawing, NApp::SRect(10,10,60,60));
new NApp::CCircle(pDrawing, NApp::SRect(80,10,140,60), 0, 7);
NApp::CGroup* pSubDraw = new NApp::CGroup;
pSubDraw->SetParent(pDrawing);
pSubDraw->SetAttribute(NApp::CGroup::attr_linecolor, 4);
new NApp::CRectangle(pSubDraw, NApp::SRect(20,70,130,120));
new NApp::CRectangle(pSubDraw, NApp::SRect(20,125,130,150), 5);
NApp::CDrawer::Ptr pDrw; pDrw.New();
pDrawing->Draw(pDrw);
std::cout << "\nPress any key ...\n";
while(!getch());
trc("End Program\n");
return 0;
}
that gives the following debug trace:
( 1).Begin test
( 0)..First reference for 002F10E0
( 0)..First reference for 002F11A8
( 0)..First reference for 002F1278
( 0)..First reference for 002F29D0
( 0)..First reference for 002F2A98
( 0)..First reference for 002F2B68
( 0)..First reference for 002F2C38
( 2)..Drawing class GE_::NApp::CGroup at 3084512
( 2)...Drawing class GE_::NApp::CGroup at 3090896
( 2)....Drawing class GE_::NApp::CRectangle at 3091304
( 2)....Drawing class GE_::NApp::CRectangle at 3091096
( 2)...Drawing class GE_::NApp::CCircle at 3084920
( 2)...Drawing class GE_::NApp::CCircle at 3084712
( 1).End Program
( 0)..Last release for 002F2C38
( 0)...002F2C38 suicide
( 0)..Last release for 002F10E0
( 0)...002F10E0 suicide
( 0)....Last release for 002F29D0
( 0).....002F29D0 suicide
( 0)......Last release for 002F1278
( 0).......002F1278 suicide
( 0)........Last release for 002F11A8
( 0).........002F11A8 suicide
( 0)......Last release for 002F2B68
( 0).......002F2B68 suicide
( 0)........Last release for 002F2A98
( 0).........002F2A98 suicide
and the following output:
Drawing group at 002F10E0 ...
Drawing group at 002F29D0 ...
Drawing rectangle at 002F2B68
Line color = 5 Fill color = 2 Text color = 3
placement: (20,125,130,150)
Drawing rectangle at 002F2A98
Line color = 4 Fill color = 2 Text color = 3
placement: (20,70,130,120)
end drawing group at 002F29D0 ...
Drawing circle at 002F1278
Line color = 0 Fill color = 7
placement: (80,10,140,60)
Drawing circle at 002F11A8
Line color = 1 Fill color = 2
placement: (10,10,60,60)
end drawing group at 002F10E0 ...
Press any key ...
Note: "Drawing" has been implemented by writing messages on std::cout.
<H2
>Conclusion
</H2
>
The code has been grouped into a static library and two projects using it.
The library is named LTypes, and the projects CompVisit and ReferableTest.
Of course, I don't pretend the projects to be useful to something. I hope this illustration can be used as a sample where multiple virtual inheritance and dominance can be used as valid substitute of templates, allowing a more "modular" code that can be compiled separately.