Static and dynamic polymorphism in Magnum

Thanks to gener­ic pro­gram­ming and oth­er fea­tures ex­clu­sive to C++ it is pos­si­ble to han­dle poly­mor­phism the most ef­fec­tive way for each use case. As a re­sult, vir­tu­al calls are in Mag­num used very spar­ing­ly.

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

Run-time (dynamic) polymorphism

This is ba­si­cal­ly the step­ping stone of Ob­ject Ori­ent­ed Pro­gram­ming. You have some well-de­fined base class and sub­class­es are (re)im­ple­ment­ing the vir­tu­al func­tions. In many cas­es you don’t know about the im­ple­men­ta­tion and just work with ref­er­ence to the base in­ter­face. How­ev­er, this comes with per­for­mance penal­ty — each func­tion call must be dis­patched through vir­tu­al ta­ble. While this might not be an is­sue in con­ven­tion­al ap­pli­ca­tions, it is much more no­tice­able in tight game loops run­ning at 60 FPS.

Mag­num tries very hard to not overuse dy­nam­ic poly­mor­phism. It is need­ed on­ly for plug­ins and scene graph in­ter­nals. These two cas­es are im­ple­ment­ed with Herb Sut­ter sug­ges­tions on vir­tu­al­i­ty in mind, which al­lows many per­for­mance and us­abil­i­ty im­prove­ments. In par­tic­u­lar, Mag­num em­ploys this ap­proach:

  • No pub­lic-fac­ing 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­men­ta­tion con­tains on­ly the code what mat­ters, free of any boil­er­plate and re­dun­dant stuff or calls to par­ent im­ple­men­ta­tion (which are for­got­ten more of­ten than not).
  • Hav­ing non-vir­tu­al pub­lic method al­lows for greater flex­i­bil­i­ty in de­rived class­es — no is­sues with co­vari­ant re­turn types or con­flict­ing pa­ram­e­ters and the awe­some abil­i­ty to in­line the whole func­tion call. Good im­ple­men­ta­tion of pub­lic method in de­rived class can even re­move the need for vir­tu­al call com­plete­ly, with­out de­pend­ing on com­pil­er op­ti­miza­tion prom­ises.
  • If there is no need to call delete on base class, the base doesn’t even need vir­tu­al de­struc­tor, on­ly pro­tect­ed one. If the de­struc­tor is emp­ty, it’s then pos­si­ble to make the type constexpr, al­low­ing for even more op­ti­miza­tions.

You can look in­to Mag­num sources for re­al us­age, files Text/Ab­stract­Font.h and Text/Ab­stract­Font.cpp shows con­sis­ten­cy checks and con­ver­sions which would oth­er­wise need to be done re­dun­dant­ly in each and ev­ery im­ple­men­ta­tion.

Static polymorphism

Un­for­tu­nate­ly, in many stat­i­cal­ly typed lan­guages, dy­nam­ic poly­mor­phism is the on­ly re­al op­tion. If you de­fine two class­es with sim­i­lar in­ter­face, you can eas­i­ly swap one for an­oth­er by just chang­ing the type of vari­able, but that’s all you can do — it’s not pos­si­ble to use the oth­er type as pa­ram­e­ter in­to the same func­tion.

With C++’s tem­plat­ing abil­i­ties it’s pos­si­ble to de­fine gener­ic func­tions tak­ing any suit­able type. In dy­nam­i­cal­ly typed lan­guages (such as Python) this is known as duck typ­ing, but with con­sid­er­able run­time penal­ties. In C++ the per­for­mance of tem­plat­ed code is no dif­fer­ent from oth­er na­tive code (apart from pos­si­ble com­pi­la­tion time in­creas­es, but that’s an­oth­er sto­ry).

All class­es with sim­i­lar use cas­es in Mag­num are stat­i­cal­ly poly­mor­phic. It means that you can for ex­am­ple swap trans­for­ma­tion rep­re­sen­ta­tion from Du­alQuater­nion to Ma­trix4, eas­i­ly switch to dif­fer­ent plat­form tool­kit (re­place Plat­form::Glu­tAp­pli­ca­tion with Plat­form::Sdl2Ap­pli­ca­tion) or use faster in-mem­o­ry im­age rep­re­sen­ta­tion on mod­ern graph­ics cards (Buf­fer­Im­age2D in­stead of Im­age2D). In most cas­es you can do that with­out any ad­di­tion­al changes to method calls and ev­ery­thing will just work.

Not ev­ery­thing can be con­ve­nient­ly done with­out sublcass­ing. To avoid hav­ing vir­tu­al de­struc­tor, the base de­struc­tor is made pro­tect­ed (as is the case with Ab­stract­Frame­buf­fer and oth­er class­es which are not meant to be used di­rect­ly). Al­so, it’s pos­si­ble to cheat a lit­tle with prim­i­tive types for math struc­tures, as on­ly the base class con­tains the ac­tu­al da­ta and the de­struc­tors in sub­class­es are ba­si­cal­ly a no-op (for ex­am­ple Col­or4, de­rived from Math::Vec­tor4, which is de­rived from Math::Vec­tor). In this case not call­ing de­struc­tors of de­rived class­es won’t cause any harm and no mem­o­ry will be leaked.

Mak­ing (stat­i­cal­ly) poly­mor­phic in­ter­face for Plat­form names­pace was the hard­est thing — win­dow­ing tool­kits sup­port very di­verse fea­ture set, which is most no­tice­able in event han­dlers. It means that it’s not pos­si­ble to pass the val­ues as sep­a­rate func­tion pa­ram­e­ters, be­cause switch­ing to an­oth­er tool­kit with dif­fer­ent event prop­er­ties would be a night­mare. A struc­ture is passed in­stead, which then has sep­a­rate get­ters for all the prop­er­ties.

From us­er’s point-of-view the us­age is the same as if these class­es were im­ple­ment­ed us­ing dy­nam­ic poly­mor­phism, but in­ter­nal­ly the code is much faster thanks to in­lin­ing and no need for vir­tu­al dis­patch or con­ver­sion of pa­ram­e­ters to im­ple­men­ta­tion-spe­cif­ic val­ues. More­over this is the way how STL is de­signed.

No polymorphism

Thanks to op­er­a­tor over­load­ing in C++ it is pos­si­ble to use op­er­a­tor in­stead of im­ple­ment­ing vir­tu­al func­tion. The es­sen­tial ex­am­ple is equal­i­ty com­par­i­son and de­bug out­put (var­i­ous toString() or equals() func­tions). The util­i­ty Debug class us­es, sim­i­lar­ly to STL and Qt’s qDebug(), operator<< to print val­ues on de­bug out­put.