C++11 features in Magnum: Simplifying daily workflow

While the pre­vi­ous part was fo­cused on C++11 fea­tures im­prov­ing per­form­ance, here I will de­scribe fea­tures which are used just to sim­pli­fy your life by writ­ing more con­cise code with less mis­takes.

Vari­ad­ic tem­plates

Vari­ad­ic tem­plates are used ex­tens­ively through­out the en­gine, but in many cases you won’t even no­tice. With them the en­gine is able to catch many mis­takes, which oth­er­wise would be im­possible to de­tect, at com­pile time. Con­sider cre­at­ing 7-ele­ment vec­tor the C++03 way:

Float data[] = { 3.5f, 0.3f, 4.0f, -7.3f, 5.0f, 1.0f -5.0f }; // oops
Math::Vector<7, Float> vec = Math::Vector<7, Float>::from(data);

This op­er­a­tion is not a simple one-line ex­pres­sion, but it must be writ­ten in two state­ments. Moreover the func­tion can­not gen­er­ally check that we fed it with prop­er amount of data, as it gets just a Float* point­er. In Mag­num the same can be writ­ten us­ing C++11 vari­ad­ic con­struct­or like this:

Math::Vector<7, Float> vec(3.5f, 0.3f, 4.0f, -7.3f, 5.0f, 1.0f -5.0f); // error!

Note that un­like with e.g. ini­tial­izer lists, men­tioned in pre­vi­ous art­icle, the er­ror will be de­tec­ted at com­pile time — we for­got to sep­ar­ate last two val­ues with a comma, thus passed only six ele­ments to the con­struct­or caus­ing un­defined be­ha­vi­or in the C++03 case.

In oth­er situ­ations, vari­ad­ic tem­plates might cal­cu­late some things auto­mat­ic­ally and save re­pet­it­ive typ­ing, which oth­er­wise leads to ac­ci­dent­al mis­takes. Ima­gine adding buf­fer with in­ter­leaved ver­tex at­trib­utes, spe­cify­ing one at­trib­ute at a time:

Int offset = 4238;
Int stride = 36;
Mesh mesh;
mesh.addVertexBuffer(vertexBuffer, offset, Shader::Position{}, stride-12)
    .addVertexBuffer(vertexBuffer, offset+12, Shader::Normal{}, stride-12)
    .addVertexBuffer(vertexBuffer, offset+24, Shader::TextureCoords{}, stride-12)
    .addVertexBuffer(vertexBuffer, offset+32, Shader::Weight{}, stride-12);

Each in­ter­leaved at­trib­ute is spe­cified by start­ing off­set and size of the gap after it (con­tain­ing data of oth­er at­trib­utes). Note that, due to copy-paste er­ror, all the at­trib­utes have spe­cified the same gap, even though the tex­ture co­ordin­ates are two floats and thus oc­cupy only 8 bytes (not 12), and the weight is only a single four-byte float. If the whole ver­tex format is spe­cified at once us­ing vari­ad­ic func­tion, the sizes, off­sets and strides are cal­cu­lated auto­mat­ic­ally be­hind the scenes, leav­ing no room for mis­takes:

mesh.addVertexBuffer(vertexBuffer, 4238, Shader::Position{}, Shader::Normal{},
    Shader::TextureCoords{}, Shader::Weight{}, 3);

Lastly, with vari­ad­ic classes you can merge many sim­il­ar tasks in­to one. Ima­gine hav­ing type-safe re­source man­ager for meshes, tex­tures and sound buf­fers:

ResouceManager<Mesh> meshManager;
ResouceManager<Texture2D> textureManager;
ResouceManager<Audio::Buffer> soundManager;

Mesh* mesh;
meshManager.set("steam-locomotive", mesh);
Texture* texture;
textureManager.set("steam-locomotive-diffuse", texture);
Audio::Buffer* buffer;
soundManager.set("steam-locomotive-honk", buffer);

// ...

meshManager.free();
textureManager.free();
soundManager.free();

// ...

soundManager.clear();

