Static and dynamic polymorphism in Magnum

Thanks to gen­er­ic pro­gram­ming and oth­er fea­tures ex­clus­ive to C++ it is pos­sible to handle poly­morph­ism the most ef­fect­ive way for each use case. As a res­ult, vir­tu­al calls are in Mag­num used very spar­ingly.

Some time ago I was dig­ging in some open­source C++ graph­ics lib­rary and found their Matrix4 class de­rived from some gen­er­ic BaseObject class with many vir­tu­al func­tions, debugOutput() be­ing among them. I de­cided to write this post to show that it can be done bet­ter than that.

Run-time (dy­nam­ic) poly­morph­ism

This is ba­sic­ally the step­ping stone of Ob­ject Ori­ented Pro­gram­ming. You have some well-defined base class and sub­classes are (re)im­ple­ment­ing the vir­tu­al func­tions. In many cases you don’t know about the im­ple­ment­a­tion and just work with ref­er­ence to the base in­ter­face. How­ever, this comes with per­form­ance pen­alty — each func­tion call must be dis­patched through a vir­tu­al table. While this might not be an is­sue in con­ven­tion­al ap­plic­a­tions, it is much more no­tice­able in tight game loops run­ning at 60 FPS.

Mag­num tries very hard to not over­use dy­nam­ic poly­morph­ism. It is needed only for plu­gins and scene graph in­tern­als. These two cases are im­ple­men­ted with Herb Sut­ter sug­ges­tions on vir­tu­al­ity in mind, which al­lows many per­form­ance and us­ab­il­ity im­prove­ments. In par­tic­u­lar, Mag­num em­ploys this ap­proach:

  • No pub­lic-fa­cing virtual meth­ods — all vir­tu­al meth­ods are called through pub­lic non-vir­tu­al proxy func­tions, which do ad­di­tion­al checks, con­ver­sions and as­ser­tions. It means that the im­ple­ment­a­tion con­tains only the code what mat­ters, free of any boil­er­plate and re­dund­ant stuff or calls to par­ent im­ple­ment­a­tion (which are for­got­ten more of­ten than not).
  • Hav­ing non-vir­tu­al pub­lic meth­od al­lows for great­er flex­ib­il­ity in de­rived classes — no is­sues with co­v­ari­ant re­turn types or con­flict­ing para­met­ers and the awe­some abil­ity to in­line the whole func­tion call. Good im­ple­ment­a­tion of pub­lic meth­od in de­rived class can even re­move the need for vir­tu­al call com­pletely, without de­pend­ing on com­piler op­tim­iz­a­tion prom­ises.
  • If there is no need to call delete on base class, the base doesn’t even need vir­tu­al de­struct­or, only a pro­tec­ted one. If the de­struct­or is empty, it’s then pos­sible to make the type constexpr, al­low­ing for even more op­tim­iz­a­tions.

You can look in­to Mag­num sources for real us­age, files Mag­num/Text/Ab­stract­Font.h and Mag­num/Text/Ab­stract­Font.cpp show con­sist­ency checks and con­ver­sions which would oth­er­wise need to be done re­dund­antly in each and every im­ple­ment­a­tion.

Stat­ic poly­morph­ism

Un­for­tu­nately, in many stat­ic­ally typed lan­guages, dy­nam­ic poly­morph­ism is the only real op­tion. If you define two classes with sim­il­ar in­ter­face, you can eas­ily swap one for an­oth­er by just chan­ging the type of vari­able, but that’s all you can do — it’s not pos­sible to use the oth­er type as para­met­er in­to the same func­tion.

With C++’s tem­plat­ing abil­it­ies it’s pos­sible to define gen­er­ic func­tions tak­ing any suit­able type. In dy­nam­ic­ally typed lan­guages (such as Py­thon) this is known as duck typ­ing, but with con­sid­er­able runtime pen­al­ties. In C++ the per­form­ance of tem­plated code is no dif­fer­ent from oth­er nat­ive code (apart from pos­sible com­pil­a­tion time / bin­ary size in­creases, but that’s an­oth­er story).

All classes with sim­il­ar use cases in Mag­num are stat­ic­ally poly­morph­ic. It means that you can for ex­ample swap trans­form­a­tion rep­res­ent­a­tion from Du­alQua­ternion to Mat­rix4, eas­ily switch to dif­fer­ent plat­form toolkit (re­place Plat­form::GlfwAp­plic­a­tion with Plat­form::Sdl2Ap­plic­a­tion) or use faster in-memory im­age rep­res­ent­a­tion on mod­ern graph­ics cards (GL::Buf­fer­Im­age2D in­stead of Im­age2D). In most cases you can do that without any ad­di­tion­al changes to meth­od calls and everything will just work.

Not everything can be con­veni­ently done without sub­l­cas­sing. To avoid hav­ing vir­tu­al de­struct­or, the base de­struct­or is made pro­tec­ted (as is the case with GL::Ab­stract­Frame­buf­fer and oth­er classes which are not meant to be in­stan­ti­ated dir­ectly). Also, it’s pos­sible to cheat a little with prim­it­ive types for math struc­tures, as only the base class con­tains the ac­tu­al data and the de­struct­ors in sub­classes are ba­sic­ally a no-op (for ex­ample Math::Col­or4, de­rived from Math::Vec­tor4, which is de­rived from Math::Vec­tor). In this case not call­ing de­struct­ors of de­rived classes won’t cause any harm and no memory will be leaked.

Mak­ing (stat­ic­ally) poly­morph­ic in­ter­face for Plat­form namespace was the hard­est thing — win­dow­ing toolkits sup­port very di­verse fea­ture set, which is most no­tice­able in event hand­lers. It means that it’s not pos­sible to pass the val­ues as sep­ar­ate func­tion para­met­ers, be­cause switch­ing to an­oth­er toolkit with dif­fer­ent event prop­er­ties would be a night­mare. A struc­ture is passed in­stead, which then has sep­ar­ate get­ters for all the prop­er­ties.

From user’s point-of-view the us­age is the same as if these classes were im­ple­men­ted us­ing dy­nam­ic poly­morph­ism, but in­tern­ally the code is much faster thanks to in­lin­ing and no need for vir­tu­al dis­patch or con­ver­sion of para­met­ers to im­ple­ment­a­tion-spe­cif­ic val­ues. Moreover this is the way how STL is de­signed.

No poly­morph­ism

Thanks to op­er­at­or over­load­ing in C++ it is pos­sible to use an op­er­at­or in­stead of im­ple­ment­ing a vir­tu­al func­tion. The es­sen­tial ex­ample is equal­ity com­par­is­on and de­bug out­put (vari­ous toString() or equals() func­tions). The Util­ity::De­bug class uses, sim­il­arly to STL and Qt’s qDebug(), operator<< to print val­ues on de­bug out­put.