Skip to content

Commit 3dd50bb

Browse files
Add WeakValueHashMap class
1 parent 1c3ac22 commit 3dd50bb

File tree

3 files changed

+464
-0
lines changed

3 files changed

+464
-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: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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+
class Impl;
66+
67+
public:
68+
/// ValueHandle is a handle to a value in the WeakValueHashMap.
69+
70+
/// It holds a strong pointer to the value and removes the entry from the map when it is destroyed.
71+
class ValueHandle
72+
{
73+
public:
74+
ValueHandle() = default;
75+
76+
~ValueHandle()
77+
{
78+
Release();
79+
}
80+
81+
// Disable copy semantics
82+
ValueHandle(const ValueHandle&) = delete;
83+
ValueHandle& operator=(const ValueHandle&) = delete;
84+
85+
ValueHandle(ValueHandle&& rhs) noexcept :
86+
m_pMap{std::move(rhs.m_pMap)},
87+
m_pValue{std::move(rhs.m_pValue)},
88+
m_Key{std::move(rhs.m_Key)}
89+
{
90+
rhs.m_pMap.reset();
91+
rhs.m_pValue.reset();
92+
}
93+
94+
ValueHandle& operator=(ValueHandle&& rhs) noexcept
95+
{
96+
if (this != &rhs)
97+
{
98+
Release();
99+
100+
m_pMap = std::move(rhs.m_pMap);
101+
m_pValue = std::move(rhs.m_pValue);
102+
m_Key = std::move(rhs.m_Key);
103+
104+
rhs.m_pMap.reset();
105+
rhs.m_pValue.reset();
106+
}
107+
return *this;
108+
}
109+
110+
ValueType* Get() { return m_pValue.get(); }
111+
const ValueType* Get() const { return m_pValue.get(); }
112+
113+
ValueType& operator*() { return *m_pValue; }
114+
const ValueType& operator*() const { return *m_pValue; }
115+
ValueType* operator->() { return m_pValue.get(); }
116+
const ValueType* operator->() const { return m_pValue.get(); }
117+
118+
explicit operator bool() const { return m_pMap && m_pValue; }
119+
120+
private:
121+
friend class WeakValueHashMap<KeyType, ValueType>;
122+
123+
void Release()
124+
{
125+
if (m_pMap)
126+
{
127+
// Release the shared pointer first so that Remove() can check if
128+
// any other shared pointers exist
129+
m_pValue.reset();
130+
131+
m_pMap->Remove(m_Key);
132+
m_pMap.reset();
133+
}
134+
}
135+
136+
explicit ValueHandle(Impl& Map,
137+
std::shared_ptr<ValueType> pValue,
138+
KeyType Key) :
139+
m_pMap{Map.shared_from_this()},
140+
m_pValue{std::move(pValue)},
141+
m_Key{std::move(Key)}
142+
{}
143+
144+
private:
145+
std::shared_ptr<Impl> m_pMap;
146+
std::shared_ptr<ValueType> m_pValue;
147+
KeyType m_Key;
148+
};
149+
150+
ValueHandle Get(const KeyType& Key) const
151+
{
152+
return m_pImpl->Get(Key);
153+
}
154+
155+
template <typename... ArgsType>
156+
ValueHandle GetOrInsert(const KeyType& Key, ArgsType&&... Args) const
157+
{
158+
return m_pImpl->GetOrInsert(Key, std::forward<ArgsType>(Args)...);
159+
}
160+
161+
private:
162+
class Impl : public std::enable_shared_from_this<Impl>
163+
{
164+
public:
165+
ValueHandle Get(const KeyType& Key)
166+
{
167+
std::lock_guard<std::mutex> Lock{m_Mtx};
168+
169+
auto it = m_Map.find(Key);
170+
if (it != m_Map.end())
171+
{
172+
if (auto pValue = it->second.lock())
173+
{
174+
return ValueHandle{*this, std::move(pValue), Key};
175+
}
176+
else
177+
{
178+
// Since ValueHandle::Release() resets the shared_ptr before calling Remove(),
179+
// we may find expired weak pointers in the map. Remove them.
180+
m_Map.erase(it);
181+
}
182+
}
183+
184+
return ValueHandle{};
185+
}
186+
187+
template <typename... ArgsType>
188+
ValueHandle GetOrInsert(const KeyType& Key, ArgsType&&... Args)
189+
{
190+
if (ValueHandle Handle = Get(Key))
191+
{
192+
return Handle;
193+
}
194+
195+
// Create the new value outside of the lock
196+
auto pNewValue = std::make_shared<ValueType>(std::forward<ArgsType>(Args)...);
197+
198+
std::lock_guard<std::mutex> Lock{m_Mtx};
199+
200+
// Check again in case another thread inserted the value while we were creating it
201+
if (auto it = m_Map.find(Key); it != m_Map.end())
202+
{
203+
if (auto pValue = it->second.lock())
204+
{
205+
// Discard the newly created value and use the one created by the other thread
206+
return ValueHandle{*this, std::move(pValue), Key};
207+
}
208+
else
209+
{
210+
// Replace the expired weak pointer with the newly created value
211+
it->second = pNewValue;
212+
return ValueHandle{*this, std::move(pNewValue), Key};
213+
}
214+
}
215+
216+
// Insert the new value
217+
bool Inserted = m_Map.emplace(Key, pNewValue).second;
218+
VERIFY(Inserted, "Failed to insert new value into the map. This should never happen as we have already checked that the key does not exist.");
219+
return ValueHandle{*this, std::move(pNewValue), Key};
220+
}
221+
222+
void Remove(const KeyType& Key)
223+
{
224+
std::lock_guard<std::mutex> Lock{m_Mtx};
225+
226+
auto Iter = m_Map.find(Key);
227+
if (Iter == m_Map.end())
228+
{
229+
// If two ValueHandles are destroyed simultaneously from different threads,
230+
// both may try to remove the same entry. In this case, just return.
231+
return;
232+
}
233+
234+
// If the weak pointer is not expired, it means that another ValueHandle instance exists,
235+
// which will remove the entry when it is destroyed.
236+
if (Iter->second.expired())
237+
{
238+
m_Map.erase(Iter);
239+
}
240+
}
241+
242+
~Impl()
243+
{
244+
VERIFY(m_Map.empty(), "Map is not empty upon destruction. This should never happen because all entries should have been "
245+
"removed by destructors of ValueHandle objects, and the map can't be destroyed while any ValueHandle "
246+
"instances are alive.");
247+
}
248+
249+
private:
250+
std::mutex m_Mtx;
251+
std::unordered_map<KeyType, std::weak_ptr<ValueType>> m_Map;
252+
};
253+
std::shared_ptr<Impl> m_pImpl = std::make_shared<Impl>();
254+
};
255+
256+
} // namespace Diligent

0 commit comments

Comments
 (0)