1 /** 2 Implements a memory block to place elements on. 3 */ 4 5 module alid.blockreusable; 6 7 import alid.errornogc : NogcError; 8 import std.typecons : Flag, Yes; 9 10 // The Error type that ReusableBlock throws 11 private mixin NogcError!"block"; 12 13 /** 14 A reusable memory block for placing objects on. 15 16 The elements are added only at the back either by copying or by moving. They 17 are removed only from the front. The elements are never moved to a different 18 memory location inside the buffer. 19 20 Params: 21 22 T = the type of the elements that will be stored on the block 23 dtors = whether to execute destructors of elements upon removing them 24 */ 25 struct ReusableBlock(T, Flag!"dtors" dtors = Yes.dtors) 26 { 27 private: 28 29 T * ptr_; // The address of the beginning of the block 30 size_t capacity_; // Total elements that the block can hold 31 size_t head_; // The block index where element 0 is currently at 32 size_t tail_; // The block index where the next element will be placed at 33 34 public: 35 36 /** 37 Construct an object that will use the provided memory 38 39 Params: 40 41 buffer = the memory _buffer to use for the elements; the `.ptr` property 42 must match `T.alignof` 43 */ 44 this(ubyte[] buffer) @nogc nothrow pure scope @trusted 45 { 46 const extra = cast(ulong)buffer.ptr % T.alignof; 47 if (extra) { 48 // Align to the next aligned memory location 49 buffer = buffer[T.alignof - extra .. $]; 50 } 51 assert(cast(ulong)buffer.ptr % T.alignof == 0); 52 53 this.ptr_ = cast(T*)(buffer.ptr); 54 this.capacity_ = buffer.length / T.sizeof; 55 this.head_ = 0; 56 this.tail_ = 0; 57 } 58 59 /// Pointer to the beginning of the block 60 inout(T) * ptr() inout @nogc nothrow pure @safe scope 61 { 62 return ptr_; 63 } 64 65 /// Total _capacity of the block 66 size_t capacity() const @nogc nothrow pure @safe scope 67 { 68 return capacity_; 69 } 70 71 /// Number of elements the block currently has room for 72 size_t freeCapacity() const @nogc nothrow pure @safe scope 73 in (tail_ <= capacity_, blockError("Tail is ahead of capacity", this)) 74 { 75 return capacity - tail_; 76 } 77 78 /// Current number of elements in the block 79 size_t length() const @nogc nothrow pure @safe scope 80 in (head_ <= tail_, blockError("Head is ahead of tail", this)) 81 { 82 return tail_ - head_; 83 } 84 85 /// Whether the block has no elements at all 86 bool empty() const @nogc nothrow pure @safe scope 87 { 88 return length == 0; 89 } 90 91 /** 92 Add an _element after the existing elements; lvalues are copied, rvalues 93 are moved. 94 95 Params: 96 97 element = the _element to add 98 */ 99 void opOpAssign(string op, SourceT)(auto ref SourceT element) 100 if (op == "~") 101 in (freeCapacity, blockError("No room to append", this)) 102 { 103 import std.traits : isCopyable; 104 105 // This code is copied from std.array.Appender.put: 106 auto elementUnqual = (() @trusted => & cast() element)(); 107 108 static if (__traits(isRef, element)) 109 { 110 // We have an lvalue 111 import core.lifetime : emplace; 112 import std.format : format; 113 114 static assert(isCopyable!SourceT, 115 format!"%s is not copyable"(SourceT.stringof)); 116 117 emplace(unqualPtr_ + tail_, *elementUnqual); 118 ++tail_; 119 } 120 else 121 { 122 // We have an rvalue 123 this.moveEmplace(element); 124 } 125 } 126 127 /** 128 Calls `~=` to copy the specified _element 129 130 Params: 131 132 element = the _element to copy 133 */ 134 void copyBack(SourceT)(auto ref const(SourceT) element) 135 { 136 this ~= element; 137 } 138 139 /** 140 Move the specified _element to the back of existing elements 141 142 Params: 143 144 element = the _element to move 145 */ 146 void moveEmplace(SourceT)(ref SourceT element) 147 in (freeCapacity, blockError("No room to moveEmplaceBack", this)) 148 { 149 import core.lifetime : moveEmplace; 150 151 // This code is copied from std.array.Appender.put: 152 auto elementUnqual = (() @trusted => & cast() element)(); 153 154 moveEmplace(*elementUnqual, *(unqualPtr_ + tail_)); 155 ++tail_; 156 } 157 158 /** 159 Construct a new element after the existing elements 160 161 Params: 162 163 args = the constructor arguments to use 164 */ 165 void emplaceBack(Args...)(auto ref Args args) 166 in (freeCapacity, blockError("No room to emplaceBack", this)) 167 { 168 import core.lifetime : emplace; 169 import std.format : format; 170 171 static assert(__traits(compiles, emplace(ptr_, args)), 172 format!"%s is not copyable"(T.stringof)); 173 174 emplace(ptr_ + tail_, args); 175 ++tail_; 176 } 177 178 /** 179 Return a reference to an element 180 181 Params: 182 183 index = the _index of the element to return 184 */ 185 ref inout(T) opIndex(in size_t index) inout @nogc nothrow pure scope 186 in (index < length, blockError("Invalid index", index, length)) 187 { 188 return ptr_[head_ + index]; 189 } 190 191 /** 192 Remove elements from the head of the block. If `n == length`, 193 `freeCapacity` will be equal to `capacity` after this call. Whether the 194 destructors are called on the removed elements is determined by the 195 `dtors` template parameter. 196 197 Params: 198 199 n = number of elements to remove 200 */ 201 void removeFrontN(in size_t n) scope 202 in (n <= length, blockError("Not enough elements to removeFrontN", n, this)) 203 { 204 import std.traits : hasElaborateDestructor; 205 206 static if (dtors && hasElaborateDestructor!T) 207 { 208 foreach_reverse(i; head_ .. head_ + n) 209 { 210 destroy(unqualPtr_[i]); 211 } 212 } 213 214 if (n == length) 215 { 216 // No element left; start from the beginning 217 head_ = 0; 218 tail_ = 0; 219 } 220 else 221 { 222 head_ += n; 223 } 224 } 225 226 /// The same as calling `this.removeFrontN(this.length)` 227 void clear() scope 228 { 229 removeFrontN(length); 230 } 231 232 /// Number of elements in the block 233 size_t opDollar() const @nogc nothrow pure @safe scope 234 { 235 return length; 236 } 237 238 /** 239 A slice providing access _to the specified elements 240 241 Params: 242 243 from = the index of the first element of the slice 244 to = the index of the element one beyond the last element of the slice 245 */ 246 inout(T)[] opSlice(in size_t from, in size_t to) inout @nogc nothrow pure scope 247 in (from <= to, blockError("Range begin is greater than end", from, to)) 248 in (to - from <= length, blockError("Range is too long", from, to, length)) 249 { 250 return ptr[head_ + from .. head_ + to]; 251 } 252 253 /// A slice to all elements; the same as `[0..$]` 254 inout(T)[] opSlice() inout @nogc nothrow pure scope 255 { 256 return ptr[head_ .. tail_]; 257 } 258 259 /// String representation of this object mostly for debugging 260 void toString(scope void delegate(in char[]) sink) const scope 261 { 262 import std.format : formattedWrite; 263 264 sink.formattedWrite!"%s @%s cap:%s elems:[%s..%s]"( 265 T.stringof, ptr_, capacity_, head_, tail_); 266 } 267 268 private auto unqualPtr_() inout @nogc nothrow pure scope 269 { 270 import std.traits : Unqual; 271 272 return cast(Unqual!T*)ptr_; 273 } 274 } 275 276 /// 277 unittest 278 { 279 // Constructing a block on a piece of memory 280 ubyte[100] buffer; 281 auto b = ReusableBlock!int(buffer); 282 283 // Depending on the alignment of the elements, the capacity may be less than 284 // then requested amount 285 assert(b.capacity <= buffer.length / int.sizeof); 286 287 // Add 2 elements 288 b ~= 0; 289 b ~= 1; 290 291 b.length.shouldBe(2); 292 b[0].shouldBe(0); 293 b[1].shouldBe(1); 294 b.freeCapacity.shouldBe(b.capacity - 2); 295 296 // Remove the first one 297 b.removeFrontN(1); 298 b.length.shouldBe(1); 299 b[0].shouldBe(1); 300 // Note that free capacity does not increase: 301 b.freeCapacity.shouldBe(b.capacity - 2); 302 303 // Remove all elements and reset the block 304 b.clear(); 305 b.length.shouldBe(0); 306 // This time all capacity is free 307 b.freeCapacity.shouldBe(b.capacity); 308 } 309 310 version (unittest) 311 { 312 import alid.test : shouldBe; 313 314 private void assertInitialState(B)(B b) 315 { 316 import std.array : empty; 317 318 b.capacity.shouldBe(b.freeCapacity); 319 b.length.shouldBe(0); 320 assert(b.empty); 321 assert(b[0..$].empty); 322 assert(b[].empty); 323 } 324 325 private auto makeValue(T)(int i) 326 if (is (T == string)) 327 { 328 import std.format : format; 329 330 return format!"value_%s"(i); 331 } 332 333 private auto makeValue(T)(int i) 334 if (!is (T == string)) 335 { 336 return cast(T)i; 337 } 338 } 339 340 unittest 341 { 342 // Initializing with null buffer should not be an error 343 344 auto b = ReusableBlock!int(null); 345 assertInitialState(b); 346 } 347 348 unittest 349 { 350 // Test with some fundamental types 351 352 import std.array : empty; 353 import std.meta : AliasSeq; 354 355 void test(T)() 356 { 357 // Must be large enough to pass range tests below 358 enum length = 40 * int.sizeof; 359 ubyte[length] buffer; 360 auto b = ReusableBlock!T(buffer); 361 const cap = b.capacity; 362 363 assertInitialState(b); 364 365 // Add 1 element 366 const e = makeValue!T(42); 367 b ~= e; 368 b.capacity.shouldBe(cap); 369 b.freeCapacity.shouldBe(b.capacity - 1); 370 b.length.shouldBe(1); 371 assert(!b.empty); 372 b[0].shouldBe(e); 373 b[0..$].shouldBe([ e ]); 374 b[].shouldBe([ e ]); 375 376 // Drop the element 377 b.removeFrontN(1); 378 b.capacity.shouldBe(cap); 379 380 // As all elements are removed, free capacity is increased automatically 381 b.freeCapacity.shouldBe(b.capacity); 382 b.length.shouldBe(0); 383 assert(b.empty); 384 assert(b[0..$].empty); 385 assert(b[].empty); 386 387 // Add another one 388 const f = makeValue!T(43); 389 b ~= f; 390 b.capacity.shouldBe(cap); 391 392 // Note that free capacity is reduced 393 b.freeCapacity.shouldBe(b.capacity - 1); 394 b.length.shouldBe(1); 395 assert(!b.empty); 396 b[0].shouldBe(f); 397 b[0..$].shouldBe([ f ]); 398 b[].shouldBe([ f ]); 399 400 // Clearing should get us back to the beginning 401 b.clear(); 402 assertInitialState(b); 403 404 // Fill test 405 T[] expected; 406 foreach (i; 0 .. b.capacity) 407 { 408 import std.conv : to; 409 410 assert(b.freeCapacity); 411 const elem = makeValue!T(i.to!int); 412 b ~= elem; 413 expected ~= elem; 414 } 415 416 // Maximum capacity should not change 417 b.capacity.shouldBe(cap); 418 b.freeCapacity.shouldBe(0); 419 b.length.shouldBe(b.capacity); 420 assert(!b.empty); 421 b[0..$].shouldBe(expected); 422 b[].shouldBe(expected); 423 424 b.removeFrontN(7); 425 b.length.shouldBe(b.capacity - 7); 426 b[].shouldBe(expected[7..$]); 427 b[0..$].shouldBe(expected[7..$]); 428 b[2..$-1].shouldBe(expected[9..$-1]); 429 430 // Multiple clear calls 431 b.clear(); 432 b.clear(); 433 assertInitialState(b); 434 } 435 436 alias Ts = AliasSeq!(int, const(int), double, immutable(double), ubyte, string); 437 438 foreach (T; Ts) 439 { 440 test!T(); 441 } 442 } 443 444 unittest 445 { 446 import std.meta : AliasSeq; 447 448 struct S 449 { 450 int i = int.max; 451 int j = int.max; 452 453 this(int i, int j) 454 { 455 this.i = i; 456 this.j = j; 457 } 458 459 this(this) @nogc pure @safe scope {} 460 } 461 462 void test(T)() 463 { 464 enum length = 400; 465 auto buffer = new ubyte[length]; 466 auto b = ReusableBlock!T(buffer); 467 468 b ~= T(2, 2); 469 470 const n = T(7, 7); 471 b~= n; 472 473 b.copyBack(n); 474 b.copyBack(T(8, 8)); 475 b.copyBack(n); 476 477 auto m = T(3, 3); 478 b.moveEmplace(m); 479 assert(m == S.init); 480 } 481 482 alias Ts = AliasSeq!(S, const(S)); 483 484 foreach (T; Ts) 485 { 486 test!T(); 487 } 488 } 489 490 unittest 491 { 492 // Destructors should be executed when requested 493 494 import std.algorithm : each; 495 import std.format : format; 496 import std.range : iota; 497 import std.typecons : No; 498 499 void test(Flag!"dtors" dtors)(size_t elementCount, size_t expectedDtorCount) 500 { 501 static struct S 502 { 503 size_t * count = null; 504 505 ~this() 506 { 507 if (count) 508 { 509 ++(*count); 510 } 511 } 512 } 513 514 auto b = ReusableBlock!(S, dtors)(new ubyte[100]); 515 516 size_t count = 0; 517 assert(count == 0, "Invalid unittest"); 518 519 iota(elementCount).each!(_ => b ~= S(&count)); 520 b.removeFrontN(elementCount); 521 522 count.shouldBe(expectedDtorCount); 523 } 524 525 test!(Yes.dtors)(10, 10); 526 test!(No.dtors)(10, 0); 527 }