C++11 features in Magnum: Better memory management

Re­gard­less to what lan­guage you use, you al­ways need to think about memory man­age­ment. Garbage col­lect­ors might give you a sense that it’s done auto­ma­gic­ally, but to get more per­form­ance you need to have con­trol over heap us­age in code run­ning at 60 FPS. C++11 helps to avoid many heap al­loc­a­tions without sac­ri­fi­cing us­ab­il­ity.

Get­ting rid of ex­pli­cit memory man­age­ment

Many ob­jects in the en­gine are memory-heavy (e.g. ver­tex data), which means that copy­ing them is not a good idea. Moreover, OpenGL ob­jects (such as tex­tures, meshes or buf­fers) aren’t copy­able, sim­il­arly to e.g. the std::fstream ob­jects. In C++03 world it meant that you had ba­sic­ally two solu­tions for avoid­ing copy­ing. You can force the user to cre­ate an in­stance be­fore­hand and then use the func­tion to per­form some ac­tion on it:

Trade::MeshData3D mesh;
importer->meshData3D("steam-locomotive", mesh);

Or you can cre­ate the in­stance on heap, re­turn point­er to it and in­struct the user to ex­pli­citly de­lete the ob­ject af­ter­wards:

Trade::MeshData3D* mesh = importer->meshData3D("steam-locomotive");
if(mesh) {
    // ...
}
delete mesh;

Both meth­ods re­quire some non-trivi­al ac­tion from the user. In the first case the ob­ject might have non-trivi­al con­struct­or (which ac­tu­ally is the case with Trade::Mesh­Data3D) and the func­tion has no in­tu­it­ive way to tell that the im­port went bad and the mesh is now in­val­id. The second case does heap al­loc­a­tion, which is slower than stack al­loc­a­tion, but more im­port­antly re­quires ex­pli­cit de­le­tion, which more of­ten than not leads to ac­ci­dent­al memory leaks.

C++11 in­tro­duces move se­mantics, which means that the ob­ject can be just moved out from the func­tion without copy­ing:

Trade::MeshData3D mesh = importer->meshData3D("steam-locomotive");

While this is fi­nally a clean oneliner, it now isn’t pos­sible to in­dic­ate that the mesh im­port failed.

The long-awaited std::optional, which got fi­nally in­cluded in C++17, aims to solve ex­actly this. The op­tion­al ob­ject con­tains a boolean in­dic­at­ing the state and ad­di­tion­al space where the in­stance can be stored (i.e., without any heap al­loc­a­tion). The class is in­spired with boost::optional but since the en­gine needs to be­have the same also on C++11 com­pilers, Cor­rade in­tro­duced its own Con­tain­ers::Op­tion­al that doesn’t re­quire C++17 and works on C++11-cap­able com­pilers as well.

Containers::Optional<Trade::MeshData3D> mesh = importer->meshData3D("steam-locomotive");
if(mesh) {
    // ...
}

Lastly, some func­tions re­turn poly­morph­ic types, which can’t be done any oth­er way than with heap al­loc­a­tion. C++11’s std::unique_ptr will handle the de­le­tion im­pli­citly and un­like std::shared_ptr it adds only a tiny over­head, be­cause it doesn’t need to do any ref­er­ence count­ing.

std::unique_ptr<Trade::AbstractMaterialData> material = importer->material("scratched-copper");
if(material) {
    // ...
}

Ini­tial­izer lists

In C++03 code, when you want to pass a list of some val­ues (known at com­pile-time) to a func­tion, the most per­form­ant way is this:

Source* sources[] = {backgroundMusic, boom, laughter, eternalPain};
Audio::Source::play(sources, 4);

If you want to write the same as one-liner, you can achieve that us­ing spe­cially craf­ted con­tain­er con­tain­ing some ma­gic with operator, or operator<<, but with not ex­actly in­tu­it­ive us­age and at a cost of run-time heap al­loc­a­tion, for ex­ample:

