Skip to content

Best Practices

Timur Gafarov edited this page Aug 16, 2018 · 43 revisions

This page describes an idiomatic way of writing dlib-friendly code.

Memory

Writing effective and predictable real-time applications require full control over dynamic memory allocation and freeing. Garbage collector, which is one of the most controversial features of D, is unaffordable for a high-end game engine. Luckily we can do allocations manually with malloc instead of D's native heap allocator, but isn't it a step backward into the old days of С? Not with dlib!

dlib allows you to handle memory management in two fashions: C++ style (using New and Delete) and Delphi style (using ownership).

New and Delete are template functions that wrap malloc and free. They allocate and free arrays, classes and structs, properly initializing them and calling constructors when necessary:

import dlib.core.memory;

void main()
{
    int[] array = New!(int[])(100);
    Delete(array);

    MyClass obj = New!MyClass(1, 2, 3);
    Delete(obj);
}

This may be enough for many simple cases, but if you're writing an application with lots of objects things can quickly become complicated. The memory may end up leaking if you forget to delete some object, and bugs like that are not the easiest to fix. Garbage collection is there to solve this problem, but it brings plenty of other issues, so dlib provides an alternative - object ownership.

Core concept of ownership is that any object may 'belong' to other object. When the owner object is deleted, all its belonging objects are also automatically deleted.

import dlib.core.memory;
import dlib.core.ownership;

class MyClass: Owner
{
    this(Owner owner = null)
    {
        super(owner);
    }
}

void main()
{
    MyClass c1 = New!MyClass();
    MyClass c2 = New!MyClass(c1);
    Delete(c1); // c2 is deleted automatically
}

This, of course, imposes some restrictions on object's lifetime, but the idea of ownership can be applied to many use cases, GUI and game engines being two common examples. If you can think of your data model as a tree, ownership is a perfect memory management policy for it.

Comparison

D's native memory management with GC

  • Pros: easiest to use, built into the language.
  • Cons: unpredictable, requires additional logics to correctly handle external resources, still can cause memory leaks if used carelessly.
  • Summary: best suited for applications that constantly allocate short-living objects (such as servers), or for utilities that perform one task and then exit. Not suitable for game engines that work heavily with VRAM data, which cannot be managed with GC.

Pure New/Delete

  • Pros: predictable, gives full control over memory management.
  • Cons: requires too much discipline, unconvenient to use in big projects, often causes memory leaks and double free errors
  • Summary: best suited for relatively small games with simple engine architecture. In large projects turns code into unmaintainable mess.

Ownership

  • Pros: predictable, requires little or no programmer attention.
  • Cons: constraints object's lifetime, works only with classes, can be an overkill for small applications.
  • Summary: best suited for complex games and GUI applications. Useless if you don't write in object-oriented style.

Error Handling

Exceptions are common mean of error handling, but we recommend against using them. They tend to increase code complexity, they are hard to manage properly, and they make applications crash too often. In D they are also tied to OOP and garbage collector. We recommend a simpler alternative - Go-style error tuples. They can be easily constructed using Compound type:

import dlib.core.compound;

Compound!(Data, string) doSomething()
{
    Data data;

    // Process data

    if (someErrorOccured)
        return compound(data, "Error message");
    else
        return compound(data, "");
}

auto res = doSomething();
if (res[1].length)
    writeln(res[1]);
else
    // use res[0]

You can use nested functions to finalize your data before returning an error:

Compound!(Data, string) doSomething()
{
    Data data;

    Compound!(Data, string) error(string errorMsg)
    {
        // Finalize data...

        return compound(data, errorMsg);
    }

    // Process data...

    if (someErrorOccured)
        return error("Error message");
    else
        return compound(data, "");
}

Files

All file I/O in dlib is based on dlib.core.stream and dlib.filesystem. The latter supports both garbage-collected and manual memory management.

For simple cases use free functions from dlib.filesystem.local:

import dlib.core.stream;
import dlib.filesystem.local;

InputStream s = openForInput("my.file");
// can read from s

OutputStream s = openForOutput("my.file");
// can write to s

The code above will allocate GC memory. It's GC-free version is the following:

import dlib.filesystem.stdfs;

StdFileSystem fs = New!StdFileSystem();

InputStream s = fs.openForInput("my.file");
// can read from s
Delete(s);

OutputStream s = fs.openForOutput("my.file");
// can write to s
Delete(s);

Delete(fs);

Containers

dlib provides GC-free alternatives to D's native dynamic and associative arrays - dlib.container.array and dlib.container.dict, respectively.

import dlib.container.array;

DynamicArray!int a;
a ~= 10;
a ~= [11, 12, 13];
foreach(i, ref v; a)
    writeln(v);
a.removeBack(1);
assert(a.length == 2);
a.free();
import dlib.container.dict;

Dict!(int, string) d = New!(Dict!(int, string))();
d["key1"] = 10;
d["key2"] = 20;
Delete(d);
Clone this wiki locally