This is the second in a series of articles about C++ metaprogramming. The first article introduces the Boost Meta Programming Language (MPL) and type lists. This article covers another topic of C++ metaprogramming: classes and conditional inheritance.
The C++ language continues to evolve. Two evolutionary steps were the introduction of templates and Alexander Stepanov's invention of the Standard Template Library (STL). The STL is incorporated into the C++ language standard.
An unexpected consequence of the introduction of templates was template metaprogramming. Template metaprogramming provides a mechanism to write a compile-time program. In other words, abstract application logic in a way that lets the C++ compiler check its correctness.
Template metaprogramming leads to new areas of program capability. In this article, we'll examine conditional inheritance. We'll be using concepts from the C++ Standard Library, the Boost Libraries and in particular the Boost Metaprogramming Library (MPL).
The Element conditional inheritance and future examples assume use of the C++ Standard Library. In other words, for MicroStation developers, this is C++ stuff for which MDL (old-style C programming) is not suitable.
The Element conditional inheritance use the Boost libraries, particularly the Meta Programming Library (boost::mpl).
C++ 11 has changed the playing field. The addition of variadic templates with their associated parameter packs added a compile-time list of types structure directly into the language. With C++ 11, type lists are as easy as …
// C++11 template<class... T> struct type_list {};
If you're an experienced C++ developer, you know about inheritance. Skip to the next section. If you're not an experienced C++ developer, later sections of this article may seem obtuse.
Here's a C++ base class …
struct base { int Foo () { return 42; } };
Invoke that class …
int main (int argc, char* argv [])
{
base b;
int i = b.Foo (); // i takes value 42
}
Here's a derived class that inherits from base …
struct derived : public base { int Foo2 () { return 53; } }
Invoke that class …
int main (int argc, char* argv []) { base b; int i = b.Foo (); // i takes value 42 derived d; int j = d.Foo2 (); // j takes value 53 int k = d.Foo (); // k takes value 42 from base class }
Interface classes contain no data members and provide virtual pure function declarations but no function definitions. You design an interface class with the intention that it should be inherited. An interface class is useless by itself.
Here's a C++ class intended to be inherited …
struct interface
{
// virtual means "You should override this method"
// = 0 means "You must implement this method in your inheriting class
virtual int Foo3 () = 0;
};
Derive from our interface class …
struct class_with_interface : public interface
{
// We must implement the interface.Foo3 method
int Foo3 () { return 64; }
};
Invoke that class-with-interface …
int main (int argc, char* argv [])
{
class_with_interface cwi;
int i = cwi.Foo3 (); // i takes value 64
}
Suppose we want to inherit a class only in certain circumstances. We're modelling animals. If the animal is a mammal, it has four legs and can walk. If it's not a mammal, it may not be able to walk. But if it's a bird, it has two legs and can walk so deciding that an animal can't walk just because it's not a mammal is not a sound conclusion. Equally, just because an animal has two legs doesn't imply that it can fly (that would be over-generous to we humans). We need a mechanism that reveals certain traits so that we can design classes accordingly. The traits inform a derived class whether an animal can walk or fly (or even both, which is true for most birds but not the ostrich).
Here's a first attempt …
// A class designed, as an interface, to be inherited struct animal { virtual int nLegs () = 0; virtual int nWings () = 0; virtual void Walk () = 0; virtual void Fly () = 0; }; struct mammal : public animal { int nLegs () { return 4; } int nWings () { return 0; /* Unnecessary */ } void Walk () { /* OK */ } void Fly () { /* What? */ } }; struct bird : public animal { int nLegs () { return 2; } int nWings () { return 2; } void Walk () { /* OK */ } void Fly () { /* OK, but not ostrich */ } };
Because our interface class has pure virtual methods, we have to implement each of them, even when, as in the case of mammals, it doesn't make sense (OK — bats can fly and they are mammals, but I'm writing for programmers, not biologists). We can make interface methods non-pure, so it's not obligatory to implement them, but that means the implementation lies in the programmer's forgetful domain.
The mammal and bird classes cover too broad a spectrum of possibilities. It's too easy to find exceptions to the rules provided by those possibilities. We need a finer-grained mechanism to let us define animals and birds. One solution might be to use C++ multiple inheritance, then write very simple interface classes that define a single trait. Using multiple inheritance, we can derive a class having multiple traits …
struct IAnimalHasLegs { virtual int nLegs () = 0; }; struct IAnimalHasWings { virtual int nWings () = 0; }; struct IAnimalThatWalks { virtual void Walk () = 0; }; struct IAnimalThatFlies { virtual void Fly () = 0; };
Now we can design an animal class that doesn't have anomalies …
struct Rabbit : public IAnimalHasLegs, IAnimalThatWalks { int nLegs () { return 4; } void Walk () { /* OK */ } }; struct Duck : public IAnimalHasLegs, IAnimalThatWalks, IAnimalHasWings, IAnimalThatFlies { int nLegs () { return 2; } int nWings () { return 2; } void Fly () { /* OK */ } void Walk () { /* OK, but should be 'Waddle' */ } }; struct Bat : public IAnimalHasLegs, IAnimalThatWalks, IAnimalHasWings, IAnimalThatFlies { int nLegs () { return 2; } int nWings () { return 2; } // Although a bat is a mammal, it can both walk and fly void Walk () { /* OK */ } void Fly () { /* OK */ } }; struct Ostrich : public IAnimalHasLegs, IAnimalThatWalks, IAnimalHasWings { int nLegs () { return 2; } int nWings () { return 2; } void Walk () { /* OK */ } // Although an ostrich is a bird and has wings, it can't fly, // so we omit interface IAnimalThatFlies };
The interface classes that define only a single trait let us fine-tune our derived classes. We can create derived classes at the expense of a myriad of traits classes, which as programmers we have to select carefully. Humans who are charged with selecting things carefully tend to make sloppy selections. We end up with elephants that live in burrows, or rabbits with tusks.
What we need is a way to automate the inheritance of interfaces. That is, abstract the traits of various groups of animals and provide a mechanism to inherit or not inherit them in a deterministic manner.
We already have such a mechanism: type lists. Read the previous article to learn more about type lists and Boost.MPL.
If we convert animal characteristic to traits classes and put those in a type list, we can make compile-time decisions by evaluating each type list in the context of a required characteristic. Here are some animal type definitions …
enum AnimalTypes { Eagle, Ostrich, Bat, Cat, Dog, Blackbird, }; using EagleType = boost::mpl::int_<Eagle> using BlackbirdType = boost::mpl::int_<Blackbird> using OstrichType = boost::mpl::int_<Ostrich> using BatType = boost::mpl::int_<Bat> using CatType = boost::mpl::int_<Cat> using DogType = boost::mpl::int_<Dog>
enum AnimalTypes { Eagle, Ostrich, Bat, Cat, Dog, Blackbird, }; typedef boost::mpl::int_<Eagle> EagleType; typedef boost::mpl::int_<Blackbird> BlackbirdType; typedef boost::mpl::int_<Ostrich> OstrichType; typedef boost::mpl::int_<Bat> BatType; typedef boost::mpl::int_<Cat> CatType; typedef boost::mpl::int_<Dog> DogType;
Here are some type lists that agreggate characteristics as a trait …
using MammalTypes = boost::mpl::vector <CatType, DogType, BatType> using BirdTypes = boost::mpl::vector <BlackbirdType, OstrichType, EagleType> using QuadrupedTypes = boost::mpl::vector <CatType, DogType> using BipedTypes = boost::mpl::vector <EagleType, BlackbirdType, BatType, OstrichType> using FlyingTypes = boost::mpl::vector <EagleType, BlackbirdType, BatType> using WalkingTypes = boost::mpl::vector <CatType, DogType, OstrichType>
typedef boost::mpl::vector <CatType, DogType, BatType> typedef boost::mpl::vector <BlackbirdType, OstrichType, EagleType> typedef boost::mpl::vector <CatType, DogType> typedef boost::mpl::vector <EagleType, BlackbirdType, BatType, OstrichType> typedef boost::mpl::vector <EagleType, BlackbirdType, BatType> typedef boost::mpl::vector <CatType, DogType, OstrichType>
Now we can ask questions about a particular animal type using boost::mpl
metafunctions.
First, we need a meta function that tells us if a given type exists in a type list.
Here it is …
// Metafunction implemented by class template IsTypeInList template <typename TypeList, typename queryType> struct IsTypeInList { using type_pos = typename boost::mpl::find <TypeList, queryType>::type; using finish = typename boost::mpl::end <TypeList>::type finish; // By convention, MPL metafunctions return type, value_type and value using type = typename boost::mpl::not_ <boost::is_same <type_pos, finish> >::type; using value_type = typename type::value_type value_type; static const bool value = type::value; };
// Metafunction implemented by class template IsTypeInList template <typename TypeList, typename queryType> struct IsTypeInList { typedef typename boost::mpl::find <TypeList, queryType>::type type_pos; typedef typename boost::mpl::end <TypeList>::type finish; // By convention, MPL metafunctions return type, value_type and value typedef typename boost::mpl::not_ <boost::is_same <type_pos, finish> >::type type; typedef typename type::value_type value_type; static const bool value = type::value; };
We can use that function anywhere we want to test for a boolean predicate. Here's a somewhat contrived function that tells us at run-time if an Eagle can fly …
static bool CanFly ()
{
// The following compile-time expression resolves to a
// static const boolean value
return IsTypeInList<FlyingTypes, EagleType >::value;
}
Now we have all the information we need to create a conditionally-inherited class. We include a metafunction in the inheritance list of a class. The metafunction selects a base class to inherit if it has a required trait.
Here's some code that indicates where we're heading. The comment for each inherited interface class tells you what we want to do: inherit or not inherit that class …
class Eagle : public IAnimalHasLegs /* inherit */, public IAnimalThatWalks /* inherit */, public IAnimalThatFlies /* inherit */, public IAnimalHasWings /* inherit */ { };
class Ostrich : public IAnimalHasLegs /* inherit */, public IAnimalThatWalks /* inherit */, public IAnimalThatFlies /* don't inherit */, public IAnimalHasWings /* inherit */ { };
We need to figure out what to substitute for those comments. What metafunction can we provide that selects a given base class when an animal shows that trait? Equally important, how do we know when to omit a base class (e.g. for the ostrich that can't fly)?
Unsurprisingly, the solution is in two parts …
Here's the templated derived class …
template <typename animalType> class TypedAnimal : public IAnimalHasLegs <animalType>, public IAnimalThatWalks <animalType>, public IAnimalThatFlies <animalType>, public IAnimalHasWings <animalType> { };
Here's the desired specialisation for Ostrich.
Note that even though we might inherit from IAnimalHasWings
,
in this hypothetical implementation we've omitted IAnimalThatFlies
and therefore don't have to implement its method Fly
…
class Ostrich public : public TypedAnimal <OstrichType> { // Interface methods that we must implement int nLegs (); // IAnimalHasLegs int nWings (); // IAnimalHasWings void Walk (); // IAnimalThatWalks };
How do we achieve that desired result?
We must modify our interface class definitions so that they morph according to
the animalType
they receive from the inheritance trail above.
For example, here's IAnimalHasLegs
re-cast as a templated class …
// Generalised template — this is never instantiated template<typename animalType, typename enabler = void> struct IAnimalHasLegs; /// IAnimalHasLegs interface specialised for bipeds template<typename animalType> struct IAnimalHasLegs < animalType, typename boost::enable_if < typename IsTypeInList < BipedTypes, animalType >::type >::type > { virtual int nLegs () = 0; // Inheriting class must implement ... other virtual functions };
There remains a problem with this approach.
When a trait fails the interface class metafunction test, it is not instantiated (which is what we want).
However, that leaves a hole in the TypedAnimal
class's inheritance list.
If we could see it, it would look like this …
class Ostrich public : public TypedAnimal <OstrichType>, public IAnimalWithLegs, // inherited public IAnimalThatWalks, // inherited /* IAnimalThatFlies */, // omitted public IAnimalHasWings // inherited { };
The problem lies with the omitted interface IAnimalThatFlies
.
Because the meta-code has made it vanish, there are two commas between
IAnimalThatWalks
and IAnimalHasWings
.
Two commas make the compiler complain about a syntax error.
The solution to that problem is another templated version of the interface class. This version is instantianted only when the animal type is not in the type list, and the class body is completely empty …
// Empty template class specialisation for animals that are not bipeds template<typename animalType> struct IAnimalHasLegs < animalType, typename boost::enable_if < // This is where the test differs from the previous specialisation typename boost::mpl::not_ < IsTypeInList < BipedTypes, animalType > >::type >::type > { // Nothing here: this class is empty };
The empty class fills the space between the commas, so the compiler is content. However, the class may not be quite empty: the compiler provides default constructors and a destructor. If that bothers you, you can write an empty constructor to override the compiler's default behaviour. Subsequently, the optimiser probably removes that empty class.
Here's the complete definition of interface IAnimalHasLegs
.
To summarise, we have …
animalType
(OstrichType
)
is found in the type list of animal traits
animalType
(OstrichType
)
is not found in the type list of animal traits
// Generalised template — this is never instantiated template<typename animalType, typename enabler = void> struct IAnimalHasLegs; /// IAnimalHasLegs interface specialised for bipeds template<typename animalType> struct IAnimalHasLegs < animalType, typename boost::enable_if < typename IsTypeInList < BipedTypes, animalType >::type >::type > { // Dynamic polymorphic implementation virtual int nLegs () = 0; // Derived class must implement ... other virtual functions }; // Empty template class specialisation for animals that are not bipeds template<typename animalType> struct IAnimalHasLegs < animalType, typename boost::enable_if < typename boost::mpl::not_ < IsTypeInList < BipedTypes, animalType > >::type >::type > { // Nothing here: this class is empty };
Voilà! We've achieved conditional class inheritance. At this point, we can choose to implement dynamic or static polymorphism.
Dynamic polymorphism is implemented through the traditional virtual pure interface, which is what is shown above. The interface class specifies virtual pure functions that the derived class must implement.
Static polymorphism is achieved through templated classes. In particular, we can use the curious recurring template pattern (CRTP). CRTP tells the base class to provide an interface function that is implemented in the derived class, without the drawbacks of virtual functions.
You can read more about dynamic and static polymorphism in the next chapter of this series.
What we've described here is a general approach to a technique that enables conditional class inheritance. However, we've written this article keeping MicroStation development in mind. There's a practical example available that defines MicroStation element types, and some MPL metafunctions that operate on those classes.
Read the next episode: Polymorphic Classes for MicroStation Elements
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 C++ and the MicroStationAPI to the MicroStation Programming Forum.