| /** |
| This module defines `TypedAllocator`, a statically-typed allocator that |
| aggregates multiple untyped allocators and uses them depending on the static |
| properties of the types allocated. For example, distinct allocators may be used |
| for thread-local vs. thread-shared data, or for fixed-size data (`struct`, |
| `class` objects) vs. resizable data (arrays). |
| |
| Macros: |
| T2=$(TR <td style="text-align:left">$(D $1)</td> $(TD $(ARGS $+))) |
| */ |
| |
| module std.experimental.allocator.typed; |
| |
| import std.experimental.allocator; |
| import std.experimental.allocator.common; |
| import std.range : isInputRange, isForwardRange, walkLength, save, empty, |
| front, popFront; |
| import std.traits : isPointer, hasElaborateDestructor; |
| import std.typecons : Flag, Yes, No; |
| |
| /** |
| Allocation-related flags dictated by type characteristics. `TypedAllocator` |
| deduces these flags from the type being allocated and uses the appropriate |
| allocator accordingly. |
| */ |
| enum AllocFlag : uint |
| { |
| init = 0, |
| /** |
| Fixed-size allocation (unlikely to get reallocated later). Examples: `int`, |
| `double`, any `struct` or `class` type. By default it is assumed that the |
| allocation is variable-size, i.e. susceptible to later reallocation |
| (for example all array types). This flag is advisory, i.e. in-place resizing |
| may be attempted for `fixedSize` allocations and may succeed. The flag is |
| just a hint to the compiler it may use allocation strategies that work well |
| with objects of fixed size. |
| */ |
| fixedSize = 1, |
| /** |
| The type being allocated embeds no pointers. Examples: `int`, `int[]`, $(D |
| Tuple!(int, float)). The implicit conservative assumption is that the type |
| has members with indirections so it needs to be scanned if garbage |
| collected. Example of types with pointers: `int*[]`, $(D Tuple!(int, |
| string)). |
| */ |
| hasNoIndirections = 4, |
| /** |
| By default it is conservatively assumed that allocated memory may be `cast` |
| to `shared`, passed across threads, and deallocated in a different thread |
| than the one that allocated it. If that's not the case, there are two |
| options. First, `immutableShared` means the memory is allocated for |
| `immutable` data and will be deallocated in the same thread it was |
| allocated in. Second, `threadLocal` means the memory is not to be shared |
| across threads at all. The two flags cannot be simultaneously present. |
| */ |
| immutableShared = 8, |
| /// ditto |
| threadLocal = 16, |
| } |
| |
| /** |
| `TypedAllocator` acts like a chassis on which several specialized allocators |
| can be assembled. To let the system make a choice about a particular kind of |
| allocation, use `Default` for the respective parameters. |
| |
| There is a hierarchy of allocation kinds. When an allocator is implemented for |
| a given combination of flags, it is used. Otherwise, the next down the list is |
| chosen. |
| |
| $(BOOKTABLE , |
| |
| $(TR $(TH `AllocFlag` combination) $(TH Description)) |
| |
| $(T2 AllocFlag.threadLocal |$(NBSP)AllocFlag.hasNoIndirections |
| |$(NBSP)AllocFlag.fixedSize, |
| This is the most specific allocation policy: the memory being allocated is |
| thread local, has no indirections at all, and will not be reallocated. Examples |
| of types fitting this description: `int`, `double`, $(D Tuple!(int, long)), but |
| not $(D Tuple!(int, string)), which contains an indirection.) |
| |
| $(T2 AllocFlag.threadLocal |$(NBSP)AllocFlag.hasNoIndirections, |
| As above, but may be reallocated later. Examples of types fitting this |
| description are $(D int[]), $(D double[]), $(D Tuple!(int, long)[]), but not |
| $(D Tuple!(int, string)[]), which contains an indirection.) |
| |
| $(T2 AllocFlag.threadLocal, |
| As above, but may embed indirections. Examples of types fitting this |
| description are $(D int*[]), $(D Object[]), $(D Tuple!(int, string)[]).) |
| |
| $(T2 AllocFlag.immutableShared |$(NBSP)AllocFlag.hasNoIndirections |
| |$(NBSP)AllocFlag.fixedSize, |
| The type being allocated is `immutable` and has no pointers. The thread that |
| allocated it must also deallocate it. Example: `immutable(int)`.) |
| |
| $(T2 AllocFlag.immutableShared |$(NBSP)AllocFlag.hasNoIndirections, |
| As above, but the type may be appended to in the future. Example: `string`.) |
| |
| $(T2 AllocFlag.immutableShared, |
| As above, but the type may embed references. Example: `immutable(Object)[]`.) |
| |
| $(T2 AllocFlag.hasNoIndirections |$(NBSP)AllocFlag.fixedSize, |
| The type being allocated may be shared across threads, embeds no indirections, |
| and has fixed size.) |
| |
| $(T2 AllocFlag.hasNoIndirections, |
| The type being allocated may be shared across threads, may embed indirections, |
| and has variable size.) |
| |
| $(T2 AllocFlag.fixedSize, |
| The type being allocated may be shared across threads, may embed indirections, |
| and has fixed size.) |
| |
| $(T2 0, The most conservative/general allocation: memory may be shared, |
| deallocated in a different thread, may or may not be resized, and may embed |
| references.) |
| ) |
| |
| Params: |
| PrimaryAllocator = The default allocator. |
| Policies = Zero or more pairs consisting of an `AllocFlag` and an allocator |
| type. |
| */ |
| struct TypedAllocator(PrimaryAllocator, Policies...) |
| { |
| import std.algorithm.sorting : isSorted; |
| import std.meta : AliasSeq; |
| import std.typecons : Tuple; |
| |
| static assert(Policies.length == 0 || isSorted([Stride2!Policies])); |
| |
| private template Stride2(T...) |
| { |
| static if (T.length >= 2) |
| { |
| alias Stride2 = AliasSeq!(T[0], Stride2!(T[2 .. $])); |
| } |
| else |
| { |
| alias Stride2 = AliasSeq!(T[0 .. $]); |
| } |
| } |
| |
| // state |
| static if (stateSize!PrimaryAllocator) private PrimaryAllocator primary; |
| else alias primary = PrimaryAllocator.instance; |
| static if (Policies.length > 0) |
| private Tuple!(Stride2!(Policies[1 .. $])) extras; |
| |
| private static bool match(uint have, uint want) |
| { |
| enum uint maskAway = |
| ~(AllocFlag.immutableShared | AllocFlag.threadLocal); |
| // Do we offer thread local? |
| if (have & AllocFlag.threadLocal) |
| { |
| if (want & AllocFlag.threadLocal) |
| return match(have & maskAway, want & maskAway); |
| return false; |
| } |
| if (have & AllocFlag.immutableShared) |
| { |
| // Okay to ask for either thread local or immutable shared |
| if (want & (AllocFlag.threadLocal |
| | AllocFlag.immutableShared)) |
| return match(have & maskAway, want & maskAway); |
| return false; |
| } |
| // From here on we have full-blown thread sharing. |
| if (have & AllocFlag.hasNoIndirections) |
| { |
| if (want & AllocFlag.hasNoIndirections) |
| return match(have & ~AllocFlag.hasNoIndirections, |
| want & ~AllocFlag.hasNoIndirections); |
| return false; |
| } |
| // Fixed size or variable size both match. |
| return true; |
| } |
| |
| /** |
| Given `flags` as a combination of `AllocFlag` values, or a type `T`, returns |
| the allocator that's a closest fit in capabilities. |
| */ |
| auto ref allocatorFor(uint flags)() |
| { |
| static if (Policies.length == 0 || !match(Policies[0], flags)) |
| { |
| return primary; |
| } |
| else static if (Policies.length && match(Policies[$ - 2], flags)) |
| { |
| return extras[$ - 1]; |
| } |
| else |
| { |
| foreach (i, choice; Stride2!Policies) |
| { |
| static if (!match(choice, flags)) |
| { |
| return extras[i - 1]; |
| } |
| } |
| assert(0); |
| } |
| } |
| |
| /// ditto |
| auto ref allocatorFor(T)() |
| { |
| static if (is(T == void[])) |
| { |
| return primary; |
| } |
| else |
| { |
| return allocatorFor!(type2flags!T)(); |
| } |
| } |
| |
| /** |
| Given a type `T`, returns its allocation-related flags as a combination of |
| `AllocFlag` values. |
| */ |
| static uint type2flags(T)() |
| { |
| uint result; |
| static if (is(T == immutable)) |
| result |= AllocFlag.immutableShared; |
| else static if (is(T == shared)) |
| result |= AllocFlag.forSharing; |
| static if (!is(T == U[], U)) |
| result |= AllocFlag.fixedSize; |
| import std.traits : hasIndirections; |
| static if (!hasIndirections!T) |
| result |= AllocFlag.hasNoIndirections; |
| return result; |
| } |
| |
| /** |
| Dynamically allocates (using the appropriate allocator chosen with |
| `allocatorFor!T`) and then creates in the memory allocated an object of |
| type `T`, using `args` (if any) for its initialization. Initialization |
| occurs in the memory allocated and is otherwise semantically the same as |
| `T(args)`. (Note that using `make!(T[])` creates a pointer to an |
| (empty) array of `T`s, not an array. To allocate and initialize an |
| array, use `makeArray!T` described below.) |
| |
| Params: |
| T = Type of the object being created. |
| args = Optional arguments used for initializing the created object. If not |
| present, the object is default constructed. |
| |
| Returns: If `T` is a class type, returns a reference to the created `T` |
| object. Otherwise, returns a `T*` pointing to the created object. In all |
| cases, returns `null` if allocation failed. |
| |
| Throws: If `T`'s constructor throws, deallocates the allocated memory and |
| propagates the exception. |
| */ |
| auto make(T, A...)(auto ref A args) |
| { |
| return .make!T(allocatorFor!T, args); |
| } |
| |
| /** |
| Create an array of `T` with `length` elements. The array is either |
| default-initialized, filled with copies of `init`, or initialized with |
| values fetched from `range`. |
| |
| Params: |
| T = element type of the array being created |
| length = length of the newly created array |
| init = element used for filling the array |
| range = range used for initializing the array elements |
| |
| Returns: |
| The newly-created array, or `null` if either `length` was `0` or |
| allocation failed. |
| |
| Throws: |
| The first two overloads throw only if the used allocator's primitives do. |
| The overloads that involve copy initialization deallocate memory and propagate the exception if the copy operation throws. |
| */ |
| T[] makeArray(T)(size_t length) |
| { |
| return .makeArray!T(allocatorFor!(T[]), length); |
| } |
| |
| /// Ditto |
| T[] makeArray(T)(size_t length, auto ref T init) |
| { |
| return .makeArray!T(allocatorFor!(T[]), init, length); |
| } |
| |
| /// Ditto |
| T[] makeArray(T, R)(R range) |
| if (isInputRange!R) |
| { |
| return .makeArray!T(allocatorFor!(T[]), range); |
| } |
| |
| /** |
| Grows `array` by appending `delta` more elements. The needed memory is |
| allocated using the same allocator that was used for the array type. The |
| extra elements added are either default-initialized, filled with copies of |
| `init`, or initialized with values fetched from `range`. |
| |
| Params: |
| T = element type of the array being created |
| array = a reference to the array being grown |
| delta = number of elements to add (upon success the new length of `array` |
| is $(D array.length + delta)) |
| init = element used for filling the array |
| range = range used for initializing the array elements |
| |
| Returns: |
| `true` upon success, `false` if memory could not be allocated. In the |
| latter case `array` is left unaffected. |
| |
| Throws: |
| The first two overloads throw only if the used allocator's primitives do. |
| The overloads that involve copy initialization deallocate memory and |
| propagate the exception if the copy operation throws. |
| */ |
| bool expandArray(T)(ref T[] array, size_t delta) |
| { |
| return .expandArray(allocatorFor!(T[]), array, delta); |
| } |
| /// Ditto |
| bool expandArray(T)(T[] array, size_t delta, auto ref T init) |
| { |
| return .expandArray(allocatorFor!(T[]), array, delta, init); |
| } |
| /// Ditto |
| bool expandArray(T, R)(ref T[] array, R range) |
| if (isInputRange!R) |
| { |
| return .expandArray(allocatorFor!(T[]), array, range); |
| } |
| |
| /** |
| Shrinks an array by `delta` elements using `allocatorFor!(T[])`. |
| |
| If $(D arr.length < delta), does nothing and returns `false`. Otherwise, |
| destroys the last $(D arr.length - delta) elements in the array and then |
| reallocates the array's buffer. If reallocation fails, fills the array with |
| default-initialized data. |
| |
| Params: |
| T = element type of the array being created |
| arr = a reference to the array being shrunk |
| delta = number of elements to remove (upon success the new length of |
| `arr` is $(D arr.length - delta)) |
| |
| Returns: |
| `true` upon success, `false` if memory could not be reallocated. In the |
| latter case $(D arr[$ - delta .. $]) is left with default-initialized |
| elements. |
| |
| Throws: |
| The first two overloads throw only if the used allocator's primitives do. |
| The overloads that involve copy initialization deallocate memory and |
| propagate the exception if the copy operation throws. |
| */ |
| bool shrinkArray(T)(ref T[] arr, size_t delta) |
| { |
| return .shrinkArray(allocatorFor!(T[]), arr, delta); |
| } |
| |
| /** |
| Destroys and then deallocates (using `allocatorFor!T`) the object pointed |
| to by a pointer, the class object referred to by a `class` or `interface` |
| reference, or an entire array. It is assumed the respective entities had |
| been allocated with the same allocator. |
| */ |
| void dispose(T)(T* p) |
| { |
| return .dispose(allocatorFor!T, p); |
| } |
| /// Ditto |
| void dispose(T)(T p) |
| if (is(T == class) || is(T == interface)) |
| { |
| return .dispose(allocatorFor!T, p); |
| } |
| /// Ditto |
| void dispose(T)(T[] array) |
| { |
| return .dispose(allocatorFor!(T[]), array); |
| } |
| } |
| |
| /// |
| @system unittest |
| { |
| import std.experimental.allocator.gc_allocator : GCAllocator; |
| import std.experimental.allocator.mallocator : Mallocator; |
| import std.experimental.allocator.mmap_allocator : MmapAllocator; |
| alias MyAllocator = TypedAllocator!(GCAllocator, |
| AllocFlag.fixedSize | AllocFlag.threadLocal, Mallocator, |
| AllocFlag.fixedSize | AllocFlag.threadLocal |
| | AllocFlag.hasNoIndirections, |
| MmapAllocator, |
| ); |
| MyAllocator a; |
| auto b = &a.allocatorFor!0(); |
| static assert(is(typeof(*b) == shared GCAllocator)); |
| enum f1 = AllocFlag.fixedSize | AllocFlag.threadLocal; |
| auto c = &a.allocatorFor!f1(); |
| static assert(is(typeof(*c) == Mallocator)); |
| enum f2 = AllocFlag.fixedSize | AllocFlag.threadLocal; |
| static assert(is(typeof(a.allocatorFor!f2()) == Mallocator)); |
| // Partial match |
| enum f3 = AllocFlag.threadLocal; |
| static assert(is(typeof(a.allocatorFor!f3()) == Mallocator)); |
| |
| int* p = a.make!int; |
| scope(exit) a.dispose(p); |
| int[] arr = a.makeArray!int(42); |
| scope(exit) a.dispose(arr); |
| assert(a.expandArray(arr, 3)); |
| assert(a.shrinkArray(arr, 4)); |
| } |