1 /** 2 Internal hash map implementation. 3 4 Copyright: © 2013 RejectedSoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module vutil.hashmap; 9 10 import vutil.memory; 11 12 import std.conv : emplace; 13 import std.traits; 14 15 16 struct DefaultHashMapTraits(Key) { 17 enum clearValue = Key.init; 18 static bool equals(in Key a, in Key b) 19 { 20 static if (is(Key == class)) return a is b; 21 else return a == b; 22 } 23 static size_t hashOf(in ref Key k) 24 { 25 static if (is(Key == class) && &Key.toHash == &Object.toHash) 26 return cast(size_t)cast(void*)k; 27 else static if (__traits(compiles, Key.init.toHash())) 28 return k.toHash(); 29 else static if (__traits(compiles, Key.init.toHashShared())) 30 return k.toHashShared(); 31 else { 32 // evil casts to be able to get the most basic operations of 33 // HashMap nothrow and @nogc 34 static size_t hashWrapper(in ref Key k) { 35 static typeinfo = typeid(Key); 36 return typeinfo.getHash(&k); 37 } 38 static @nogc nothrow size_t properlyTypedWrapper(in ref Key k) { return 0; } 39 return (cast(typeof(&properlyTypedWrapper))&hashWrapper)(k); 40 } 41 } 42 } 43 44 struct HashMap(TKey, TValue, Traits = DefaultHashMapTraits!TKey) 45 { 46 import vmeta.traits : isOpApplyDg; 47 48 alias Key = TKey; 49 alias Value = TValue; 50 51 struct TableEntry { 52 UnConst!Key key = Traits.clearValue; 53 Value value; 54 55 this(Key key, Value value) { this.key = cast(UnConst!Key)key; this.value = value; } 56 } 57 private { 58 TableEntry[] m_table; // NOTE: capacity is always POT 59 size_t m_length; 60 Allocator m_allocator; 61 bool m_resizing; 62 } 63 64 this(Allocator allocator) 65 { 66 m_allocator = allocator; 67 } 68 69 ~this() 70 { 71 if (m_table) freeArray(m_allocator, m_table); 72 } 73 74 @disable this(this); 75 76 @property size_t length() const { return m_length; } 77 78 void remove(Key key) 79 { 80 auto idx = findIndex(key); 81 assert (idx != size_t.max, "Removing non-existent element."); 82 auto i = idx; 83 while (true) { 84 m_table[i].key = Traits.clearValue; 85 m_table[i].value = Value.init; 86 87 size_t j = i, r; 88 do { 89 if (++i >= m_table.length) i -= m_table.length; 90 if (Traits.equals(m_table[i].key, Traits.clearValue)) { 91 m_length--; 92 return; 93 } 94 r = Traits.hashOf(m_table[i].key) & (m_table.length-1); 95 } while ((j<r && r<=i) || (i<j && j<r) || (r<=i && i<j)); 96 m_table[j] = m_table[i]; 97 } 98 } 99 100 Value get(Key key, lazy Value default_value = Value.init) 101 { 102 auto idx = findIndex(key); 103 if (idx == size_t.max) return default_value; 104 return m_table[idx].value; 105 } 106 107 /// Workaround #12647 108 package Value getNothrow(Key key, Value default_value = Value.init) 109 { 110 auto idx = findIndex(key); 111 if (idx == size_t.max) return default_value; 112 return m_table[idx].value; 113 } 114 115 static if (!is(typeof({ Value v; const(Value) vc; v = vc; }))) { 116 const(Value) get(Key key, lazy const(Value) default_value = Value.init) 117 { 118 auto idx = findIndex(key); 119 if (idx == size_t.max) return default_value; 120 return m_table[idx].value; 121 } 122 } 123 124 void clear() 125 { 126 foreach (i; 0 .. m_table.length) 127 if (!Traits.equals(m_table[i].key, Traits.clearValue)) { 128 m_table[i].key = Traits.clearValue; 129 m_table[i].value = Value.init; 130 } 131 m_length = 0; 132 } 133 134 void opIndexAssign(Value value, Key key) 135 { 136 assert(!Traits.equals(key, Traits.clearValue), "Inserting clear value into hash map."); 137 grow(1); 138 auto i = findInsertIndex(key); 139 if (!Traits.equals(m_table[i].key, key)) m_length++; 140 m_table[i] = TableEntry(key, value); 141 } 142 143 ref inout(Value) opIndex(Key key) 144 inout { 145 auto idx = findIndex(key); 146 assert (idx != size_t.max, "Accessing non-existent key."); 147 return m_table[idx].value; 148 } 149 150 inout(Value)* opBinaryRight(string op)(Key key) 151 inout if (op == "in") { 152 auto idx = findIndex(key); 153 if (idx == size_t.max) return null; 154 return &m_table[idx].value; 155 } 156 157 int opApply(DG)(scope DG del) if (isOpApplyDg!(DG, Key, Value)) 158 { 159 import std.traits : arity; 160 foreach (i; 0 .. m_table.length) 161 if (!Traits.equals(m_table[i].key, Traits.clearValue)) { 162 static assert(arity!del >= 1 && arity!del <= 2, 163 "isOpApplyDg should have prevented this"); 164 static if (arity!del == 1) { 165 if (int ret = del(m_table[i].value)) 166 return ret; 167 } else 168 if (int ret = del(m_table[i].key, m_table[i].value)) 169 return ret; 170 } 171 return 0; 172 } 173 174 private size_t findIndex(Key key) 175 const { 176 if (m_length == 0) return size_t.max; 177 size_t start = Traits.hashOf(key) & (m_table.length-1); 178 auto i = start; 179 while (!Traits.equals(m_table[i].key, key)) { 180 if (Traits.equals(m_table[i].key, Traits.clearValue)) return size_t.max; 181 if (++i >= m_table.length) i -= m_table.length; 182 if (i == start) return size_t.max; 183 } 184 return i; 185 } 186 187 private size_t findInsertIndex(Key key) 188 const { 189 auto hash = Traits.hashOf(key); 190 size_t target = hash & (m_table.length-1); 191 auto i = target; 192 while (!Traits.equals(m_table[i].key, Traits.clearValue) && !Traits.equals(m_table[i].key, key)) { 193 if (++i >= m_table.length) i -= m_table.length; 194 assert (i != target, "No free bucket found, HashMap full!?"); 195 } 196 return i; 197 } 198 199 private void grow(size_t amount) 200 { 201 auto newsize = m_length + amount; 202 if (newsize < (m_table.length*2)/3) return; 203 auto newcap = m_table.length ? m_table.length : 16; 204 while (newsize >= (newcap*2)/3) newcap *= 2; 205 resize(newcap); 206 } 207 208 private void resize(size_t new_size) 209 @trusted { 210 assert(!m_resizing); 211 m_resizing = true; 212 scope(exit) m_resizing = false; 213 214 if (!m_allocator) m_allocator = defaultAllocator(); 215 216 uint pot = 0; 217 while (new_size > 1) pot++, new_size /= 2; 218 new_size = 1 << pot; 219 220 auto oldtable = m_table; 221 222 // allocate the new array, automatically initializes with empty entries (Traits.clearValue) 223 m_table = allocArray!TableEntry(m_allocator, new_size); 224 225 // perform a move operation of all non-empty elements from the old array to the new one 226 foreach (ref el; oldtable) 227 if (!Traits.equals(el.key, Traits.clearValue)) { 228 auto idx = findInsertIndex(el.key); 229 (cast(ubyte[])(&m_table[idx])[0 .. 1])[] = (cast(ubyte[])(&el)[0 .. 1])[]; 230 } 231 232 // all elements have been moved to the new array, so free the old one without calling destructors 233 if (oldtable) freeArray(m_allocator, oldtable, false); 234 } 235 } 236 237 unittest { 238 import std.conv; 239 240 HashMap!(string, string) map; 241 242 foreach (i; 0 .. 100) { 243 map[to!string(i)] = to!string(i) ~ "+"; 244 assert(map.length == i+1); 245 } 246 247 foreach (i; 0 .. 100) { 248 auto str = to!string(i); 249 auto pe = str in map; 250 assert(pe !is null && *pe == str ~ "+"); 251 assert(map[str] == str ~ "+"); 252 } 253 254 foreach (i; 0 .. 50) { 255 map.remove(to!string(i)); 256 assert(map.length == 100-i-1); 257 } 258 259 foreach (i; 50 .. 100) { 260 auto str = to!string(i); 261 auto pe = str in map; 262 assert(pe !is null && *pe == str ~ "+"); 263 assert(map[str] == str ~ "+"); 264 } 265 } 266 267 // test for nothrow/@nogc compliance 268 static if (__VERSION__ >= 2066) 269 nothrow unittest { 270 HashMap!(int, int) map1; 271 HashMap!(string, string) map2; 272 map1[1] = 2; 273 map2["1"] = "2"; 274 275 @nogc nothrow void performNoGCOps() 276 { 277 foreach (int v; map1) {} 278 foreach (int k, int v; map1) {} 279 assert(1 in map1); 280 assert(map1.length == 1); 281 assert(map1[1] == 2); 282 assert(map1.getNothrow(1, -1) == 2); 283 284 foreach (string v; map2) {} 285 foreach (string k, string v; map2) {} 286 assert("1" in map2); 287 assert(map2.length == 1); 288 assert(map2["1"] == "2"); 289 assert(map2.getNothrow("1", "") == "2"); 290 } 291 292 performNoGCOps(); 293 } 294 295 unittest { // test for proper use of constructor/post-blit/destructor 296 static struct Test { 297 static size_t constructedCounter = 0; 298 bool constructed = false; 299 this(int) { constructed = true; constructedCounter++; } 300 this(this) { if (constructed) constructedCounter++; } 301 ~this() { if (constructed) constructedCounter--; } 302 } 303 304 assert(Test.constructedCounter == 0); 305 306 { // sanity check 307 Test t; 308 assert(Test.constructedCounter == 0); 309 t = Test(1); 310 assert(Test.constructedCounter == 1); 311 auto u = t; 312 assert(Test.constructedCounter == 2); 313 t = Test.init; 314 assert(Test.constructedCounter == 1); 315 } 316 assert(Test.constructedCounter == 0); 317 318 { // basic insertion and hash map resizing 319 HashMap!(int, Test) map; 320 foreach (i; 1 .. 67) { 321 map[i] = Test(1); 322 assert(Test.constructedCounter == i); 323 } 324 } 325 326 assert(Test.constructedCounter == 0); 327 328 { // test clear() and overwriting existing entries 329 HashMap!(int, Test) map; 330 foreach (i; 1 .. 67) { 331 map[i] = Test(1); 332 assert(Test.constructedCounter == i); 333 } 334 map.clear(); 335 foreach (i; 1 .. 67) { 336 map[i] = Test(1); 337 assert(Test.constructedCounter == i); 338 } 339 foreach (i; 1 .. 67) { 340 map[i] = Test(1); 341 assert(Test.constructedCounter == 66); 342 } 343 } 344 345 assert(Test.constructedCounter == 0); 346 347 { // test removing entries and adding entries after remove 348 HashMap!(int, Test) map; 349 foreach (i; 1 .. 67) { 350 map[i] = Test(1); 351 assert(Test.constructedCounter == i); 352 } 353 foreach (i; 1 .. 33) { 354 map.remove(i); 355 assert(Test.constructedCounter == 66 - i); 356 } 357 foreach (i; 67 .. 130) { 358 map[i] = Test(1); 359 assert(Test.constructedCounter == i - 32); 360 } 361 } 362 363 assert(Test.constructedCounter == 0); 364 } 365 366 private template UnConst(T) { 367 static if (is(T U == const(U))) { 368 alias UnConst = U; 369 } else static if (is(T V == immutable(V))) { 370 alias UnConst = V; 371 } else alias UnConst = T; 372 } 373 374 static if (__VERSION__ < 2066) private static bool nogc() { return false; }