Previous articles introduced the C++ template metaprogramming language (MPL) and type lists. We showed how Boost.mpl helps us to write lists of interesting types, and how those techniques apply to MicroStation element types.
By writing a meta function that tests whether a type is present in a type list, we introduced a way to perform conditional class inheritance. Conditional class inheritance enables either dynamic or static polymorphism.
In this article we apply MicroStation element type lists to enable polymorphic C++ classes that correspond to MicroStation elements. The implemention is static, rather than dynamic. By that we mean that polymorphism is realised through C++ template classes. The templated classes implement a set of interface methods that are determined at compile-time. If you're interested in reading about dynamic polymorphism then skip to the preceding article.
A MicroStation DGN file contains one or more models.
A model may be 2D or 3D.
A model contains graphic objects termed elements.
MicroStation elements share some common characteristics, such as element type
and element ID, symbology and level.
The element ID is a 64-bit number, unique in each DGN file where elements are stored.
Graphic elements have geometric attributes, such as origin, length or rotation.
They have symbology such as colour, line thickness and level.
The element type informs us of the form an element can take: for example, a line, ellipse or text.
We model those elements with C++ classes: LineElm
, EllipseElm
and TextElm
.
For the purpose of this discussion we'll mention only a few MicroStation element types. In practise there are about one hundred types. Not all types indicate a graphic element — some types are used as data containers in a MicroStation DGN file. We'll focus on visible graphic elements.
The API that extracts geometric and other data is specific to each type of element.
A TextElm
's location is determined by a single coordinate, known in MicroStation as the
text origin, a 3D data point (stored in struct DPoint3d
).
A LineElm
is described by a vector of 3D data points;
an EllipseElm
is defined by its two axes.
The C++ classes that model MicroStation elements clearly need to be polymorphic:
they must expose the distinct characterists of each element type.
A TextElm
must be able to provide its origin and text content,
but we don't want those from a LineElm
.
A LineElm
must be able to give us the 3D coordinates of its segments,
but it can't tell us about its text content.
And so on: each class that represents a MicroStation element must exhibit
traits peculiar to that element.
Elements share some characteristics, such as symbology, but others are unique to that element type. Multiple class inheritance lets us model subtle differences quite well. Using type lists and MPL to control those subtleties via conditional class inheritance lets us automate trait selection. We'll focus on point elements as an example, to illustrate what we want to achieve.
The typelist that defines point elements is PointTypes
(see header file ElementTypes.h
).
We've chosen to include cell elements, shared cell elements, text elements and attribute elements (tags) in that list.
The MDL representation of an element is in struct MSElement
, which includes a
C-style union
of most element structs
.
MSElement
also stores MicroStation symbology and level information.
Each of those element types has the following traits in common …
DPoint3d
struct)
RotMatrix
rotation matrix)
double
Element
base class
The common properties in the Element
base class can be handled uniformly,
more or less, in that base class.
The point element traits provide the same result for each element type,
but the API for extracting those data are unique to each element …
Element | Trait | Extractor Function |
---|---|---|
Cell | Origin | mdlCell_extract |
Cell | Rotation | mdlCell_extract |
Cell | Scale | mdlCell_extract |
Shared | Origin | mdlSharedCell_extract |
Shared | Rotation | mdlSharedCell_extract |
Shared | Scale | mdlSharedCell_extract |
Text | Origin | mdlText_extractWide |
Text | Rotation | mdlText_extractWide |
Text | Scale | always 1.0 |
Tag | Origin | mdlTag_extract |
Tag | Rotation | mdlTag_extract |
Tag | Scale | always 1.0 |
We need a class hierarchy that abstracts the traits and hands each trait implementation to
the appropriate derived class CellHeaderElm
, TextElm
, etc.
There are two approaches we can use: virtual inheritance or the
curious recurring template pattern (CRTP).
Template metaprogramming enables static class polymorphism: a base class defines an interface, and derived classes implement that interface. This is static polymorphism, because the implementation functions are obtained via the class's inheritance chain at compile-time. In other words, something like this …
// Traits class specifies an interface
template <typename Derived>
struct IPointTraits
{
DPoint3d Origin () { return [what goes here?] }
RotMatrix Rotation () { return [what goes here?] }
double Scale () { return [what goes here?] };
};
Each interface method is specified, but what goes here? Thanks to the curious recurring template pattern, what goes there is the derived class implementation of that interface …
// Most-derived text class implements the interface struct TextElm : public IPointTraits <TextElm> { DPoint3d OriginImpl () { DPoint3d origin; mdlText_extractWide (&origin, …); return origin; } … etc }; // Most-derived tag class implements the interface struct TagElm : public IPointTraits <TagElm> { DPoint3d OriginImpl () { DPoint3d origin; mdlTag_extract (&origin, …); return origin; } … etc };
The curious recurring template pattern
(CRTP) provides static polymorphism.
Template programming lets us define the implementation statically (at compile time).
I'm not going to attempt to explain CRTP here, but I'll show what we can do.
Here's the CRTP implementation of the IPointTraitsCRTP
interface …
// base template class specifies an interface template<typename Derived> struct IPointTraitsCRTP { // Convenience function Derived& derived () { return static_cast<Derived&>(*this); } // public interface hands implementation to the Derived class DPoint3d Origin () { return derived ().OriginImpl (); } ...etc. }; // Most-derived text class implements the interface struct TextElm : public IPointTraitsCRTP<TextElm> { DPoint3d OriginImpl () { DPoint3d origin; mdlText_extractWide (&origin, …); return origin; } ...etc. }; // Most-derived tag class implements the interface struct TagElm : public IPointTraitsCRTP<TagElm> { DPoint3d OriginImpl () { DPoint3d origin; mdlTag_extract (&origin, …); return origin; } ...etc. };
The element class hierarchy has three levels …
Element
base class
TypedElement
inherits from Element
and is characterised by its MicroStation element type
TypedElement
also inherits from one or more traits template classes, such as AreaTypes
.
Those classes specify an interface to be inherited. For example the Area()
method
LineStringElm
, inherit from TypedElement
TextElm
, for example, provides the Text()
method
The MicroStation software developers kit (SDK) delivers the MicroStation Development Library (MDL) and the MicroStationAPI. MDL is a library of C-style functions; the MicroStationAPI provides a C++ interface. If you want more information about MicroStation or its SDK, contact Bentley Systems.
The Element
base class implements methods common to all MicroStation element types, such as
Element ID, element type and symbology.
It owns an EditElemHandle
(from the MicroStationAPI), which we expect to be initialised with an ElementRef
and DgnModelRef
when the class is constructed.
Type MSElementTypes
appears frequently in these articles.
It is an enum
introduced by MDL header file <mselems.h>
.
class Element { private: // No copy or assignment, which are disallowed in EditElemHandle Element (Element const&); Element& operator=(Element const&); protected: Bentley::Ustn::Element::EditElemHandle eeh_; public: // Construction Element (MSElementTypes MetaType) : MetaType_ (MetaType) { } Element (ElementRef elRef, DgnModelRefP modelRef, MSElementTypes metaType) : eeh_ (elRef, modelRef), MetaType_ (metaType) { } Element (MSElementCP el, DgnModelRefP modelRef, MSElementTypes metaType) : eeh_ (el, modelRef), MetaType_ (metaType) { } Element (MSElementDescrP descr, bool owned, bool isUnmodified, MSElementTypes metaType) : eeh_ (descr, owned, isUnmodified), MetaType_ (metaType) { } virtual ~Element () {} // Implementation common to all elements const MSElementTypes MetaType_; MSElementTypes MetaType () { return MetaType_; } MSElementTypes Type () { const MSElementTypes& t = IsValid ()? static_cast <MSElementTypes> (elementRef_getElemType (eeh_.GetElemRef ())) : static_cast<MSElementTypes>(0); return t; } bool IsValid () { return eeh_.IsValid (); } // Check IsValid before calling any of the following … MSElementDescrP GetElemDescrP () { return eeh_.GetElemDescrP (); } MSElementP GetElementP () { return eeh_.GetElementP (); } ElementRef GetElementRef () { return eeh_.GetElemRef (); } ElementID GetElementID () { return mdlElement_getID (eeh_.GetElementP ()); } DgnModelRefP GetModelRef () { return eeh_.GetModelRef (); } std::wstring Describe () { if (IsValid ()) { using namespace Bentley::Ustn::Element; Handler& handler = eeh_.GetHandler (); Bentley::WString wcDescr; enum StringLength { DesiredLength = 128, // See MicroStationAPI documentation }; handler.GetDescription (eeh_, wcDescr, DesiredLength); return std::wstring (wcDescr.c_str ()); } //else return std::wstring (L"Element not initialised"); } };
The TypedElement
class provides the inheritance framework.
It inherits from all the interface classes (ILinear
etc.).
However, the inheritance of a set of meaningful interface methods takes place only if the
element type (elemType
) enables that interface.
It the element does not match the interface type list then TypedElement
inherits an empty class, which the C++ optimiser will magically erase.
As discussed in
previous articles, the conditional inheritance is a compile-time decision.
Once an element class, such as LineStringElm
, is instantiated all its interface methods
are defined.
As we are using template classes to implement polymorphism using
curious recurring template pattern (CRTP),
those interface methods are chosen at
compile-time. There are no virtual methods and the class does not have a vtable.
template <typename elemType, typename Derived> class TypedElement : public Element, public elemType, public ILinear<elemType, Derived>, public IArea<elemType, Derived>, public IPoint<elemType, Derived>, public IText<elemType, Derived> { static const MSElementTypes MetaType_ = static_cast<MSElementTypes>(elemType::value); public: // Construction TypedElement () : Element (MetaType_) { } TypedElement (ElementRef elRef, DgnModelRefP modelRef) : Element (elRef, modelRef, MetaType_) { } TypedElement (MSElementCP el, DgnModelRefP modelRef) : Element (el, modelRef, MetaType_) { } TypedElement (MSElementDescrP descr, bool owned, bool isUnmodified) : Element (descr, owned, isUnmodified, MetaType_) { } virtual ~TypedElement () {} // Implementation // The following static bool functions provide run-time // access to facts established at compile-time // Complex elements static bool IsComplex () { using namespace LASolutions::ElementTypes; return IsTypeInList<ComplexTypes, elemType>::value; } // Linear elements static bool IsLinear () { using namespace LASolutions::ElementTypes; return IsTypeInList<LinearTypes, elemType>::value; } // Point elements static bool IsPoint () { using namespace LASolutions::ElementTypes; return IsTypeInList<PointTypes, elemType>::value; } // Area elements static bool HasArea () { using namespace LASolutions::ElementTypes; return IsTypeInList<AreaTypes, elemType>::value; } // Text and tag elements static bool HasText () { using namespace LASolutions::ElementTypes; return IsTypeInList<TextTypes, elemType>::value; } };
There are a number of interface traits classes.
Each interface class focusses on a particular trait exhibited by MicroStation elements.
For example, the IPoint
class defines the set of methods to be provided by
MicroStation elements that are 'point' objects.
That term includes those elements that need a single point (DPoint3d
) coordinate to
determine their location in a DGN model.
For example, text elements, cell elements and zero-length lines are point objects.
Other interfaces define methods to be implemented by other element classes.
We won't discuss them all here because of their repetitive nature, at least
for the purpose of understanding the C++ technicalities of each interface.
However, no two interfaces define the same method.
For example, IPoint
defines the Origin
method,
but no other interface defines an Origin
method.
Each interface class is a template class. There are three parts to each interface template …
The generic template is never instantiated, because the specialisations are always a better choice for the compiler.
The template specialisation that defines interface methods is matched when the element type is found
in the list of element types for that interface.
For example, the IPoint
interface matches an element type in the PointTypes
list.
If the element type is not in the list, then the second specialisation is instantiated.
But the unmatched specialisation is an empty class, and so it defines no interface methods.
Here's the unimpressive generic template class. It doesn't need a body because it's never instantiated …
template<typename elemType, typename Derived, typename enabler = void> struct IPoint;
Here's the class template specialisation for an element type that is found in the element type list …
template<typename elemType, typename Derived> struct IPoint < elemType, Derived typename boost::enable_if < typename LASolutions::ElementTypes::IsTypeInList < LASolutions::ElementTypes::PointTypes, elemType >::type >::type > { // Helper method Derived& derived () { return static_cast<Derived&>(*this); } // Interface methods DPoint3d Origin () { return derived ().OriginImpl (); } };
Working from innermost to outermost, essential parts of that template are …
LASolutions::ElementTypes::PointTypes
elemType
to test
LASolutions::ElementTypes::IsTypeInList
,
which resolves to true
if elemType
is found in the list
boost::enable_if
. This has the magical effect
of permitting the instantiation of the template if its argument resolves to true
<typename elemType> struct IPoint
The class body contains the interface methods (in this example there is only one).
It also contains the derived()
helper function, for internal use only …
Origin()
function
derived()
function
The derived()
function yields a reference to the most-derived class instance.
The most-derived class implements the interface methods, which the traits class calls directly
(i.e. there are no virtual functions).
Here's the class template specialisation for an element type that is not found in the element type list …
template<typename elemType, typename Derived>
struct IPoint
<
elemType, Derived
typename boost::enable_if
< typename boost::mpl::not_
<
LASolutions::ElementTypes::IsTypeInList
<
LASolutions::ElementTypes::PointTypes, elemType
>
>::type
>::type
>
{
// No interface methods!
};
Working from innermost to outermost, essential parts of that template are …
LASolutions::ElementTypes::PointTypes
elemType
to test
LASolutions::ElementTypes::IsTypeInList
,
which resolves to false
if elemType
is not found in the list
boost::mpl::not_
, which simply negates the enclosed test
boost::enable_if
. This has the magical effect
of denying the instantiation of the template if its argument resolves to false
<typename elemType> struct IPoint
The empty class body contains no interface methods.
A concrete class models a MicroStation element.
For example, LineStringElm
and TextElm
represent a MicroStation line-string element and text element respectively.
The concrete class is the end product of a chain of inheritance that includes …
Element
base class
TypedElement
intermediate class
IPoint
or IText
Note that the class TextElm
is passed as a template argument to TypedElement
,
which in turn passes it to each interface template class.
This is the
curious recurring template pattern.
class TextElm : public TypedElement<LASolutions::ElementTypes::TextType, TextElm> { public: // Construction TextElm () {} TextElm (ElementRef elRef, DgnModelRefP modelRef) : TypedElement (elRef, modelRef) { } TextElm (MSElementCP el, DgnModelRefP modelRef) : TypedElement (el, modelRef) { } TextElm (MSElementDescrP descr, bool owned, bool isUnmodified) : TypedElement (descr, owned, isUnmodified) { } virtual ~TextElm () {} // Implementation // Origin() is defined by the IPoint interface and calls this implementation method DPoint3d OriginImpl () { DPoint3d origin = { 0.0, 0.0, 0.0 }; // MDL methods not shown return origin; } // Text() is defined by the IText interface and calls this implementation method std::wstring TextImpl () { // MDL methods not shown return std::wstring (L"Write this code!"); } };
The header file provides several type definitions. These include smart pointer types, anticipating the use of these classes in standard C++ collection classes and their manufacture by a class factory …
typedef boost::shared_ptr<Element> ElementPtr; typedef boost::shared_ptr<TextElm> TextElmPtr; typedef boost::shared_ptr<LineStringElm> LineStringElmPtr; typedef boost::shared_ptr<EllipseElm> EllipseElmPtr; typedef boost::shared_ptr<CellHeaderElm> CellHeaderElmPtr; typedef boost::shared_ptr<LineElm> LineElmPtr;
Index | |
Type Lists and the MPL | |
Conditional Inheritance and the MPL | |
Polymorphic Classes for MicroStation Elements | |
Dynamic Polymorphic Element header file overview | |
Static Polymorphic Element header file overview | |
Element Factory | |
Development Tool Versions |
Post questions about MicroStation programming to the MicroStation Programming Forum.