Skip to content

Commit 31d461e

Browse files
Add WeakValueHashMap class
1 parent 1c3ac22 commit 31d461e

File tree

3 files changed

+478
-0
lines changed

3 files changed

+478
-0
lines changed

Common/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ set(INTERFACE
5050
interface/Cast.hpp
5151
interface/CompilerDefinitions.h
5252
interface/CallbackWrapper.hpp
53+
interface/WeakValueHashMap.hpp
5354
)
5455

5556
set(SOURCE
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* Copyright 2025 Diligent Graphics LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* In no event and under no legal theory, whether in tort (including negligence),
17+
* contract, or otherwise, unless required by applicable law (such as deliberate
18+
* and grossly negligent acts) or agreed to in writing, shall any Contributor be
19+
* liable for any damages, including any direct, indirect, special, incidental,
20+
* or consequential damages of any character arising as a result of this License or
21+
* out of the use or inability to use the software (including but not limited to damages
22+
* for loss of goodwill, work stoppage, computer failure or malfunction, or any and
23+
* all other commercial damages or losses), even if such Contributor has been advised
24+
* of the possibility of such damages.
25+
*/
26+
27+
#pragma once
28+
29+
/// \file
30+
/// Defines WeakValueHashMap class
31+
32+
#include <unordered_map>
33+
#include <memory>
34+
#include <mutex>
35+
36+
#include "../../Platforms/Basic/interface/DebugUtilities.hpp"
37+
38+
namespace Diligent
39+
{
40+
41+
/// WeakValueHashMap is a thread-safe hash map that holds weak pointers to its values.
42+
43+
/// \tparam KeyType - Type of the keys in the map. Must be hashable and comparable.
44+
/// \tparam ValueType - Type of the values in the map.
45+
///
46+
/// When a value is requested via GetOrInsert(), a strong pointer (shared_ptr) to the value is returned
47+
/// wrapped in a ValueHandle object. The ValueHandle object is responsible for removing the entry from the map
48+
/// when it is destroyed. If there are no more strong pointers to the value, the entry is removed from the map.
49+
///
50+
/// If a value is requested via Get(), a strong pointer to the value is returned wrapped in a ValueHandle object
51+
/// if the entry exists and the value has not expired. Otherwise, an empty ValueHandle is returned.
52+
///
53+
/// The map is thread-safe and can be accessed from multiple threads simultaneously.
54+
///
55+
/// Example usage:
56+
///
57+
/// WeakValueHashMap<int, std::string> Map;
58+
/// auto Handle = Map.GetOrInsert(1, "Value");
59+
/// std::cout << *Handle << std::endl; // Outputs "Value"
60+
///
61+
template <typename KeyType, typename ValueType>
62+
class WeakValueHashMap
63+
{
64+
private:
65+
using MapType = std::unordered_map<KeyType, std::weak_ptr<ValueType>>;
66+
using MapIterType = typename MapType::iterator;
67+
class Impl;
68+
69+
public:
70+
/// ValueHandle is a handle to a value in the WeakValueHashMap.
71+
72+
/// It holds a strong pointer to the value and removes the entry from the map when it is destroyed.
73+
class ValueHandle
74+
{
75+
public:
76+
ValueHandle() = default;
77+
78+
~ValueHandle()
79+
{
80+
Release();
81+
}
82+
83+
// Disable copy semantics
84+
ValueHandle(const ValueHandle&) = delete;
85+
ValueHandle& operator=(const ValueHandle&) = delete;
86+
87+
ValueHandle(ValueHandle&& rhs) noexcept :
88+
m_pMap{std::move(rhs.m_pMap)},
89+
m_pValue{std::move(rhs.m_pValue)},
90+
m_Key{std::move(rhs.m_Key)}
91+
{
92+
rhs.m_pMap.reset();
93+
rhs.m_pValue.reset();
94+
}
95+
96+
ValueHandle& operator=(ValueHandle&& rhs) noexcept
97+
{
98+
if (this != &rhs)
99+
{
100+
Release();
101+
102+
m_pMap = std::move(rhs.m_pMap);
103+
m_pValue = std::move(rhs.m_pValue);
104+
m_Key = std::move(rhs.m_Key);
105+
106+
rhs.m_pMap.reset();
107+
rhs.m_pValue.reset();
108+
}
109+
return *this;
110+
}
111+
112+
ValueType* Get() { return m_pValue.get(); }
113+
const ValueType* Get() const { return m_pValue.get(); }
114+
115+
ValueType& operator*() { return *m_pValue; }
116+
const ValueType& operator*() const { return *m_pValue; }
117+
ValueType* operator->() { return m_pValue.get(); }
118+
const ValueType* operator->() const { return m_pValue.get(); }
119+
120+
explicit operator bool() const { return m_pMap && m_pValue; }
121+
122+
private:
123+
friend class WeakValueHashMap<KeyType, ValueType>;
124+
125+
void Release()
126+
{
127+
if (m_pMap)
128+
{
129+
// Release the shared pointer first so that Remove() can check if
130+
// any other shared pointers exist
131+
m_pValue.reset();
132+
133+
m_pMap->Remove(m_Key);
134+
m_pMap.reset();
135+
}
136+
}
137+
138+
explicit ValueHandle(Impl& Map,
139+
std::shared_ptr<ValueType> pValue,
140+
KeyType Key) :
141+
m_pMap{Map.shared_from_this()},
142+
m_pValue{std::move(pValue)},
143+
m_Key{std::move(Key)}
144+
{}
145+
146+
private:
147+
std::shared_ptr<Impl> m_pMap;
148+
std::shared_ptr<ValueType> m_pValue;
149+
KeyType m_Key;
150+
};
151+
152+
ValueHandle Get(const KeyType& Key) const
153+
{
154+
return m_pImpl->Get(Key);
155+
}
156+
157+
template <typename... ArgsType>
158+
ValueHandle GetOrInsert(const KeyType& Key, ArgsType&&... Args) const
159+
{
160+
return m_pImpl->GetOrInsert(Key, std::forward<ArgsType>(Args)...);
161+
}
162+
163+
private:
164+
class Impl : public std::enable_shared_from_this<Impl>
165+
{
166+
public:
167+
ValueHandle Get(const KeyType& Key)
168+
{
169+
std::lock_guard<std::mutex> Lock{m_Mtx};
170+
171+
auto it = m_Map.find(Key);
172+
if (it != m_Map.end())
173+
{
174+
auto pValue = it->second.lock();
175+
if (pValue)
176+
{
177+
return ValueHandle{*this, std::move(pValue), Key};
178+
}
179+
else
180+
{
181+
// Since ValueHandle::Release() resets the shared_ptr before calling Remove(),
182+
// we may find expired weak pointers in the map. Remove them.
183+
m_Map.erase(it);
184+
}
185+
}
186+
187+
return ValueHandle{};
188+
}
189+
190+
template <typename... ArgsType>
191+
ValueHandle GetOrInsert(const KeyType& Key, ArgsType&&... Args)
192+
{
193+
{
194+
std::lock_guard<std::mutex> Lock{m_Mtx};
195+
196+
auto it = m_Map.find(Key);
197+
if (it != m_Map.end())
198+
{
199+
if (auto pValue = it->second.lock())
200+
{
201+
return ValueHandle{*this, std::move(pValue), Key};
202+
}
203+
else
204+
{
205+
// Since ValueHandle::Release() resets the shared_ptr before calling Remove(),
206+
// we may find expired weak pointers in the map. Remove them.
207+
m_Map.erase(it);
208+
}
209+
}
210+
}
211+
212+
// Create the new value outside of the lock
213+
auto pNewValue = std::make_shared<ValueType>(std::forward<ArgsType>(Args)...);
214+
215+
std::lock_guard<std::mutex> Lock{m_Mtx};
216+
217+
// Check again in case another thread inserted the value while we were creating it
218+
if (auto it = m_Map.find(Key); it != m_Map.end())
219+
{
220+
if (auto pValue = it->second.lock())
221+
{
222+
// Discard the newly created value and use the one created by the other thread
223+
return ValueHandle{*this, std::move(pValue), Key};
224+
}
225+
226+
// Replace the expired weak pointer with the newly created value
227+
it->second = pNewValue;
228+
return ValueHandle{*this, std::move(pNewValue), Key};
229+
}
230+
231+
// Insert the new value
232+
m_Map.emplace(Key, pNewValue);
233+
return ValueHandle{*this, std::move(pNewValue), Key};
234+
}
235+
236+
void Remove(const KeyType& Key)
237+
{
238+
std::lock_guard<std::mutex> Lock{m_Mtx};
239+
240+
auto Iter = m_Map.find(Key);
241+
if (Iter == m_Map.end())
242+
{
243+
// If two ValueHandles are destroyed simultaneously from different threads,
244+
// both may try to remove the same entry. In this case, just return.
245+
return;
246+
}
247+
248+
// If the weak pointer is not expired, it means that another ValueHandle instance exists,
249+
// which will remove the entry when it is destroyed.
250+
if (Iter->second.expired())
251+
{
252+
m_Map.erase(Iter);
253+
}
254+
}
255+
256+
~Impl()
257+
{
258+
VERIFY(m_Map.empty(), "Map is not empty upon destruction. This should never happen because all entries should have been "
259+
"removed by destructors of ValueHandle objects, and the map can't be destroyed while any ValueHandle "
260+
"instances are alive.");
261+
}
262+
263+
private:
264+
std::mutex m_Mtx;
265+
MapType m_Map;
266+
};
267+
std::shared_ptr<Impl> m_pImpl = std::make_shared<Impl>();
268+
};
269+
270+
} // namespace Diligent

0 commit comments

Comments
 (0)