1 /** Statically allocated arrays with compile-time known lengths. 2 */ 3 module nxt.container.static_array; 4 5 @safe pure: 6 7 /** Statically allocated `T`-array of fixed pre-allocated length. 8 * 9 * Similar to C++'s `std::static_vector<T, Capacity>` 10 * Similar to Rust's `fixedvec`: https://docs.rs/fixedvec/0.2.4/fixedvec/ 11 * Similar to `mir.small_array` at http://mir-algorithm.libmir.org/mir_small_array.html 12 * 13 * See_Also: https://en.cppreference.com/w/cpp/container/array 14 * 15 * TODO: Merge member functions with basic_*_array.d and array_ex.d 16 */ 17 struct StaticArray(T, uint capacity_, bool borrowChecked = false) { 18 import core.exception : onRangeError; 19 import core.lifetime : move, moveEmplace; 20 import core.internal.traits : hasElaborateDestructor; 21 import std.bitmanip : bitfields; 22 import std.traits : isSomeChar, isAssignable, hasIndirections; 23 import nxt.container.traits : mustAddGCRange; 24 25 alias capacity = capacity_; // for public use 26 27 /// Store of `capacity` number of elements. 28 T[capacity] _store; /+ TODO: use store constructor +/ 29 30 static if (borrowChecked) { 31 /// Number of bits needed to store number of read borrows. 32 private enum readBorrowCountBits = 3; 33 34 /// Maximum value possible for `_readBorrowCount`. 35 enum readBorrowCountMax = 2^^readBorrowCountBits - 1; 36 37 static if (capacity <= 2^^(8*ubyte.sizeof - 1 - readBorrowCountBits) - 1) { 38 private enum lengthMax = 2^^4 - 1; 39 alias Length = ubyte; 40 /+ TODO: make private: +/ 41 mixin(bitfields!(Length, "_length", 4, /// number of defined elements in `_store` 42 bool, "_writeBorrowed", 1, 43 uint, "_readBorrowCount", readBorrowCountBits, 44 )); 45 } 46 else static if (capacity <= 2^^(8*ushort.sizeof - 1 - readBorrowCountBits) - 1) { 47 alias Length = ushort; 48 private enum lengthMax = 2^^14 - 1; 49 /+ TODO: make private: +/ 50 mixin(bitfields!(Length, "_length", 14, /// number of defined elements in `_store` 51 bool, "_writeBorrowed", 1, 52 uint, "_readBorrowCount", readBorrowCountBits, 53 )); 54 } 55 else 56 { 57 static assert("Too large requested capacity " ~ capacity); 58 } 59 } 60 else 61 { 62 static if (capacity <= ubyte.max) { 63 static if (T.sizeof == 1) 64 alias Length = ubyte; // pack length 65 else 66 alias Length = uint; 67 } 68 else static if (capacity <= ushort.max) { 69 static if (T.sizeof <= 2) 70 alias Length = uint; // pack length 71 else 72 alias Length = uint; 73 } 74 else 75 { 76 static assert("Too large requested capacity " ~ capacity); 77 } 78 Length _length; /// number of defined elements in `_store` 79 } 80 81 /// Is `true` if `U` can be assigned to the elements of `this`. 82 private enum isElementAssignable(U) = isAssignable!(T, U); 83 84 /// Empty. 85 void clear() @nogc 86 { 87 releaseElementsStore(); 88 resetInternalData(); 89 } 90 91 /// Release elements and internal store. 92 private void releaseElementsStore() @trusted @nogc 93 { 94 static if (borrowChecked) { assert(!isBorrowed); } 95 foreach (immutable i; 0 .. length) 96 static if (hasElaborateDestructor!T) 97 .destroy(_store.ptr[i]); 98 else static if (hasIndirections!T) 99 _store.ptr[i] = T.init; // nullify any pointers 100 } 101 102 /// Reset internal data. 103 private void resetInternalData() @nogc 104 { 105 version (D_Coverage) {} else pragma(inline, true); 106 _length = 0; 107 } 108 109 /// Construct from element `values`. 110 this(Us...)(Us values) @trusted 111 if (Us.length <= capacity) { 112 static foreach (const i, value; values) 113 static if (__traits(isPOD, typeof(value))) 114 _store[i] = value; 115 else 116 moveEmplace(value, _store[i]); 117 _length = cast(Length)values.length; 118 static if (borrowChecked) { 119 _writeBorrowed = false; 120 _readBorrowCount = 0; 121 } 122 } 123 124 /// Construct from element `values`. 125 this(U)(U[] values) @trusted 126 if (__traits(isCopyable, U)// && 127 /+ TODO: isElementAssignable!U +/ 128 ) // prevent accidental move of l-value `values` in array calls 129 { 130 version (assert) if (values.length > capacity) onRangeError(); // `Arguments don't fit in array` 131 _store[0 .. values.length] = values; 132 _length = cast(Length)values.length; 133 static if (borrowChecked) { 134 _writeBorrowed = false; 135 _readBorrowCount = 0; 136 } 137 } 138 139 /// Construct from element `values`. 140 static typeof(this) fromValuesUnsafe(U)(U[] values) @system 141 if (__traits(isCopyable, U) && 142 isElementAssignable!U 143 ) // prevent accidental move of l-value `values` in array calls 144 { 145 typeof(return) that; /+ TODO: use Store constructor: +/ 146 147 that._store[0 .. values.length] = values; 148 that._length = cast(Length)values.length; 149 150 static if (borrowChecked) { 151 that._writeBorrowed = false; 152 that._readBorrowCount = 0; 153 } 154 155 return that; 156 } 157 158 static if (borrowChecked || 159 hasElaborateDestructor!T) { 160 /** Destruct. */ 161 ~this() nothrow @nogc 162 { 163 releaseElementsStore(); 164 } 165 } 166 167 /** Add elements `es` to the back. 168 * Throws when array becomes full. 169 * NOTE: doesn't invalidate any borrow 170 */ 171 void insertBack(Es...)(Es es) @trusted 172 if (Es.length <= capacity) /+ TODO: use `isAssignable` +/ 173 { 174 version (assert) if (_length + Es.length > capacity) onRangeError(); // `Arguments don't fit in array` 175 static foreach (const i, e; es) { 176 static if (__traits(isPOD, T)) 177 _store[_length + i] = e; 178 else 179 moveEmplace(e, _store[_length + i]); /+ TODO: remove when compiler does this +/ 180 } 181 _length = cast(Length)(_length + Es.length); /+ TODO: better? +/ 182 } 183 /// ditto 184 alias put = insertBack; // `OutputRange` support 185 186 /** Try to add elements `es` to the back. 187 * NOTE: doesn't invalidate any borrow 188 * Returns: `true` iff all `es` were pushed, `false` otherwise. 189 */ 190 bool insertBackMaybe(Es...)(Es es) @trusted 191 if (Es.length <= capacity) /+ TODO: use `isAssignable` +/ 192 { 193 version (LDC) pragma(inline, true); 194 if (_length + Es.length > capacity) { return false; } 195 insertBack(es); 196 return true; 197 } 198 /// ditto 199 alias putMaybe = insertBackMaybe; 200 201 /** Add elements `es` to the back. 202 * NOTE: doesn't invalidate any borrow 203 */ 204 void opOpAssign(string op, Us...)(Us values) 205 if (op == "~" && 206 values.length >= 1 && 207 allSatisfy!(isElementAssignable, Us)) { 208 insertBack(values.move()); /+ TODO: remove `move` when compiler does it for +/ 209 } 210 211 import std.traits : isMutable; 212 static if (isMutable!T) { 213 /** Pop first (front) element. */ 214 auto ref popFront() in(!empty) { 215 static if (borrowChecked) { assert(!isBorrowed); } 216 /+ TODO: is there a reusable Phobos function for this? +/ 217 foreach (immutable i; 0 .. _length - 1) 218 move(_store[i + 1], _store[i]); // like `_store[i] = _store[i + 1];` but more generic 219 _length = cast(typeof(_length))(_length - 1); /+ TODO: better? +/ 220 return this; 221 } 222 } 223 224 /** Pop last (back) element. */ 225 pragma(inline, true) 226 void popBack()() @trusted in(!empty) /*tlm*/ 227 { 228 static if (borrowChecked) { assert(!isBorrowed); } 229 _length = cast(Length)(_length - 1); /+ TODO: better? +/ 230 static if (hasElaborateDestructor!T) 231 .destroy(_store.ptr[_length]); 232 else static if (mustAddGCRange!T) 233 _store.ptr[_length] = T.init; // avoid GC mark-phase dereference 234 } 235 236 pragma(inline, true) 237 T takeBack()() @trusted in(!empty) /*tlm*/ 238 { 239 static if (__traits(isPOD, T)) 240 return _store.ptr[--_length]; // no move needed 241 else 242 return move(_store.ptr[--_length]); // move is indeed need here 243 } 244 245 /** Pop the `n` last (back) elements. */ 246 void popBackN()(size_t n) @trusted in(length >= n) /*tlm*/ 247 { 248 static if (borrowChecked) { assert(!isBorrowed); } 249 _length = cast(Length)(_length - n); /+ TODO: better? +/ 250 static if (hasElaborateDestructor!T) 251 foreach (const i; 0 .. n) 252 .destroy(_store.ptr[_length + i]); 253 else static if (mustAddGCRange!T) // avoid GC mark-phase dereference 254 foreach (const i; 0 .. n) 255 _store.ptr[_length + i] = T.init; 256 } 257 258 /** Move element at `index` to return. */ 259 static if (isMutable!T) { 260 /** Pop element at `index`. */ 261 void popAt()(size_t index) /*tlm*/ 262 @trusted 263 @("complexity", "O(length)") 264 in(index < this.length) { 265 .destroy(_store.ptr[index]); 266 shiftToFrontAt(index); 267 _length = cast(Length)(_length - 1); 268 } 269 270 T moveAt()(size_t index) /*tlm*/ 271 @trusted 272 @("complexity", "O(length)") 273 in(index < this.length) { 274 auto value = _store.ptr[index].move(); 275 shiftToFrontAt(index); 276 _length = cast(Length)(_length - 1); 277 return value; 278 } 279 280 private void shiftToFrontAt()(size_t index) /*tlm*/ 281 @trusted 282 { 283 foreach (immutable i; 0 .. this.length - (index + 1)) { 284 immutable si = index + i + 1; // source index 285 immutable ti = index + i; // target index 286 moveEmplace(_store.ptr[si], 287 _store.ptr[ti]); 288 } 289 } 290 } 291 292 /** Index operator. */ 293 pragma(inline, true) 294 ref inout(T) opIndex(size_t i) inout return => _store[i]; 295 296 /** First (front) element. */ 297 pragma(inline, true) 298 ref inout(T) front() inout return => _store[0]; 299 300 /** Last (back) element. */ 301 pragma(inline, true) 302 ref inout(T) back() inout return => _store[_length - 1]; 303 304 static if (borrowChecked) { 305 import nxt.borrowed : ReadBorrowed; 306 import core.internal.traits : Unqual; 307 308 /// Get full read-only slice. 309 ReadBorrowed!(T[], typeof(this)) sliceRO() const @trusted return scope 310 in(!_writeBorrowed, "Already write-borrowed") 311 => typeof(return)(_store[0 .. _length], 312 cast(Unqual!(typeof(this))*)(&this)); // trusted unconst cast 313 314 /// Get read-only slice in range `i` .. `j`. 315 ReadBorrowed!(T[], typeof(this)) sliceRO(size_t i, size_t j) const @trusted return scope 316 in(!_writeBorrowed, "Already write-borrowed") 317 => typeof(return)(_store[i .. j], 318 cast(Unqual!(typeof(this))*)(&this)); // trusted unconst cast 319 320 import nxt.borrowed : WriteBorrowed; 321 322 /// Get full read-write slice. 323 WriteBorrowed!(T[], typeof(this)) sliceRW() @trusted return scope /+ TODO: remove @trusted? +/ 324 in(!_writeBorrowed, "Already write-borrowed") 325 in(_readBorrowCount == 0, "Already read-borrowed") 326 => typeof(return)(_store[0 .. _length], &this); 327 328 /// Get read-write slice in range `i` .. `j`. 329 WriteBorrowed!(T[], typeof(this)) sliceRW(size_t i, size_t j) @trusted return scope /+ TODO: remove @trusted? +/ 330 in(!_writeBorrowed, "Already write-borrowed") 331 in(_readBorrowCount == 0, "Already read-borrowed") 332 => typeof(return)(_store[0 .. j], &this); 333 334 @property pragma(inline, true) { 335 /// Get read-only slice in range `i` .. `j`. 336 auto opSlice(size_t i, size_t j) const return scope => sliceRO(i, j); 337 /// Get read-write slice in range `i` .. `j`. 338 auto opSlice(size_t i, size_t j) return scope => sliceRW(i, j); 339 340 /// Get read-only full slice. 341 auto opSlice() const return scope => sliceRO(); 342 /// Get read-write full slice. 343 auto opSlice() return scope => sliceRW(); 344 345 /// Returns: `true` iff `this` is either write or read borrowed. 346 bool isBorrowed() const => _writeBorrowed || _readBorrowCount >= 1; 347 348 /// Returns: `true` iff `this` is write borrowed. 349 bool isWriteBorrowed() const => _writeBorrowed; 350 351 /// Returns: number of read-only borrowers of `this`. 352 uint readBorrowCount() const => _readBorrowCount; 353 } 354 } 355 else 356 { 357 /// Get slice in range `i` .. `j`. 358 pragma(inline, true); 359 inout(T)[] opSlice(size_t i, size_t j) inout @trusted return /+ TODO: remove @trusted? +/ 360 // in(i <= j) 361 // in(j <= _length) 362 => _store[i .. j]; 363 364 /// Get full slice. 365 pragma(inline, true) 366 inout(T)[] opSlice() inout @trusted return /+ TODO: remove @trusted? +/ 367 => _store[0 .. _length]; 368 } 369 370 @property pragma(inline, true) { 371 /** Returns: `true` iff `this` is empty, `false` otherwise. */ 372 bool empty() const @property { return _length == 0; } 373 374 /** Returns: `true` iff `this` is full, `false` otherwise. */ 375 bool full() const { return _length == capacity; } 376 377 /** Get length. */ 378 auto length() const { return _length; } 379 alias opDollar = length; /// ditto 380 381 static if (isSomeChar!T) { 382 /** Get as `string`. */ 383 scope const(T)[] toString() const return 384 { 385 version (DigitalMars) pragma(inline, false); 386 return opSlice(); 387 } 388 } 389 } 390 391 /** Comparison for equality. */ 392 bool opEquals()(const scope auto ref typeof(this) rhs) const 393 => this[] == rhs[]; 394 /// ditto 395 bool opEquals(U)(const scope U[] rhs) const 396 if (is(typeof(T[].init == U[].init))) 397 => this[] == rhs; 398 } 399 400 /** Stack-allocated string of maximum length of `capacity.` 401 * 402 * Similar to `mir.small_string` at http://mir-algorithm.libmir.org/mir_small_string.html. 403 */ 404 alias StringN(uint capacity, bool borrowChecked = false) = StaticArray!(immutable(char), capacity, borrowChecked); 405 406 /** Stack-allocated wstring of maximum length of `capacity.` */ 407 alias WStringN(uint capacity, bool borrowChecked = false) = StaticArray!(immutable(wchar), capacity, borrowChecked); 408 409 /** Stack-allocated dstring of maximum length of `capacity.` */ 410 alias DStringN(uint capacity, bool borrowChecked = false) = StaticArray!(immutable(dchar), capacity, borrowChecked); 411 412 /** Stack-allocated mutable string of maximum length of `capacity.` */ 413 alias MutableStringN(uint capacity, bool borrowChecked = false) = StaticArray!(char, capacity, borrowChecked); 414 415 /** Stack-allocated mutable wstring of maximum length of `capacity.` */ 416 alias MutableWStringN(uint capacity, bool borrowChecked = false) = StaticArray!(char, capacity, borrowChecked); 417 418 /** Stack-allocated mutable dstring of maximum length of `capacity.` */ 419 alias MutableDStringN(uint capacity, bool borrowChecked = false) = StaticArray!(char, capacity, borrowChecked); 420 421 /// construct from array may throw 422 pure @safe unittest { 423 enum capacity = 3; 424 alias T = int; 425 alias A = StaticArray!(T, capacity); 426 static assert(!mustAddGCRange!A); 427 428 auto a = A([1, 2, 3].s[]); 429 assert(a[] == [1, 2, 3].s); 430 } 431 432 /// unsafe construct from array 433 @trusted pure nothrow @nogc unittest { 434 enum capacity = 3; 435 alias T = int; 436 alias A = StaticArray!(T, capacity); 437 static assert(!mustAddGCRange!A); 438 439 auto a = A.fromValuesUnsafe([1, 2, 3].s); 440 assert(a[] == [1, 2, 3].s); 441 } 442 443 /// construct from scalars is nothrow 444 pure nothrow @safe @nogc unittest { 445 enum capacity = 3; 446 alias T = int; 447 alias A = StaticArray!(T, capacity); 448 static assert(!mustAddGCRange!A); 449 450 auto a = A(1, 2, 3); 451 assert(a[] == [1, 2, 3].s); 452 453 static assert(!__traits(compiles, { auto _ = A(1, 2, 3, 4); })); 454 } 455 456 /// scope checked string 457 pure @safe unittest { 458 enum capacity = 15; 459 foreach (StrN; AliasSeq!(StringN// , WStringN, DStringN 460 )) { 461 alias String15 = StrN!(capacity); 462 463 typeof(String15.init[0])[] xs; 464 assert(xs.length == 0); 465 auto x = String15("alphas"); 466 467 assert(x[0] == 'a'); 468 assert(x[$ - 1] == 's'); 469 470 assert(x[0 .. 2] == "al"); 471 assert(x[] == "alphas"); 472 473 const y = String15("åäö_åäöå"); // fits in 15 chars 474 assert(y.length == capacity); 475 } 476 } 477 478 /// scope checked string 479 pure unittest { 480 enum capacity = 15; 481 foreach (Str; AliasSeq!(StringN!capacity, 482 WStringN!capacity, 483 DStringN!capacity)) { 484 static assert(!mustAddGCRange!Str); 485 static if (hasPreviewDIP1000) { 486 static assert(!__traits(compiles, { 487 auto f() @safe pure 488 { 489 auto x = Str("alphas"); 490 auto y = x[]; 491 return y; // errors with -dip1000 492 } 493 })); 494 } 495 } 496 } 497 498 pure @safe unittest { 499 static assert(mustAddGCRange!(StaticArray!(string, 1, false))); 500 static assert(mustAddGCRange!(StaticArray!(string, 1, true))); 501 static assert(mustAddGCRange!(StaticArray!(string, 2, false))); 502 static assert(mustAddGCRange!(StaticArray!(string, 2, true))); 503 } 504 505 /// 506 pure @safe unittest { 507 import std.exception : assertNotThrown; 508 509 alias T = char; 510 enum capacity = 3; 511 512 alias A = StaticArray!(T, capacity, true); 513 static assert(!mustAddGCRange!A); 514 static assert(A.sizeof == T.sizeof*capacity + 1); 515 516 import std.range.primitives : isOutputRange; 517 static assert(isOutputRange!(A, T)); 518 519 auto ab = A("ab"); 520 assert(!ab.empty); 521 assert(ab[0] == 'a'); 522 assert(ab.front == 'a'); 523 assert(ab.back == 'b'); 524 assert(ab.length == 2); 525 assert(ab[] == "ab"); 526 assert(ab[0 .. 1] == "a"); 527 assertNotThrown(ab.insertBack('_')); 528 assert(ab[] == "ab_"); 529 ab.popBack(); 530 assert(ab[] == "ab"); 531 assert(ab.toString == "ab"); 532 533 ab.popBackN(2); 534 assert(ab.empty); 535 assertNotThrown(ab.insertBack('a', 'b')); 536 537 const abc = A("abc"); 538 assert(!abc.empty); 539 assert(abc.front == 'a'); 540 assert(abc.back == 'c'); 541 assert(abc.length == 3); 542 assert(abc[] == "abc"); 543 assert(ab[0 .. 2] == "ab"); 544 assert(abc.full); 545 static assert(!__traits(compiles, { const abcd = A('a', 'b', 'c', 'd'); })); // too many elements 546 547 assert(ab[] == "ab"); 548 ab.popFront(); 549 assert(ab[] == "b"); 550 551 const xy = A("xy"); 552 assert(!xy.empty); 553 assert(xy[0] == 'x'); 554 assert(xy.front == 'x'); 555 assert(xy.back == 'y'); 556 assert(xy.length == 2); 557 assert(xy[] == "xy"); 558 assert(xy[0 .. 1] == "x"); 559 560 const xyz = A("xyz"); 561 assert(!xyz.empty); 562 assert(xyz.front == 'x'); 563 assert(xyz.back == 'z'); 564 assert(xyz.length == 3); 565 assert(xyz[] == "xyz"); 566 assert(xyz.full); 567 static assert(!__traits(compiles, { const xyzw = A('x', 'y', 'z', 'w'); })); // too many elements 568 } 569 570 /// 571 pure @safe unittest { 572 static void testAsSomeString(T)() { 573 enum capacity = 15; 574 alias A = StaticArray!(immutable(T), capacity); 575 static assert(!mustAddGCRange!A); 576 auto a = A("abc"); 577 assert(a[] == "abc"); 578 579 import std.conv : to; 580 const x = "a".to!(T[]); 581 } 582 583 foreach (T; AliasSeq!(char// , wchar, dchar 584 )) { 585 testAsSomeString!T(); 586 } 587 } 588 589 /// equality 590 pure @safe unittest { 591 enum capacity = 15; 592 alias S = StaticArray!(int, capacity); 593 static assert(!mustAddGCRange!S); 594 595 assert(S([1, 2, 3].s[]) == 596 S([1, 2, 3].s[])); 597 assert(S([1, 2, 3].s[]) == 598 [1, 2, 3]); 599 } 600 601 pure @safe unittest { 602 class C { int value; } 603 alias S = StaticArray!(C, 2); 604 static assert(mustAddGCRange!S); 605 } 606 607 /// `insertBackMaybe` is nothrow @nogc. 608 pure nothrow @safe @nogc unittest { 609 alias S = StaticArray!(int, 2); 610 S s; 611 assert(s.insertBackMaybe(42)); 612 assert(s.insertBackMaybe(43)); 613 assert(!s.insertBackMaybe(0)); 614 assert(s.length == 2); 615 } 616 617 /// equality 618 @system pure nothrow @nogc unittest { 619 enum capacity = 15; 620 alias S = StaticArray!(int, capacity); 621 622 assert(S.fromValuesUnsafe([1, 2, 3].s) == 623 S.fromValuesUnsafe([1, 2, 3].s)); 624 625 const ax = [1, 2, 3].s; 626 assert(S.fromValuesUnsafe([1, 2, 3].s) == ax); 627 assert(S.fromValuesUnsafe([1, 2, 3].s) == ax[]); 628 629 const cx = [1, 2, 3].s; 630 assert(S.fromValuesUnsafe([1, 2, 3].s) == cx); 631 assert(S.fromValuesUnsafe([1, 2, 3].s) == cx[]); 632 633 immutable ix = [1, 2, 3].s; 634 assert(S.fromValuesUnsafe([1, 2, 3].s) == ix); 635 assert(S.fromValuesUnsafe([1, 2, 3].s) == ix[]); 636 } 637 638 /// assignment from `const` to `immutable` element type 639 pure @safe unittest { 640 enum capacity = 15; 641 alias String15 = StringN!(capacity); 642 static assert(!mustAddGCRange!String15); 643 644 enum n = 4; 645 const char[n] _ = ['a', 'b', 'c', 'd']; 646 auto x = String15(_[]); 647 assert(x.length == 4); 648 assert(x[] == _); 649 650 foreach_reverse (const i; 0 .. n) 651 assert(x.takeBack() == _[i]); 652 assert(x.empty); 653 } 654 655 /// borrow checking 656 @system pure unittest { 657 enum capacity = 15; 658 alias String15 = StringN!(capacity, true); 659 static assert(String15.readBorrowCountMax == 7); 660 static assert(!mustAddGCRange!String15); 661 662 auto x = String15("alpha"); 663 664 assert(x[] == "alpha"); 665 666 { 667 auto xw1 = x[]; 668 assert(x.isWriteBorrowed); 669 assert(x.isBorrowed); 670 } 671 672 auto xr1 = (cast(const)x)[]; 673 assert(x.readBorrowCount == 1); 674 675 auto xr2 = (cast(const)x)[]; 676 assert(x.readBorrowCount == 2); 677 678 auto xr3 = (cast(const)x)[]; 679 assert(x.readBorrowCount == 3); 680 681 auto xr4 = (cast(const)x)[]; 682 assert(x.readBorrowCount == 4); 683 684 auto xr5 = (cast(const)x)[]; 685 assert(x.readBorrowCount == 5); 686 687 auto xr6 = (cast(const)x)[]; 688 assert(x.readBorrowCount == 6); 689 690 auto xr7 = (cast(const)x)[]; 691 assert(x.readBorrowCount == 7); 692 693 assertThrown!AssertError((cast(const)x)[]); 694 } 695 696 version (unittest) { 697 import std.meta : AliasSeq; 698 import std.exception : assertThrown; 699 import core.exception : AssertError; 700 701 import nxt.array_help : s; 702 import nxt.container.traits : mustAddGCRange; 703 import nxt.dip_traits : hasPreviewDIP1000; 704 }