Audio::Source::play((Array<Source*>(), backgroundMusic, boom, laughter, eternalPain));

C++11’s std::ini­tial­izer­_l­ist al­lows to write this as a one-liner without any ad­di­tion­al over­head. In many cases Mag­num also provides a Con­tain­ers::Ar­rayView over­load for lists of run-time de­pend­ent size.

Audio::Source::play({backgroundMusic, boom, laughter, eternalPain});

Vari­able-length ar­rays

While this fea­ture is of­ten frowned-upon, it has its use. Many func­tions in OpenGL and oth­er frame­works, most not­ably the re­cent AR­B_mul­ti_­bind ex­ten­sion, ac­cept ar­rays of in­tegers to do an op­er­a­tion on a giv­en list of ob­jects. In pub­lic Mag­num API this is of­ten done us­ing std::ini­tial­izer­_l­ist of point­ers to giv­en ob­jects, as shown above. But in­tern­ally the lib­rary needs to ex­tract IDs from all ob­jects, put them in some newly al­loc­ated ar­ray of vari­able length, pass that ar­ray to giv­en func­tion and then de­lete the ar­ray again. Stack-al­loc­ated ar­rays solve this and the fea­ture is already avail­able as non-stand­ard ex­ten­sion in GCC, but it’s not yet used in Mag­num due to pos­sible port­ab­il­ity is­sues.

Set­ters and move se­mantics

Set­ters in C++03 code com­monly take const ref­er­ence to ob­ject and then copy it to the des­tin­a­tion:

void Configuration::setFilename(const std::string& filename) {
    _filename = filename;
}

While tak­ing ob­ject by ref­er­ence avoids cre­at­ing an­oth­er copy com­pared to tak­ing ob­ject by value, it doesn’t avoid un­ne­ces­sary cop­ies al­to­geth­er:

Configuration conf;
std::string file = "game.conf";
conf.setFilename(file);         // okay, copied from named variable
conf.setFilename("game.conf");  // bad, copied from temporary variable

In the second case, tem­por­ary std::string vari­able is cre­ated (first al­loc­a­tion), then its con­tents are copied (second al­loc­a­tion) and then this tem­por­ary is dis­carded (deal­loc­a­tion). The un­needed al­loc­a­tion and deal­loc­a­tion can be avoided us­ing move se­mantics, but from user point-of-view the us­age is still the same. In Mag­num all set­ters tak­ing heavy types (strings, vec­tors…) are done this way.

void Configuration::setFilename(std::string filename) {
    _filename = std::move(filename);
}

Semi-auto­mat­ic memory man­age­ment

Hand­ling memory deal­loc­a­tions in inter-de­pend­ent scene graph with many shared re­sources is a pain to do manu­ally and this is ex­actly the case where a sane garbage col­lect­or is ac­tu­ally use­ful. Mag­num of­fers two ways of auto­mat­ic memory man­age­ment: the scene graph and re­source man­ager.

Scene graph is a tree of ob­jects, sim­il­ar to what Qt’s QObject hier­archy is. When some ob­ject is des­troyed, all its chil­dren and at­tached fea­tures are auto­mat­ic­ally des­troyed too. In fact, to­geth­er with meth­od chain­ing you can add ob­jects to a scene and con­fig­ure them without even sav­ing them to a vari­able. You can read more about scene graph in the doc­u­ment­a­tion.

(new Chair(&scene))
    ->translate({0.4f, 0.0f, -1.0f})
    ->rotateY(25.0_degf);

Re­source man­ager of­fers more fine-grained op­tions. Each re­source stored there can be either stat­ic (de­leted on man­ager de­struc­tion), manu­ally man­aged (de­leted on ex­pli­cit free() call, which can be done either for par­tic­u­lar re­source type or for whole man­ager) or ref­er­ence-coun­ted (de­leted when last ref­er­ence to the ob­ject is re­moved). The be­ha­vi­or is more tho­roguhly de­scribed in Re­source­M­an­ager class doc­u­ment­a­tion.