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 metafunction that tests whether a type is present in a type list, we introduced a way to write a C++ class having conditional inheritance. Conditional inheritance leaves us free to choose whether a class implements either dynamic or static polymorphism.
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.
In the examples we discuss in these articles,
we map MicroStation element types to C++ classes: for example, 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 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.
In this article we show how to apply MicroStation element type lists to enable polymorphic C++ classes that model MicroStation element types.
The C++ classes that model MicroStation elements clearly need to be polymorphic:
they must expose the distinct characteristics 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.
MicroStation elements share some characteristics, such as symbology, but other characteristics are unique to a particular element type. Multiple class inheritance lets us model subtle differences quite well. Using type lists and MPL to control those traits via conditional inheritance lets us automate trait selection. We'll focus on MicroStation 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.
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).
The classic C++ approach is through virtual methods: a base class defines a virtual pure
interface,
and derived classes implement that interface using the appropriate API in the above table.
This is dynamic polymorphism, because the implementation functions are obtained via the
class's
vtable
at run-time.
In other words, something like this …
// base class specifies an interface struct IPointTraits { virtual DPoint3d Origin () = 0; virtual RotMatrix Rotation () = 0; virtual double Scale () = 0; }; // derived text class implements the interface struct TextElm : public IPointTraits { DPoint3d Origin () { DPoint3d origin; mdlText_extractWide (&origin, …); return origin; } ... etc }; // derived tag class implements the interface struct TagElm : public IPointTraits { DPoint3d Origin () { 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. }; // derived text class implements the interface struct TextElm : public IPointTraitsCRTP<TextElm> { DPoint3d OriginImpl () { DPoint3d origin; mdlText_extractWide (&origin, …); return origin; } ...etc. }; // 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 classes, such as AreaTypes
Area()
method
LineStringElm
, inherit from TypedElement
TextElm
, for example, provides the Text()
method
The Element
base class is a single root interface for all elements.
It is not templated, so that an Element*
or smart pointer equivalent is available
for use in collections.
Any element class that derives, directly or indirectly, from that Element
base class
can be referenced by an Element*
or smart pointer.
Element
provides a small number of methods common to all derived classes.
That includes element type, element ID, symbology and level.
It excludes all interfaces that deal with geometry,
which are specific to each derived element class.
The TypedElement
templated class is, firstly, a specialised class that represents a
MicroStation element type.
Secondly it provides the framework for a flexible set of inherited traits classes.
The traits classes define the interface for a particular set of element types.
Traits are selected by the meta template programming (MPL) techniques described in
Type Lists and the MPL.
The traits classes define the interface for a particular set of element types. Traits are selected by the meta template programming (MPL) techniques described in Type Lists and the MPL.
Each traits class defines one or more methods required for that trait.
For example, the TextTypes
trait defines a single method Text()
.
The AreaTypes
trait defines several methods: Area()
, Perimeter()
and Centroid()
.
The interfaces defined by each trait class do not intersect: that is, no two traits define the same method.
However, a given element class is likely to inherit multiple traits classes:
for example, a TextElm
yields its Origin
and Text
values, which are supplied by different traits classes.
There is a concrete class for each MicroStation element type.
For example, EllipseElm
models a MicroStation ellipse element.
The concrete class inherits common methods, applicable to all elements, from the Element
base class.
It is obliged to implement methods defined by the interfaces that are introduced by the various
traits classes in its inheritance chain.
The polymorphism enabled by these metaprogramming techniques is not prescribed. That is, we can choose to use either dynamic or static polymorphic classes …
It's easier to understand a dynamic implementation if you're unfamiliar with template metaprogramming. The virtual classes are easy to distinguish from the metaprogramming, making it simpler to decipher what's going on.
Static despatch means that we have two sets of metaprogramming techniques going on simultaneously: one to handle the multiple traits selection, and another to implement the CRTP. Consequently, you may feel mentally overloaded with too many templates, or possibly with <too many angle brackets>, also known as chevron fatigue.
This header file provides the declaration and implementation of the Element
, traits,
TypedElement
and some concrete classes.
The traits define virtual pure interface methods that are implemented by the concrete classes.
This header file provides the declaration and implementation of the Element
, traits,
TypedElement
and some concrete classes.
The traits define interface methods that call the actual implementation in the
concrete class using the curious recurring template pattern (CRTP).
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.