Abstract
When a struct Allocator
or class Allocator
defines T opNew(T, A...)(A a) => ...
, then allocator.new Item("x")
will be rewritten to allocator.opNew!Item("x")
.
Rationale
Some D programs use a different allocator than the built-in GC allocator you get when using the new
operator. To make allocation type safe, allocator libraries often offer a templated function that creates an instance of an arbitrary type using that allocator. However, these template functions have to awkwardly use a synonym for new
, since it's a reserved keyword. A common choice is 'make' or 'alloc' (short for 'allocate').
Some real examples:
make!T
ormakeArray!T
in std.experimental.allocatoralloc!T
in memutilsconstructNew!T
in memterface
Since D programmers are familiar with new T()
syntax, enabling it for custom allocators makes code both more pleasing to read and easier to refactor when switching from using the GC to a custom allocator. It also makes it easier to switch from one custom allocator to another when there's a unified syntax, since you don't need to learn and apply new function signatures every time.
Prior work
- D runtime hooks have been translated to templates.
new X()
is already being rewritten to instances of_d_newitemT
,_d_newarrayT
, or_d_newclassT
, so overrding the implicitly imported object.d module lets you override the allocator with a custom one. This is hacky though, and only provides the means to switch a single global allocator since there's still no way to pass an allocator parameter. - class allocators have been deprecated because "Classes should not be responsible for their own (de)allocation strategy". This proposal is different because it doesn't let the class decide which allocator to use, it requires explicit mentioning of the allocator object on each allocation with
new
.
Description
The rewrite works just like existing operator overloading.
opNew
has to be defined inside an aggregate.
The typeof(new T())
will be passed as the first template parameter, while the arguments passed to new
are passed as run-time arguments.
struct GcAllocator
{
// Simple wrapper, can be swapped out later
T opNew(T, A...)(A args) => new T(args);
}
void main()
{
GcAllocator allocator;
allocator.new int(3); // a.opNew!(int*)(3)
allocator.new int[3]; // a.opNew!(int[])(3)
allocator.new int[](3); // a.opNew!(int[])(3)
allocator.new int[3][4]; // a.opNew!(int[][])(3, 4)
allocator.new Struct; // a.opNew!(Struct*)()
allocator.new Struct(args); // a.opNew!(Struct*)(args)
allocator.new Class(args); // a.opNew!Class(args)
}
Explicit instantiation of Nested class
The .new
syntax is already used for explicit instantiation of a nested class. However, I consider this an obscure legacy feature. For backwards compatibility, this will have precedence over the operator overload.
The only limitation this brings is that allocators may not contain a nested class, which shouldn't be a problem in practice.
Zero argument variant
One could imagine a zero-argument variant like this:
struct Unique(T)
{
T t;
Unique opNew() => new Unique(T.init);
}
void main()
{
auto a = Unique!int.new;
}
However, there might be an expectation that it's the same as:
auto a = new Unique!int;
So I'm not sure whether it's a good idea. It could be added in the future.
With statements
The with statement does not allow you to implicitly call opNew
, as that could lead to surprising results and breakage.
with (a)
auto p = new int; // don't try a.opNew!int
destroy()
Since the GC takes care of freeing memory, there is usually no need to call object.destroy()
on an object created with new
. When the custom allocator does region-based memory management, there's no need to free individual objects either. When the allocator does require manual freeing of objects, one could implement destroy
as a member function of the allocator.
struct Allocator
{
T opNew(T, A...)(A args) => new T(args);
void destroy(T)(T o) {}
}
void main()
{
Allocator allocator;
auto o = allocator.new Object;
allocator.destroy(o);
}
Since destroy
is a library function in object.d, which can already be shadowed by a member function, there's no need for any special consideration here.