Mutexed 0.0.1
A header-only mutex-protected value wrapper for C++20.
Loading...
Searching...
No Matches
mutexed.hpp
1#pragma once
2
3#include <condition_variable>
4#include <shared_mutex>
5#include <mutex>
6#include <type_traits>
7#include <utility>
8#include <functional>
9
10namespace llh::mutexed {
11
13template<typename F, typename A>
14concept invokable_with = requires (F&& f, A a) {
15 std::invoke(std::forward<F>(f), a);
16};
17
19template<typename M>
20concept shared_lockable = requires(M& m) {
21 m.lock_shared();
22 m.unlock_shared();
23 { m.try_lock_shared() } -> std::same_as<bool>;
24};
25
26
28struct has_cv {};
29
31struct no_cv {};
32
34template<typename Tag, typename... Ts>
35constexpr bool contains_tag() {
36 return ((std::is_same_v<std::decay_t<Ts>, Tag>) || ...);
37}
38
40template<typename Tag, typename... Ts>
41concept does_not_contain_tag = !contains_tag<Tag, Ts...>();
42
43
44namespace details {
45
47struct mutexed_tag {};
48
49template<typename T>
50struct decay_through_ref_wrap {
51 using type = std::decay_t<T>;
52};
53
54template<typename T>
55struct decay_through_ref_wrap<std::reference_wrapper<T>> {
56 using type = std::decay_t<T>;
57};
58
60template<typename T>
61using decay_through_ref_wrap_t = typename decay_through_ref_wrap<T>::type;
62
63
64/* Functor that locks all provided Mutexed for the duration of a provided function.
65 It was implemented this way instead of a being directly a free function because it needs
66 access to the private members of Mutexed, and writing `friend details::all_locker`
67 is easier than copy-pasting the whole signature of the function.
68 */
69struct all_locker {
70 // This specialization simply forwards the calls to the `lock()` functions
71 // to the inner mutex of the held `Mutexed&`.
72 template<typename M>
73 struct lockable_proxy {
74 M& m;
75
76 void lock() { m.mtx_.lock(); }
77 void unlock() { m.mtx_.unlock(); }
78 bool try_lock() { return m.mtx_.try_lock(); }
79
80 auto& inner_val_ref() { return m.val_; }
81 };
82
83 /* This specialization calls the `lock_shared()` functions on the inner mutex
84 of the held `Mutexed&` whenever their not-shared counterpart is called.
85
86 It applies when Mutexed::mutex_type is shared_lockable and when the template
87 parameter M is const.
88 */
89 template<typename M>
91 struct lockable_proxy<M const> {
92 M& m;
93
94 explicit lockable_proxy(M const& c_m) : m(const_cast<M&>(c_m)) {}
95
96 void lock() { m.mtx_.lock_shared(); }
97 void unlock() { m.mtx_.unlock_shared(); }
98 bool try_lock() { return m.mtx_.try_lock_shared(); }
99
100 auto const& inner_val_ref() { return m.val_; }
101 };
102
103 template<typename M> lockable_proxy(std::reference_wrapper<M>) -> lockable_proxy<M>;
104 template<typename M> lockable_proxy(M&) -> lockable_proxy<M>;
105
106 template<typename F, typename... M>
107 requires std::conjunction_v<std::is_base_of<mutexed_tag, decay_through_ref_wrap_t<M>>...>
108 decltype(auto) operator()(F&& f, M&&... mtxs) const {
109 /* If we just invoke f, only lock() or try_lock() will be called on the mutexes.
110 Instead, we create a lockable_proxy of the Mutexed s that will dispatch the
111 calls made by std::lock() to their shared counterparts when it is suitable.
112
113 Because std::lock() takes references, we need lockable_proxy variables
114 somewhere. This implementation puts them as arguments of a lambda that is
115 instantly called.
116 */
117 return [](auto&& f, auto&&... mp) {
118 std::scoped_lock<std::decay_t<decltype(mp)>...> lock(mp...);
119 return std::invoke(std::forward<F>(f), mp.inner_val_ref()...);
120 }(std::forward<F>(f), lockable_proxy{std::forward<M>(mtxs)}...);
121 }
122};
123
124
126template<typename M, typename H = no_cv>
127struct mutexed_base{};
128
129template<typename M>
130struct mutexed_base<M, has_cv> {
131 std::condition_variable_any mutable cv_;
132};
133
136template<>
137struct mutexed_base<std::mutex, has_cv> {
138 std::condition_variable mutable cv_;
139};
140
141} // end namespace details
142
147
148
161template<typename T, typename M = std::shared_mutex, typename H = no_cv>
162class Mutexed : private details::mutexed_tag, private details::mutexed_base<M, H> {
163private:
164 M mutable mtx_;
165 T val_;
166
167 friend details::all_locker;
168
171 template<typename DoesNotHaveCV>
172 struct defer_notify {
174 template<typename Ignored>
175 explicit defer_notify(Ignored&&) {}
176 };
177
179 template<typename HasCV>
180 requires requires(HasCV& m) { m.cv_.notify_all(); }
181 struct defer_notify<HasCV> {
182 decltype(std::declval<HasCV>().cv_)& cv_;
183
184 explicit defer_notify(HasCV const& m) : cv_(m.cv_) {}
185
186 ~defer_notify() {
187 cv_.notify_all();
188 }
189 };
190
191 using notifier = defer_notify<Mutexed>;
192
193public:
195 using value_type = T;
197 using mutex_type = M;
198
202 using possibly_shared_lock = std::conditional_t<
204 std::shared_lock<M>,
205 std::unique_lock<M>
206 >;
207
208 Mutexed(Mutexed&&) = delete;
209 Mutexed(Mutexed const&) = delete;
210
213 template<typename... ValueArgs>
214 requires does_not_contain_tag<mutex_args_t, ValueArgs...> &&
215 std::is_constructible_v<T, ValueArgs&&...>
216 explicit Mutexed(ValueArgs&&... args) : mtx_(), val_(std::forward<ValueArgs>(args)...) {}
217
220 template<typename ValArg, typename MutexArg>
221 explicit Mutexed(ValArg&& v_arg, MutexArg&& m_arg) :
222 mtx_(std::forward<MutexArg>(m_arg)),
223 val_(std::forward<ValArg>(v_arg))
224 {}
225
228 template<typename... MutexArgs>
229 requires does_not_contain_tag<value_args_t, MutexArgs...>
230 explicit Mutexed(mutex_args_t, MutexArgs&&... m_args) :
231 mtx_(std::forward<MutexArgs>(m_args)...),
232 val_()
233 {}
234
266 template<typename F>
267 requires
269 invokable_with<F, T> && std::is_copy_constructible_v<T>
270 decltype(auto) with_locked(F&& f) const {
271 possibly_shared_lock lock(mtx_);
272 return std::invoke(std::forward<F>(f), val_);
273 }
274
275
295 template<typename F>
296 requires invokable_with<F, T&>
297 decltype(auto) with_locked(F&& f) {
298 notifier dn(*this);
299 std::lock_guard lock(mtx_);
300 return std::invoke(f, val_);
301 }
302
305 template<typename = void>
306 requires std::is_copy_constructible_v<T>
307 T get_copy() const {
308 possibly_shared_lock lock(mtx_);
309 return val_;
310 }
311
312
363 template<typename Predicate>
364 requires std::is_same_v<H, has_cv> && invokable_with<Predicate, T const&>
365 void wait(Predicate&& p) const {
366 possibly_shared_lock lock(mtx_);
367 this->cv_.wait(lock, [p = std::forward<Predicate>(p), this](){ return std::invoke(p, val_); });
368 }
369
375 template<class Rep, class Period, typename Predicate>
376 requires std::is_same_v<H, has_cv> && invokable_with<Predicate, T const&>
377 bool wait_for(std::chrono::duration<Rep, Period> const& rel_time, Predicate&& p) const {
378 possibly_shared_lock lock(mtx_);
379 return this->cv_.wait_for(lock, rel_time, [p = std::forward<Predicate>(p), this](){ return std::invoke(p, val_); });
380 }
381
387 template<class Clock, class Duration, typename Predicate>
388 requires std::is_same_v<H, has_cv> && invokable_with<Predicate, T const&>
389 bool wait_until(std::chrono::time_point<Clock, Duration> const& timeout_time, Predicate&& p) const {
390 possibly_shared_lock lock(mtx_);
391 return this->cv_.wait_until(lock, timeout_time, [p = std::forward<Predicate>(p), this](){ return std::invoke(p, val_); });
392 }
393
395 // end group Waiting
396
397
415 decltype(auto) locked() {
416 class Lock {
417 private:
418 Mutexed& m;
419
420 void lock() { m.mtx_.lock(); }
421 void unlock() { m.mtx_.unlock(); }
422
423 public:
424 explicit Lock(Mutexed& mtx) : m(mtx) { lock(); }
425
426 ~Lock() {
427 unlock();
428 if constexpr (std::is_same_v<H, has_cv>) {
429 m.cv_.notify_all();
430 }
431 }
432
433 // Copies would mess with unlocks and notifications
434 Lock(Lock const&) = delete;
435 // Moves could have use-cases but would require tracking an otherwise useless state
436 Lock(Lock &&) = delete;
437 };
438 return std::tuple<Lock, T&>(*this, val_);
439 }
441 std::tuple<possibly_shared_lock, T const&> locked() const {
442 return locked_const();
443 }
463 std::tuple<possibly_shared_lock, T const&> locked_const() const {
464 return std::tuple<possibly_shared_lock, T const&>{mtx_, val_};
465 }
466};
467
468
470inline constexpr mutex_args_t mutex_args{};
471inline constexpr value_args_t value_args{};
472
474inline constexpr details::all_locker with_all_locked{};
475
476} // end namespace llh::mutexed
The Mutexed class is a value-wrapper that protects its value with a mutex that will be referred to in...
Definition mutexed.hpp:162
std::conditional_t< shared_lockable< M >, std::shared_lock< M >, std::unique_lock< M > > possibly_shared_lock
A std::shared_lock<M> if Mutexed::mutex_type is shared_lockable , a std::unique_lock<M> otherwise.
Definition mutexed.hpp:206
Mutexed(ValueArgs &&... args)
In-place-constructs the wrapped value with the provided arguments and default-initializes the mutex.
Definition mutexed.hpp:216
T value_type
The type of the wrapped value.
Definition mutexed.hpp:195
T get_copy() const
Gets a copy of the wrapped value while locking the inner mutex.
Definition mutexed.hpp:307
Mutexed(ValArg &&v_arg, MutexArg &&m_arg)
Forwards the first argument to the constructor of the value and the second argument to the constructo...
Definition mutexed.hpp:221
Mutexed(mutex_args_t, MutexArgs &&... m_args)
In-place-constructs the mutex with the provided arguments and default-initializes the wrapped value.
Definition mutexed.hpp:230
M mutex_type
The type of the inner mutex
Definition mutexed.hpp:197
std::tuple< possibly_shared_lock, T const & > locked_const() const
Provides const access to the wrapped value through a tuple of a possibly_shared_lock and a const refe...
Definition mutexed.hpp:463
decltype(auto) with_locked(F &&f) const
Calls f with a const& or a copy of the wrapped value while locking the inner mutex.
Definition mutexed.hpp:270
decltype(auto) with_locked(F &&f)
Calls f with a reference on the wrapped value while locking the inner mutex.
Definition mutexed.hpp:297
std::tuple< possibly_shared_lock, T const & > locked() const
Same as locked_const().
Definition mutexed.hpp:441
decltype(auto) locked()
Provides access to the wrapped value through a tuple of an unspecified lock guard and a reference to ...
Definition mutexed.hpp:415
Checks if Tag is not in Ts.
Definition mutexed.hpp:41
Checks the invokability of F with a value of type A.
Definition mutexed.hpp:14
Checks if M has the member functions of a shared mutex.
Definition mutexed.hpp:20
bool wait_until(std::chrono::time_point< Clock, Duration > const &timeout_time, Predicate &&p) const
Waits until this is notified and the provided predicate returns true or until the specified time poin...
Definition mutexed.hpp:389
bool wait_for(std::chrono::duration< Rep, Period > const &rel_time, Predicate &&p) const
Waits until this is notified and the provided predicate returns true or until the specified duration ...
Definition mutexed.hpp:377
void wait(Predicate &&p) const
Waits until this is notified and the provided predicate returns true.
Definition mutexed.hpp:365
A tag type to use as last template argument of Mutexed to enable the waiting API but making it handle...
Definition mutexed.hpp:28
Disambiguation tag type used to provide arguments for the in-place construction of the inner mutex.
Definition mutexed.hpp:144
The default last template argument of Mutexed, disabling the waiting API but not pay its costs.
Definition mutexed.hpp:31
Disambiguation tag type used to provide arguments for the in-place construction of the mutexed value.
Definition mutexed.hpp:146