Vari­ad­ic re­source man­ager im­ple­ment­a­tion al­lows you to sig­ni­fic­antly shorten the above code:

ResourceManager<Mesh, Texture2D, Audio::Buffer> manager;

manager.set("steam-locomotive", mesh)
       .set("steam-locomotive-diffuse", texture)
       .set("steam-locomotive-honk", buffer);

// ...

manager.free();

// ...

manager.clear<Audio::Buffer>();

Meth­od chain­ing

This is not ex­actly a C++11 fea­ture (how­ever C++11’s rvalue ref­er­ences for this can be em­ployed to im­prove per­form­ance in some corner cases), but I will men­tion it here, as it al­lows for some neat tricks. Ex­cept for help­ing you type less (as shown above), this fea­ture al­lows you to do in­stan­ti­ation and con­fig­ur­a­tion in single ex­pres­sion, go­ing nicely along the “everything is an ex­pres­sion” ap­proach:

// Configure debug shape rendering
DebugTools::set("collision-shapes", DebugTools::ShapeRendererConfiguration()
    .setColor(Color3::fromHSV(25.0f, 0.7f, 0.9f))
    .setPointSize(0.35f));

// Add more ducks to the scene
(new Duck(&scene))->translate({0.3f, 0.0f, -0.9f})->rotateY(15.0_degf);
(new Duck(&scene))->translate({0.4f, 0.0f, -1.5f})->rotateY(-5.0_degf);
(new Duck(&scene))->translate({0.5f, 0.0f, -1.1f})->rotateY(35.0_degf);

Note that this fea­ture also has its down­sides, so use it only when it im­proves read­ab­il­ity and not the oth­er way. For ex­ample, more than one meth­od chain in a single ex­pres­sion can cause the code to be un­read­able and prone to er­rors.

Mis­cel­laneous

Strongly typed units

API in­con­sist­ence, where some func­tions ac­cept ra­di­ans (STL and OpenGL) and some de­grees (Open­AL) leads to prob­lems with mis­taken units. C++11 user-defined lit­er­als, ex­pli­cit con­ver­sion op­er­at­ors and constexpr al­low to solve this in in­tu­it­ive way without sac­ri­fi­cing per­form­ance. The strong types be­have just like any oth­er nu­mer­ic type and only con­ver­sion from and to the un­der­ly­ing type needs to be done ex­pli­citly. For lit­er­als, in­stead of writ­ing f suf­fix you can just write _degf or _radf and it will be con­ver­ted to the ex­pec­ted units at com­pile time. All Mag­num func­tions deal­ing with angles are tak­ing only the strongly typed val­ues, passing plain num­bers to them res­ults in com­pile-time er­ror:

Double sin = Math::sin(45.0_deg);
//Float cos = Math::cos(1.57f); // error
object->rotateX(15.0_degf);

Oth­er com­mon units are dis­tance and time units. Mag­num doesn’t im­pose any re­stric­tion on dis­tance units, sim­il­arly to Blender, so wheth­er 1.0f is one meter or one fur­long de­pends on you (but you need to be con­sist­ent, of course). Time unit lit­er­als will be part of C++14.

Us­age of SFINAE and type traits

With C++11’s type traits and std::en­able_if it’s pos­sible to design clean gen­er­ic API free of any work­arounds for am­bigu­ous meth­od and con­struct­or calls. For ex­ample, bit­wise op­er­a­tions are en­abled only for vec­tors with in­teg­ral un­der­ly­ing type. Vec­tors and matrices have gen­er­ic sup­port for (ex­pli­cit) con­ver­sion from and to ex­tern­al types, thanks to std::en­able_if the ac­tu­al con­ver­sion can be then im­ple­men­ted in sep­ar­ate lib­rary without touch­ing the ori­gin­al im­ple­ment­a­tion.

#include <BulletIntegration/Integration.h>

// Seamless usage of Magnum and Bullet types
btVector3 a(1.0f, 3.0f, 4.0f);
Magnum::Vector3 b(a);
b *= 5.0f;
a = btVector3(b);