1 /** Structure of arrays (SoA).
2  *
3  * SoAs are common in game engines.
4  *
5  * Initially a builtin feature in the Jai programming language that later was
6  * made into a library solution.
7  *
8  * TODO: merge with soa_petar_kirov.d by
9  * 1. allocate all arrays in a single chunk
10  * 2. calculating `_capacity` based on `_length`
11  *
12  * TODO: merge with variant_arrays.d?
13  * TODO: Maybe growth logic can be hidden inside a wrapper Allocator
14  *
15  * See_Also: http://forum.dlang.org/post/wvulryummkqtskiwrusb@forum.dlang.org
16  * See_Also: https://forum.dlang.org/post/purhollnapramxczmcka@forum.dlang.org
17  * See_Also: https://forum.dlang.org/post/cvxuagislrpfomalcelc@forum.dlang.org
18  * See_Also: https://maikklein.github.io/post/soa-d/
19  */
20 module nxt.container.soa;
21 
22 import std.experimental.allocator.mallocator : Mallocator;
23 import nxt.allocator_traits : isAllocator;
24 
25 /** Structure of Arrays similar to members of `S`.
26  */
27 struct SoA(S, Capacity = size_t, Allocator = Mallocator)
28 if (is(S == struct) &&          // TODO: extend to `isAggregate!S`?
29     isAllocator!Allocator)
30 {
31     /** Growth factor P/Q.
32         https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling
33         Use 1.5 like Facebook's `fbvector` does.
34     */
35     enum _growthP = 3;          // numerator
36     /// ditto
37     enum _growthQ = 2;          // denominator
38 
39     private alias toType(string s) = typeof(__traits(getMember, S, s));
40     private alias Types = typeof(S.tupleof);
41 
42     import std.experimental.allocator.common : stateSize;
43     static if (stateSize!Allocator != 0)
44     {
45         this(in Capacity initialCapacity, Allocator allocator)
46         {
47             _capacity = initialCapacity;
48             this.allocator = allocator;
49             allocate(initialCapacity);
50         }
51 
52         this() @disable; ///< No default construction if an allocator must be provided.
53     }
54     else
55         this(in Capacity initialCapacity)
56         {
57             _capacity = initialCapacity;
58             allocate(initialCapacity);
59         }
60 
61     @disable this(this);    // disable copy constructor
62 
63     ~this() nothrow @trusted @nogc
64     {
65         import std.experimental.allocator : dispose;
66         static foreach (const index, _; S.tupleof)
67             allocator.dispose(getArray!index);
68     }
69 
70 	pragma(inline, true):
71 
72     Capacity length() const @property @safe pure nothrow @nogc => _length;
73     Capacity capacity() const @property @safe pure nothrow @nogc => _capacity;
74     bool empty() const @property @safe pure nothrow @nogc => _length == 0;
75 
76     inout(SoAElementRef!S) opIndex()(in Capacity elementIndex) inout return /* template-lazy */
77 	in(elementIndex < _length)
78 		=> typeof(return)(&this, elementIndex);
79 
80     inout(SoASlice!S) opSlice()() inout return /* template-lazy */
81     	=> typeof(return)(&this);
82 
83     void opOpAssign(string op, S)(S value) if (op == "~") => insertBack(value);
84 
85 	pragma(inline):				// reset
86 
87     auto opDispatch(string name)()
88     {
89         static foreach (const index, memberSymbol; S.tupleof)
90             static if (name == memberSymbol.stringof)
91                 return getArray!index;
92         // TODO: static assert(0, S.stringof ~ " has no field named " ~ name);
93     }
94 
95     /** Push element (struct) `value` to back of array. */
96     void insertBack()(S value) @trusted /* template-lazy */
97     {
98         import core.lifetime : moveEmplace;
99         reserveOneExtra();
100         static foreach (const index, memberSymbol; S.tupleof)
101             moveEmplace(__traits(getMember, value, memberSymbol.stringof),
102                         getArray!index[_length]); // TODO: assert that
103         ++_length;
104     }
105 
106     /** Push element (struct) `value` to back of array using its data members `members`. */
107     void insertBackMembers()(Types members) @trusted /* template-lazy */
108     {
109         import core.lifetime : moveEmplace;
110         reserveOneExtra();
111         // move each member to its position respective array
112         static foreach (const index, _; members)
113             moveEmplace(members[index], getArray!index[_length]); // same as `getArray!index[_length] = members[index];`
114         ++_length;
115     }
116 
117 private:
118     import nxt.allocator_traits : AllocatorState;
119     mixin AllocatorState!Allocator; // put first as emsi-containers do
120 
121     // generate array definitions
122     static foreach (const index, Type; Types)
123         mixin(Type.stringof ~ `[] _container` ~ index.stringof ~ ";");
124 
125     /** Get array of all fields at aggregate field index `index`. */
126 	pragma(inline, true)
127     ref inout(Types[index][]) getArray(Capacity index)() inout return
128     {
129         mixin(`return _container` ~ index.stringof ~ ";");
130     }
131 
132     Capacity _length = 0;         ///< Current length.
133     Capacity _capacity = 0;       ///< Current capacity.
134 
135     void allocate(in Capacity newCapacity) @trusted
136     {
137         import std.experimental.allocator : makeArray;
138         static foreach (const index, _; S.tupleof)
139             getArray!index = allocator.makeArray!(Types[index])(newCapacity);
140     }
141 
142     /** Grow storage with at least on element. */
143     void grow() @trusted
144     {
145         // Motivation: https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling
146         import std.algorithm.comparison : max;
147         import std.experimental.allocator : expandArray;
148         const newCapacity = _capacity == 1 ? 2 : max(1, _growthP * _capacity / _growthQ);
149         const expandSize = newCapacity - _capacity;
150         if (_capacity is 0)
151             allocate(newCapacity);
152         else
153             static foreach (const index, _; S.tupleof)
154                 allocator.expandArray(getArray!index, expandSize);
155         _capacity = newCapacity;
156     }
157 
158     void reserveOneExtra()
159     {
160         if (_length == _capacity)
161             grow();
162     }
163 }
164 alias StructArrays = SoA;
165 
166 /** Reference to element in `soaPtr` at index `elementIndex`. */
167 private struct SoAElementRef(S)
168 if (is(S == struct))        // TODO: extend to `isAggregate!S`?
169 {
170     SoA!S* soaPtr;
171     size_t elementIndex;
172 
173     @disable this(this);
174 
175     /** Access member name `memberName`. */
176     auto ref opDispatch(string memberName)() @trusted return scope
177     {
178         mixin(`return ` ~ `(*soaPtr).` ~ memberName ~ `[elementIndex];`);
179     }
180 }
181 
182 /** Reference to slice in `soaPtr`. */
183 private struct SoASlice(S)
184 if (is(S == struct))            // TODO: extend to `isAggregate!S`?
185 {
186     SoA!S* soaPtr;
187 
188     @disable this(this);
189 
190     /** Access aggregate at `index`. */
191     inout(S) opIndex(in size_t index) inout @trusted return scope
192     {
193         S s = void;
194         static foreach (const memberIndex, memberSymbol; S.tupleof)
195             mixin(`s.` ~ memberSymbol.stringof ~ `= (*soaPtr).getArray!` ~ memberIndex.stringof ~ `[index];`);
196         return s;
197     }
198 }
199 
200 @safe pure nothrow @nogc unittest
201 {
202     import nxt.dip_traits : hasPreviewDIP1000;
203 
204     struct S { int i; float f; }
205 
206     auto x = SoA!S();
207 
208     static assert(is(typeof(x.getArray!0()) == int[]));
209     static assert(is(typeof(x.getArray!1()) == float[]));
210 
211     assert(x.length == 0);
212 
213     x.insertBack(S.init);
214     assert(x.length == 1);
215 
216     x ~= S.init;
217     assert(x.length == 2);
218 
219     x.insertBackMembers(42, 43f);
220     assert(x.length == 3);
221     assert(x.i[2] == 42);
222     assert(x.f[2] == 43f);
223 
224     // uses opDispatch
225     assert(x[2].i == 42);
226     assert(x[2].f == 43f);
227 
228     const x3 = SoA!S(3);
229     assert(x3.length == 0);
230     assert(x3.capacity == 3);
231 
232     // TODO: make foreach work
233     // foreach (_; x[])
234     // {
235     // }
236 
237     static if (hasPreviewDIP1000)
238     {
239         static assert(!__traits(compiles,
240                                 {
241                                     ref int testScope() @safe
242                                     {
243                                         auto y = SoA!S(1);
244                                         y ~= S(42, 43f);
245                                         return y[0].i;
246                                     }
247                                 }));
248     }
249 }