1 module cachetools.containers.hashmap; 2 3 import std.traits; 4 import std.format; 5 import std.typecons; 6 7 import core.memory; 8 import core.bitop; 9 10 private import std.experimental.allocator; 11 private import std.experimental.allocator.mallocator : Mallocator; 12 13 private import cachetools.internal; 14 private import cachetools.hash; 15 private import cachetools.containers.lists; 16 17 class KeyNotFound : Exception { 18 this(string msg = "key not found") @safe { 19 super(msg); 20 } 21 } 22 /// 23 /// Return true if it is worth to store values inline in hash table 24 /// V footprint should be small enough 25 /// 26 package bool SmallValueFootprint(V)() { 27 import std.traits; 28 29 static if (isNumeric!V || isSomeString!V || isSomeChar!V || isPointer!V) { 30 return true; 31 } else static if (is(V == struct) && V.sizeof <= (void*).sizeof) { 32 return true; 33 } else static if (is(V == class) && __traits(classInstanceSize, V) <= (void*).sizeof) { 34 return true; 35 } else 36 return false; 37 } 38 39 private bool keyEquals(K)(const K a, const K b) { 40 static if (is(K == class)) { 41 if (a is b) { 42 return true; 43 } 44 if (a is null || b is null) { 45 return false; 46 } 47 return a.opEquals(b); 48 } else { 49 return a == b; 50 } 51 } 52 53 @safe nothrow unittest { 54 class C { 55 int c; 56 this(int v) { 57 c = v; 58 } 59 60 bool opEquals(const C other) const nothrow @safe { 61 return c == other.c; 62 } 63 } 64 65 C a = new C(0); 66 C b = new C(1); 67 C c = a; 68 C d = new C(0); 69 assert(!keyEquals(a, b)); 70 assert(keyEquals(a, c)); 71 assert(keyEquals(a, d)); 72 assert(keyEquals(1, 1)); 73 } 74 75 struct HashMap(K, V, Allocator = Mallocator) { 76 77 enum initial_buckets_num = 32; 78 enum grow_factor = 2; 79 enum inlineValues = true; //SmallValueFootprint!V()|| is(V==class); 80 81 alias StoredKeyType = StoredType!K; 82 alias StoredValueType = StoredType!V; 83 84 private { 85 alias allocator = Allocator.instance; 86 static if (hash_t.sizeof == 8) { 87 enum EMPTY_HASH = 0x00_00_00_00_00_00_00_00; 88 enum DELETED_HASH = 0x10_00_00_00_00_00_00_00; 89 enum ALLOCATED_HASH = 0x20_00_00_00_00_00_00_00; 90 enum TYPE_MASK = 0xF0_00_00_00_00_00_00_00; 91 enum HASH_MASK = 0x0F_FF_FF_FF_FF_FF_FF_FF; 92 } else static if (hash_t.sizeof == 4) { 93 enum EMPTY_HASH = 0x00_00_00_00; 94 enum DELETED_HASH = 0x10_00_00_00; 95 enum ALLOCATED_HASH = 0x20_00_00_00; 96 enum TYPE_MASK = 0xF0_00_00_00; 97 enum HASH_MASK = 0x0F_FF_FF_FF; 98 } 99 100 struct _Bucket { 101 hash_t hash; 102 StoredKeyType key; 103 static if (inlineValues) { 104 StoredValueType value; 105 } else { 106 V* value_ptr; 107 } 108 string toString() const { 109 import std.format; 110 111 static if (inlineValues) { 112 return "%s, hash: %0x,key: %s, value: %s".format([EMPTY_HASH : "free", DELETED_HASH : "deleted", 113 ALLOCATED_HASH : "allocated"][cast(long)(hash & TYPE_MASK)], hash, key, value); 114 } else { 115 return "%s, hash: %0x, key: %s, value: %s".format([EMPTY_HASH : "free", DELETED_HASH : "deleted", 116 ALLOCATED_HASH : "allocated"][cast(long)(hash & TYPE_MASK)], 117 hash, key, value_ptr !is null ? format("%s", *value_ptr) : "-"); 118 } 119 } 120 } 121 122 _Bucket[] _buckets; 123 int _buckets_num; 124 int _mask; 125 int _allocated; 126 int _deleted; 127 int _empty; 128 } 129 130 ~this() @safe { 131 if (_buckets_num > 0) { 132 static if (!inlineValues) { 133 for (int i = 0; i < _buckets_num; i++) { 134 auto t = _buckets[i].hash; 135 if (t <= DELETED_HASH) { 136 continue; 137 } 138 (() @trusted { dispose(allocator, _buckets[i].value_ptr); })(); 139 } 140 } 141 (() @trusted { 142 GC.removeRange(_buckets.ptr); 143 dispose(allocator, _buckets); 144 })(); 145 } 146 } 147 148 invariant { 149 assert(_allocated >= 0 && _deleted >= 0 && _empty >= 0); 150 assert(_allocated + _deleted + _empty == _buckets_num); 151 } 152 /// 153 /// Find any unallocated bucket starting from start_index (inclusive) 154 /// Returns index on success or hash_t.max on fail 155 /// 156 //private hash_t findEmptyIndex(const hash_t start_index) pure const @safe @nogc 157 //in 158 //{ 159 // assert(start_index < _buckets_num); 160 //} 161 //do { 162 // hash_t index = start_index; 163 // 164 // do { 165 // () @nogc {debug(cachetools) tracef("test index %d for non-ALLOCATED", index);}(); 166 // if ( _buckets[index].hash < ALLOCATED_HASH ) 167 // { 168 // return index; 169 // } 170 // index = (index + 1) & _mask; 171 // } while(index != start_index); 172 // return hash_t.max; 173 //} 174 /// 175 /// Find allocated bucket for given key and computed hash starting from start_index 176 /// Returns: index if bucket found or hash_t.max otherwise 177 /// 178 /// Inherits @nogc and from K opEquals() 179 /// 180 private hash_t findEntryIndex(const hash_t start_index, const hash_t hash, in K key) pure const @safe 181 in { 182 assert(hash < DELETED_HASH); // we look for real hash 183 assert(start_index < _buckets_num); // start position inside array 184 } 185 do { 186 hash_t index = start_index; 187 188 do { 189 immutable h = _buckets[index].hash; 190 191 debug (cachetools) 192 safe_tracef("test entry index %d (%s) for key %s", index, _buckets[index], key); 193 194 if (h == EMPTY_HASH) { 195 break; 196 } 197 198 if (h >= ALLOCATED_HASH && (h & HASH_MASK) == hash 199 && keyEquals(_buckets[index].key, key)) { 200 //() @nogc @trusted {debug(cachetools) tracef("test entry index %d for key %s - success", index, key);}(); 201 return index; 202 } 203 index = (index + 1) & _mask; 204 } 205 while (index != start_index); 206 return hash_t.max; 207 } 208 209 /// 210 /// Find place where we can insert(DELETED or EMPTY bucket) or update existent (ALLOCATED) 211 /// bucket for key k and precomputed hash starting from start_index 212 /// 213 /// 214 /// Inherits @nogc from K opEquals() 215 /// 216 private hash_t findUpdateIndex(const hash_t start_index, const hash_t computed_hash, in K key) pure const @safe 217 in { 218 assert(computed_hash < DELETED_HASH); 219 assert(start_index < _buckets_num); 220 } 221 do { 222 hash_t index = start_index; 223 224 do { 225 immutable h = _buckets[index].hash; 226 227 debug (cachetools) 228 safe_tracef("test update index %d (%s) for key %s", index, _buckets[index], key); 229 230 if (h <= DELETED_HASH) // empty or deleted 231 { 232 debug (cachetools) 233 safe_tracef("test update index %d (%s) for key %s - success", 234 index, _buckets[index], key); 235 return index; 236 } 237 assert((h & TYPE_MASK) == ALLOCATED_HASH); 238 if ((h & HASH_MASK) == computed_hash && keyEquals(_buckets[index].key, key)) { 239 debug (cachetools) 240 safe_tracef("test update index %d (%s) for key %s - success", 241 index, _buckets[index], key); 242 return index; 243 } 244 index = (index + 1) & _mask; 245 } 246 while (index != start_index); 247 return hash_t.max; 248 } 249 /// 250 /// Find unallocated entry in the buckets slice 251 /// We use this function during resize() only. 252 /// 253 private long findEmptyIndexExtended(const hash_t start_index, 254 in ref _Bucket[] buckets, int new_mask) pure const @safe @nogc 255 in { 256 assert(start_index < buckets.length); 257 } 258 do { 259 hash_t index = start_index; 260 261 do { 262 immutable t = buckets[index].hash; 263 264 debug (cachetools) 265 safe_tracef("test empty index %d (%s)", index, buckets[index]); 266 267 if (t <= DELETED_HASH) // empty or deleted 268 { 269 return index; 270 } 271 272 index = (index + 1) & new_mask; 273 } 274 while (index != start_index); 275 return hash_t.max; 276 } 277 278 private bool tooMuchDeleted() pure const @safe @nogc { 279 // 280 // _deleted > _buckets_num / 8 281 // 282 return _deleted << 3 > _buckets_num; 283 } 284 285 private bool tooHighLoad() pure const @safe @nogc { 286 // 287 // _allocated/_buckets_num > 0.8 288 // 5 * allocated > 4 * buckets_num 289 // 290 return _allocated + (_allocated << 2) > _buckets_num << 2; 291 } 292 293 private void doResize(int dest) @safe { 294 immutable _new_buckets_num = dest; 295 immutable _new_mask = dest - 1; 296 _Bucket[] _new_buckets = makeArray!(_Bucket)(allocator, _new_buckets_num); 297 298 () @trusted { 299 GC.addRange(_new_buckets.ptr, _new_buckets_num * _Bucket.sizeof); 300 }(); 301 302 // iterate over entries 303 304 debug (cachetools) 305 safe_tracef("start resizing: old loadfactor: %s", (1.0 * _allocated) / _buckets_num); 306 307 for (int i = 0; i < _buckets_num; i++) { 308 immutable hash_t h = _buckets[i].hash; 309 if (h < ALLOCATED_HASH) { // empty or deleted 310 continue; 311 } 312 313 immutable hash_t start_index = h & _new_mask; 314 immutable new_position = findEmptyIndexExtended(start_index, _new_buckets, _new_mask); 315 316 debug (cachetools) 317 safe_tracef("old hash: %0x, old pos: %d, new_pos: %d", h, i, new_position); 318 319 assert(new_position >= 0); 320 assert(_new_buckets[cast(hash_t) new_position].hash == EMPTY_HASH); 321 322 _new_buckets[cast(hash_t) new_position] = _buckets[i]; 323 } 324 (() @trusted { 325 GC.removeRange(_buckets.ptr); 326 dispose(allocator, _buckets); 327 })(); 328 _buckets = _new_buckets; 329 _buckets_num = _new_buckets_num; 330 assert(_popcnt(_buckets_num) == 1, "Buckets number must be power of 2"); 331 _mask = _buckets_num - 1; 332 _deleted = 0; 333 _empty = _buckets_num - _allocated; 334 335 debug (cachetools) 336 safe_tracef("resizing done: new loadfactor: %s", (1.0 * _allocated) / _buckets_num); 337 } 338 339 /// 340 /// Lookup methods 341 /// 342 V* opBinaryRight(string op)(in K k) @safe if (op == "in") { 343 344 if (_buckets_num == 0) 345 return null; 346 347 immutable computed_hash = hash_function(k) & HASH_MASK; 348 immutable start_index = computed_hash & _mask; 349 immutable lookup_index = findEntryIndex(start_index, computed_hash, k); 350 if (lookup_index == hash_t.max) { 351 return null; 352 } 353 static if (inlineValues) { 354 static if (is(V == StoredValueType)) { 355 return &_buckets[lookup_index].value; 356 } else { 357 V* r = () @trusted { 358 return cast(V*)&_buckets[lookup_index].value; 359 }(); 360 return r; 361 } 362 } else { 363 return _buckets[lookup_index].value_ptr; 364 } 365 } 366 367 ref V getOrAdd(T)(K k, T defaultValue) @safe { 368 V* v = k in this; 369 if (v) { 370 return *v; 371 } 372 static if (isAssignable!(V, T)) { 373 return *put(k, defaultValue); 374 } else static if (isCallable!T && isAssignable!(V, ReturnType!T)) { 375 return *put(k, defaultValue()); 376 } else { 377 static assert(0, "what?"); 378 } 379 } 380 381 alias require = getOrAdd; 382 383 /// Attention: this can't return ref as default value can be rvalue 384 V get(T)(K k, T defaultValue) @safe { 385 V* v = k in this; 386 if (v) { 387 return *v; 388 } 389 static if (isAssignable!(V, T)) { 390 return defaultValue; 391 } else static if (isCallable!T && isAssignable!(V, ReturnType!T)) { 392 return defaultValue(); 393 } else { 394 static assert(0, "You must call 'get' with default value of HashMap 'value' type, or with callable, returning HashMap 'value'"); 395 } 396 } 397 398 /// 399 /// Attention: you can't use this method in @nogc code. 400 /// Usual aa[key] method. 401 /// Throws exception if key not found 402 /// 403 ref V opIndex(in K k) @safe { 404 V* v = k in this; 405 if (v !is null) { 406 return *v; 407 } 408 throw new KeyNotFound(); 409 } 410 411 /// 412 /// Modifiers 413 /// 414 void opIndexAssign(V v, K k) @safe { 415 put(k, v); 416 } 417 /// 418 /// put pair (k,v) into hash. 419 /// it must be @safe, it inherits @nogc properties from K and V 420 /// It can resize hashtable it is overloaded or has too much deleted entries 421 /// 422 V* put(K k, V v) @safe 423 out { 424 assert(__result !is null); 425 } 426 do { 427 if (!_buckets_num) { 428 _buckets_num = _empty = initial_buckets_num; 429 assert(_popcnt(_buckets_num) == 1, "Buckets number must be power of 2"); 430 _mask = _buckets_num - 1; 431 _buckets = makeArray!(_Bucket)(allocator, _buckets_num); 432 () @trusted { 433 GC.addRange(_buckets.ptr, _buckets_num * _Bucket.sizeof); 434 }(); 435 } 436 437 debug (cachetools) 438 safe_tracef("put k: %s, v: %s", k, v); 439 440 if (tooHighLoad) { 441 doResize(grow_factor * _buckets_num); 442 } 443 444 V* r; //result 445 immutable computed_hash = hash_function(k) & HASH_MASK; 446 immutable start_index = computed_hash & _mask; 447 immutable placement_index = findUpdateIndex(start_index, computed_hash, k); 448 assert(placement_index >= 0); 449 _Bucket* bucket = &_buckets[placement_index]; 450 immutable h = bucket.hash; 451 452 debug (cachetools) 453 safe_tracef("start_index: %d, placement_index: %d", start_index, placement_index); 454 455 if (h < ALLOCATED_HASH) { 456 _allocated++; 457 _empty--; 458 } 459 static if (inlineValues) { 460 debug (cachetools) 461 safe_tracef("place inline buckets[%d] '%s'='%s'", placement_index, k, v); 462 bucket.value = v; 463 static if (is(V == StoredValueType)) { 464 r = &bucket.value; 465 } else { 466 () @trusted { r = cast(V*)&bucket.value; }(); 467 } 468 } else { 469 debug (cachetools) 470 safe_tracef("place with allocation buckets[%d] '%s'='%s'", placement_index, k, v); 471 if ((bucket.hash & TYPE_MASK) == ALLOCATED_HASH) { 472 // we just replace what we already allocated 473 r = (bucket.value_ptr); 474 *r = v; 475 } else { 476 r = bucket.value_ptr = make!(V)(allocator); 477 *r = v; 478 } 479 } 480 bucket.hash = computed_hash | ALLOCATED_HASH; 481 bucket.key = k; 482 return r; 483 } 484 485 bool remove(K k) @safe { 486 487 if (tooMuchDeleted) { 488 // do not shrink, just compact table 489 doResize(_buckets_num); 490 } 491 492 debug (cachetools) 493 safe_tracef("remove k: %s", k); 494 495 immutable computed_hash = hash_function(k) & HASH_MASK; 496 immutable start_index = computed_hash & _mask; 497 immutable lookup_index = findEntryIndex(start_index, computed_hash, k); 498 if (lookup_index == hash_t.max) { 499 // nothing to remove 500 return false; 501 } 502 503 assert((_buckets[lookup_index].hash & TYPE_MASK) == ALLOCATED_HASH, 504 "tried to remove non allocated bucket"); 505 506 static if (inlineValues) { 507 // what we have to do with removed values XXX? 508 } else { 509 // what we have to do with removed values XXX? 510 // free space 511 (() @trusted { dispose(allocator, _buckets[lookup_index].value_ptr); })(); 512 _buckets[lookup_index].value_ptr = null; 513 } 514 515 _allocated--; 516 immutable next_index = (lookup_index + 1) & _mask; 517 // if next bucket is EMPTY, then we can convert all DELETED buckets down staring from current to EMPTY buckets 518 if (_buckets[next_index].hash == EMPTY_HASH) { 519 _empty++; 520 _buckets[lookup_index].hash = EMPTY_HASH; 521 auto free_index = (lookup_index - 1) & _mask; 522 while (free_index != lookup_index) { 523 if (_buckets[free_index].hash != DELETED_HASH) { 524 break; 525 } 526 _buckets[free_index].hash = EMPTY_HASH; 527 _deleted--; 528 _empty++; 529 free_index = (free_index - 1) & _mask; 530 } 531 assert(free_index != lookup_index, "table full of deleted buckets?"); 532 } else { 533 _buckets[lookup_index].hash = DELETED_HASH; 534 _deleted++; 535 } 536 return true; 537 } 538 539 void clear() @safe { 540 static if (!inlineValues) { 541 // dispose every element 542 foreach (k; byKey) { 543 remove(k); 544 } 545 } 546 (() @trusted { 547 GC.removeRange(_buckets.ptr); 548 dispose(allocator, _buckets); 549 })(); 550 _buckets = makeArray!(_Bucket)(allocator, _buckets_num); 551 _allocated = _deleted = 0; 552 _empty = _buckets_num; 553 } 554 555 auto length() const pure nothrow @nogc @safe { 556 return _allocated; 557 } 558 559 auto size() const pure nothrow @nogc @safe { 560 return _buckets_num; 561 } 562 563 auto byKey() pure @safe @nogc { 564 struct _kvRange { 565 int _pos; 566 ulong _buckets_num; 567 _Bucket[] _buckets; 568 this(_Bucket[] _b) { 569 _buckets = _b; 570 _buckets_num = _b.length; 571 _pos = 0; 572 while (_pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH) { 573 _pos++; 574 } 575 } 576 577 bool empty() const pure nothrow @safe @nogc { 578 return _pos == _buckets_num; 579 } 580 581 K front() { 582 return _buckets[_pos].key; 583 } 584 585 void popFront() pure nothrow @safe @nogc { 586 _pos++; 587 while (_pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH) { 588 _pos++; 589 } 590 } 591 } 592 593 return _kvRange(_buckets); 594 } 595 596 auto byValue() pure @safe { 597 struct _kvRange { 598 int _pos; 599 ulong _buckets_num; 600 _Bucket[] _buckets; 601 this(_Bucket[] _b) { 602 _buckets = _b; 603 _buckets_num = _b.length; 604 _pos = 0; 605 while (_pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH) { 606 _pos++; 607 } 608 } 609 610 bool empty() const pure nothrow @safe @nogc { 611 return _pos == _buckets_num; 612 } 613 614 V front() { 615 static if (inlineValues) { 616 return _buckets[_pos].value; 617 } else { 618 return *(_buckets[_pos].value_ptr); 619 } 620 } 621 622 void popFront() pure nothrow @safe @nogc { 623 _pos++; 624 while (_pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH) { 625 _pos++; 626 } 627 } 628 } 629 630 return _kvRange(_buckets); 631 } 632 633 auto byPair() pure @safe { 634 import std.typecons; 635 636 struct _kvRange { 637 int _pos; 638 ulong _buckets_num; 639 _Bucket[] _buckets; 640 this(_Bucket[] _b) { 641 _buckets = _b; 642 _buckets_num = _b.length; 643 _pos = 0; 644 while (_pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH) { 645 _pos++; 646 } 647 } 648 649 bool empty() const pure nothrow @safe @nogc { 650 return _pos == _buckets_num; 651 } 652 653 auto front() @safe { 654 static if (inlineValues) { 655 return Tuple!(K, "key", V, "value")(_buckets[_pos].key, _buckets[_pos].value); 656 } else { 657 return Tuple!(K, "key", V, "value")(_buckets[_pos].key, 658 *(_buckets[_pos].value_ptr)); 659 } 660 } 661 662 void popFront() pure nothrow @safe @nogc { 663 _pos++; 664 while (_pos < _buckets_num && _buckets[_pos].hash < ALLOCATED_HASH) { 665 _pos++; 666 } 667 } 668 } 669 670 return _kvRange(_buckets); 671 } 672 } 673 674 /// Tests 675 676 /// test immutable struct and class as Key type 677 @safe unittest { 678 import std.experimental.logger; 679 680 globalLogLevel = LogLevel.info; 681 () @nogc nothrow{ 682 struct S { 683 int s; 684 } 685 686 HashMap!(immutable S, int) hs1; 687 immutable ss = S(1); 688 hs1[ss] = 1; 689 assert(ss in hs1 && *(ss in hs1) == 1); 690 HashMap!(int, immutable S) hs2; 691 hs2[1] = ss; 692 assert(1 in hs2 && *(1 in hs2) == ss); 693 assert(!(2 in hs2)); 694 }(); 695 696 // class 697 class C { 698 int v; 699 this(int _v) pure inout { 700 v = _v; 701 } 702 703 bool opEquals(const C o) pure const @safe @nogc nothrow { 704 return v == o.v; 705 } 706 707 override hash_t toHash() const @safe @nogc { 708 return hash_function(v); 709 } 710 } 711 712 HashMap!(immutable C, int) hc1; 713 immutable cc = new immutable C(1); 714 hc1[cc] = 1; 715 assert(hc1[cc] == 1); 716 HashMap!(int, immutable C) hc2; 717 hc2[1] = cc; 718 assert(hc2[1] is cc); 719 } 720 721 @safe unittest { 722 // test class as key 723 import std.experimental.logger; 724 725 globalLogLevel = LogLevel.info; 726 class A { 727 int v; 728 729 bool opEquals(const A o) pure const @safe @nogc nothrow { 730 return v == o.v; 731 } 732 733 override hash_t toHash() const @safe @nogc { 734 return hash_function(v); 735 } 736 737 this(int v) { 738 this.v = v; 739 } 740 741 override string toString() const { 742 import std.format; 743 744 return "A(%d)".format(v); 745 } 746 } 747 748 globalLogLevel = LogLevel.info; 749 auto x = new A(1); 750 auto y = new A(2); 751 HashMap!(A, string) dict; 752 dict.put(x, "x"); 753 dict.put(y, "y"); 754 } 755 756 @safe unittest { 757 import std.experimental.logger; 758 759 globalLogLevel = LogLevel.info; 760 () @nogc nothrow{ 761 HashMap!(int, int) int2int; 762 foreach (i; 0 .. 15) { 763 int2int.put(i, i); 764 } 765 assert(int2int.length() == 15); 766 foreach (i; 0 .. 15) { 767 assert(i in int2int); 768 } 769 foreach (i; 0 .. 15) { 770 int2int.remove(i); 771 } 772 assert(int2int.length() == 0); 773 }(); 774 () @nogc nothrow{ 775 struct LargeStruct { 776 ulong a; 777 ulong b; 778 } 779 780 HashMap!(int, LargeStruct) int2ls; 781 foreach (i; 1 .. 5) { 782 int2ls.put(i, LargeStruct(i, i)); 783 } 784 int2ls.put(33, LargeStruct(33, 33)); // <- follow key 1, move key 2 on pos 3 785 assert(1 in int2ls, "1 not in hash"); 786 assert(2 in int2ls, "2 not in hash"); 787 assert(3 in int2ls, "3 not in hash"); 788 assert(4 in int2ls, "4 not in hash"); 789 assert(33 in int2ls, "33 not in hash"); 790 int2ls.remove(33); 791 int2ls.put(2, LargeStruct(2, 2)); // <- must replace key 2 on pos 3 792 assert(2 in int2ls, "2 not in hash"); 793 }(); 794 } 795 796 @safe unittest { 797 import std.experimental.logger; 798 799 globalLogLevel = LogLevel.info; 800 () @nogc nothrow{ 801 assert(SmallValueFootprint!int()); 802 assert(SmallValueFootprint!double()); 803 struct SmallStruct { 804 ulong a; 805 } 806 //assert(SmallValueFootprint!SmallStruct); 807 struct LargeStruct { 808 ulong a; 809 ulong b; 810 } 811 812 assert(!SmallValueFootprint!LargeStruct); 813 class SmallClass { 814 ulong a; 815 } 816 //assert(!SmallValueFootprint!SmallClass); 817 818 HashMap!(int, string) int2string; 819 auto u = int2string.put(1, "one"); 820 { 821 auto v = 1 in int2string; 822 assert(v !is null); 823 assert(*v == "one"); 824 } 825 assert(2 !in int2string); 826 u = int2string.put(32 + 1, "33"); 827 assert(33 in int2string); 828 assert(int2string.remove(33)); 829 assert(!int2string.remove(33)); 830 831 HashMap!(int, LargeStruct) int2LagreStruct; 832 int2LagreStruct.put(1, LargeStruct(1, 2)); 833 { 834 auto v = 1 in int2LagreStruct; 835 assert(v !is null); 836 assert(*v == LargeStruct(1, 2)); 837 } 838 }(); 839 840 globalLogLevel = LogLevel.info; 841 } 842 843 @safe unittest { 844 import std.experimental.logger; 845 import std.experimental.allocator.gc_allocator; 846 847 globalLogLevel = LogLevel.info; 848 static int i; 849 () @safe @nogc nothrow{ 850 struct LargeStruct { 851 ulong a; 852 ulong b; 853 ~this() @safe @nogc { 854 i++; 855 } 856 } 857 858 HashMap!(int, LargeStruct) int2LagreStruct; 859 int2LagreStruct.put(1, LargeStruct(1, 2)); 860 }(); 861 globalLogLevel = LogLevel.info; 862 } 863 864 @safe unittest /* not nothrow as opIndex may throw */ { 865 import std.typecons; 866 867 alias K = Tuple!(int, int); 868 alias V = int; 869 HashMap!(K, V) h; 870 K k0 = K(0, 1); 871 V v0 = 1; 872 h.put(k0, v0); 873 int* v = k0 in h; 874 assert(v); 875 assert(*v == 1); 876 h[k0] = v0; 877 assert(h[k0] == v0); 878 } 879 880 @safe nothrow unittest { 881 class c { 882 int a; 883 this(int a) { 884 this.a = a; 885 } 886 887 override hash_t toHash() const pure @nogc @safe { 888 return hash_function(a); 889 } 890 891 bool opEquals(const c other) pure const nothrow @safe @nogc { 892 return this is other || this.a == other.a; 893 } 894 } 895 896 alias K = c; 897 alias V = int; 898 K k0 = new c(0); 899 V v0 = 1; 900 () @nogc nothrow{ 901 HashMap!(K, V) h; 902 h.put(k0, v0); 903 int* v = k0 in h; 904 assert(v); 905 assert(*v == 1); 906 h[k0] = 2; 907 v = k0 in h; 908 assert(*v == 2); 909 }(); 910 } 911 912 /// Test if we can work with non-@nogc opEquals for class-key. 913 /// opEquals anyway must be non-@system. 914 @safe nothrow unittest { 915 class c { 916 int a; 917 this(int a) { 918 this.a = a; 919 } 920 921 override hash_t toHash() const pure @safe { 922 int[] _ = [1, 2, 3]; // this cause GC 923 return hash_function(a); 924 } 925 926 bool opEquals(const c other) const pure nothrow @safe { 927 auto _ = [1, 2, 3]; // this cause GC 928 return this is other || this.a == other.a; 929 } 930 } 931 932 alias K = c; 933 alias V = int; 934 HashMap!(K, V) h; 935 K k0 = new c(0); 936 V v0 = 1; 937 h.put(k0, v0); 938 int* v = k0 in h; 939 assert(v); 940 assert(*v == 1); 941 942 } 943 /// 944 /// test byKey, byValue, byPair 945 /// 946 @safe nothrow unittest { 947 import std.algorithm; 948 import std.array; 949 import std.stdio; 950 951 HashMap!(int, string) m; 952 m[1] = "one"; 953 m[2] = "two"; 954 m[10] = "ten"; 955 assert(equal(m.byKey.array.sort, [1, 2, 10])); 956 assert(equal(m.byValue.array.sort, ["one", "ten", "two"])); 957 assert(equal(m.byPair.map!"tuple(a.key, a.value)".array.sort, [tuple(1, 958 "one"), tuple(2, "two"), tuple(10, "ten")])); 959 m.remove(1); 960 m.remove(10); 961 assert(equal(m.byPair.map!"tuple(a.key, a.value)".array.sort, [tuple(2, "two")])); 962 m.remove(2); 963 assert(m.byPair.map!"tuple(a.key, a.value)".array.sort.length() == 0); 964 m.remove(2); 965 assert(m.byPair.map!"tuple(a.key, a.value)".array.sort.length() == 0); 966 } 967 /// 968 /// compare equivalence to AA 969 /// 970 /* not @safe because of AA */ 971 unittest { 972 import std.random; 973 import std.array; 974 import std.algorithm; 975 import std.stdio; 976 import std.experimental.logger; 977 978 enum iterations = 400_000; 979 980 globalLogLevel = LogLevel.info; 981 982 HashMap!(int, int) hashMap; 983 int[int] AA; 984 985 auto rnd = Random(unpredictableSeed); 986 987 foreach (i; 0 .. iterations) { 988 int k = uniform(0, iterations, rnd); 989 hashMap.put(k, i); 990 AA[k] = i; 991 } 992 assert(equal(AA.keys().sort(), hashMap.byKey().array.sort())); 993 assert(equal(AA.values().sort(), hashMap.byValue().array.sort())); 994 assert(AA.length == hashMap.length); 995 } 996 /// 997 /// check remove 998 /// 999 @safe unittest { 1000 // test removal while iterating 1001 import std.random; 1002 import std.array; 1003 import std.algorithm; 1004 import std.stdio; 1005 import std.experimental.logger; 1006 1007 enum iterations = 400_000; 1008 1009 globalLogLevel = LogLevel.info; 1010 1011 HashMap!(int, int) hashMap; 1012 1013 auto rnd = Random(unpredictableSeed); 1014 1015 foreach (i; 0 .. iterations) { 1016 int k = uniform(0, iterations, rnd); 1017 hashMap[k] = i; 1018 } 1019 foreach (k; hashMap.byKey) { 1020 assert(hashMap.remove(k)); 1021 } 1022 assert(hashMap.length == 0); 1023 } 1024 /// 1025 /// test clear 1026 /// 1027 @safe @nogc nothrow unittest { 1028 // test clear 1029 1030 HashMap!(int, int) hashMap; 1031 1032 foreach (i; 0 .. 100) { 1033 hashMap[i] = i; 1034 } 1035 hashMap.clear(); 1036 assert(hashMap.length == 0); 1037 hashMap[1] = 1; 1038 assert(1 in hashMap && hashMap.length == 1); 1039 } 1040 /// 1041 /// test getOrAdd with value 1042 /// 1043 @safe @nogc nothrow unittest { 1044 // test of nogc getOrAdd 1045 1046 HashMap!(int, int) hashMap; 1047 1048 foreach (i; 0 .. 100) { 1049 hashMap[i] = i; 1050 } 1051 auto v = hashMap.getOrAdd(-1, -1); 1052 assert(-1 in hashMap && v == -1); 1053 } 1054 1055 /// 1056 /// test getOrAdd with callable 1057 /// 1058 @safe @nogc nothrow unittest { 1059 // test of nogc getOrAdd with lazy default value 1060 1061 HashMap!(int, int) hashMap; 1062 1063 foreach (i; 0 .. 100) { 1064 hashMap[i] = i; 1065 } 1066 int v = hashMap.getOrAdd(-1, () => -1); 1067 assert(-1 in hashMap && v == -1); 1068 assert(hashMap.get(-1, 0) == -1); // key -1 is in hash, return value 1069 assert(hashMap.get(-2, 0) == 0); // key -2 not in map, return default value 1070 assert(hashMap.get(-3, () => 0) == 0); // ditto 1071 } 1072 1073 /// 1074 /// test getOrAdd with complex data 1075 /// 1076 @safe unittest { 1077 import std.socket; 1078 1079 HashMap!(string, Socket) socketPool; 1080 Socket s0 = socketPool.getOrAdd("http://example.com", 1081 () => new Socket(AddressFamily.INET, SocketType.STREAM)); 1082 assert(s0 !is null); 1083 assert(s0.addressFamily == AddressFamily.INET); 1084 Socket s1 = socketPool.getOrAdd("http://example.com", 1085 () => new Socket(AddressFamily.INET, SocketType.STREAM)); 1086 assert(s1 !is null); 1087 assert(s1 is s0); 1088 } 1089 /// 1090 /// test with real class (socket) 1091 /// 1092 @safe unittest { 1093 import std.socket; 1094 1095 class Connection { 1096 Socket s; 1097 bool opEquals(const Connection other) const pure @safe { 1098 return s is other.s; 1099 } 1100 1101 override hash_t toHash() const @safe { 1102 return hash_function(s.handle); 1103 } 1104 } 1105 1106 HashMap!(Connection, string) socketPool; 1107 } 1108 1109 @safe unittest { 1110 // test of non-@nogc getOrAdd with lazy default value 1111 import std.conv; 1112 import std.exception; 1113 1114 class C { 1115 string v; 1116 this(int _v) @safe { 1117 v = to!string(_v); 1118 } 1119 } 1120 1121 HashMap!(int, C) hashMap; 1122 1123 foreach (i; 0 .. 100) { 1124 hashMap[i] = new C(i); 1125 } 1126 C v = hashMap.getOrAdd(-1, () => new C(-1)); 1127 assert(-1 in hashMap && v.v == "-1"); 1128 assert(hashMap[-1].v == "-1"); 1129 hashMap[-1].v ~= "1"; 1130 assert(hashMap[-1].v == "-11"); 1131 assertThrown!KeyNotFound(hashMap[-2]); 1132 // check lazyness 1133 bool called; 1134 v = hashMap.getOrAdd(-1, delegate C() { called = true; return new C(0); }); 1135 assert(!called); 1136 v = hashMap.getOrAdd(-2, delegate C() { called = true; return new C(0); }); 1137 assert(called); 1138 } 1139 /// 1140 /// test if we can handle some exotic value type 1141 /// 1142 @safe @nogc nothrow unittest { 1143 // test of nogc getOrAdd with lazy default value 1144 // corner case when V is callable 1145 1146 alias F = int function() @safe @nogc nothrow; 1147 1148 F one = function() { return 1; }; 1149 F two = function() { return 2; }; 1150 F three = function() { return 3; }; 1151 F four = function() { return 4; }; 1152 HashMap!(int, F) hashMap; 1153 hashMap.put(1, one); 1154 hashMap.put(2, two); 1155 auto p = 1 in hashMap; 1156 assert(p); 1157 assert((*p)() == 1); 1158 p = 2 in hashMap; 1159 assert(p); 1160 assert((*p)() == 2); 1161 auto f3 = hashMap.getOrAdd(3, () => function int() { return 3; }); // used as default() 1162 assert(f3() == 3); 1163 auto f4 = hashMap.getOrAdd(4, four); 1164 assert(f4() == 4); 1165 } 1166 1167 // test get() 1168 @safe @nogc nothrow unittest { 1169 HashMap!(int, int) hashMap; 1170 int i = hashMap.get(1, 55); 1171 assert(i == 55); 1172 i = hashMap.get(1, () => 66); 1173 assert(i == 66); 1174 hashMap[1] = 1; 1175 i = hashMap.get(1, () => 66); 1176 assert(i == 1); 1177 }