Memory management in other systems programming languages
Now that we understand the necessity of dynamically allocated memory, let’s compare manual and automatic ways of management.
C, which inspired the design of many of the languages that came after it, had no automatic memory management out of the box. You could make your own GC library or use a publicly available one, but it did not fit well with the design philosophy of the language: giving full responsability to the programmer, with minimal abstractions. C++ mostly followed in the footsteps of C in this regard until C++11, when smart pointers were introduced, simplifying the use of dynamic memory. In contrast, Java has a complex automatic memory management system, based on a mark-and-sweep type of garbage collection (GC).
Manual Memory Management in D
D has bindings to most of the C standard library functions (<stdlib.h>
) and you can use them by importing core.stdc.stdlib
. To see a list of bindings, you can visit this page. Today, we will refer to malloc
, calloc
, realloc
and free
. An important thing to note is, by looking at the signature, all of the 4 mentioned functions share these attributes: nothrow @nogc @system
. These functions work exactly like in C, meaning you are fully responsible for the way you use memory, and from the @nogc
attribute we can conclude that any memory allocated with these functions is completely foreign to D’s automatic memory management system. D also has the notion of @safe
functions, which have a long list of restrictions, to enforce safe memory usage. Since one of these restrictions is calling any @system
functions, using them is generally not recommended, as the user must ensure the safety and correctness of the memory management operations.
Below is an example of how you can use the bindings to the C standard library functions in your program:
import core.stdc.stdlib;
import std.stdio;
void main()
{
enum totalInts = 10;
int[totalInts] ints;
writeln(ints[0]); // will print 0, because types in D have a default initializer, called .init, which for type int gives 0
// Allocate memory for 10 ints
int* intPtrMalloc = cast(int*) malloc(int.sizeof * totalInts);
int* intPtrCalloc = cast(int*) calloc(int.sizeof, totalInts);
writeln(intPtrMalloc[0]); // will print a 'random' integer, because it does not use the D-specific initialization
free(intPtrMalloc); // free intPtrMalloc immediately
// intPtrMalloc[0] = 1; // this will not raise a segmentation fault, but it still is an error to reference free'd memory
scope(exit) free(intPtrCalloc); // use a scope guard to free intPtrCalloc at the end of the function
intPtrCalloc[0] = 1; // ok
writeln(intPtrCalloc[0]); // will print 1
}
In D, we can use scope guards to execute statements on exit
, success
or failure
:
scope(exit)
will always call the statementsscope(success)
statements are called when no exceptions have been thrownscope(failure)
denotes statements that will be called when an exception has been thrown before the block’s end
This helps in writing cleaner and safer code. It is guaranteed that cleanup code specified with scope
will always be called, and the statements are called in reverse order, enabling the developer to undo side effects of earlier expressions. You can read more about scope guards here;
The advantage to managing the memory yourself is predictability: you allocate the memory, you free the memory, and you can do it when you expect it will not impact your application’s performance. If you write code for embedded or real-time devices, the added complexity of managing the memory yourself might be a needed compromise in order to fit your terget system’s requirements.
If you manually manage the memory, the chances of memory leaks happening are high, and debugging memory leaks is not trivial in real-life applications.
We can also use the standard C functions to allocate aggregate types:
struct Point { int x, y; }
Point* onePoint = cast(Point*) malloc(Point.sizeof);
writeln(*onePoint); // prints Point(random int, random int)
Point* secondPoint = new Point();
writeln(*secondPoint); // prints Point(0, 0)
Point* tenPoints = cast(Point*) malloc(Point.sizeof * 10);
However, if you want to use malloc
, for example, together with default initializers or a struct/class constructor, you need to use emplace
. This method is parameterized with the type you want to initialize, and the first argument is a pointer to the allocated memory of the object of that type. Additional arguments can be provided, matching one of the constructors for the object.
Practice
- Use
malloc
together withemplace
, documented here, to allocate and initialize a struct and a class. For each, define a no-arg constructor and a constructor with at least one argument. Use this as a starting point:
Use the code from mallocEmplace.d
, in the practice folder.
- Define destructors and use
destroy
to call the destructors, then free the memory.