Skip to content

Commit 9967813

Browse files
authored
synchronized_value move and copy implementation (#4042)
Implemented move operations on synchronized_value which were explicitly = delete; previously, intentionally until needed. Also added tests for copy and move. I also added tests for when T is convertible to U or vice-versa and we want to copy or move one into the other when these are into synchronized_value (previous code didnt pass all the tests I just added so I fixed these cases). I also switched from using std::assignable_from to a custom weakly_assignable_from which doesnt require std::common_reference<T, U>, same previous issue than checking if operator== can be used (see weakly_equality_comparable comments for details).
1 parent 401f991 commit 9967813

File tree

2 files changed

+372
-45
lines changed

2 files changed

+372
-45
lines changed

libmamba/include/mamba/util/synchronized_value.hpp

Lines changed: 147 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ namespace mamba::util
5656
{ u != t } -> std::convertible_to<bool>;
5757
};
5858

59+
template <class T, class U>
60+
concept weakly_assignable_from = requires(T t, U&& u) { t = std::forward<U>(u); };
61+
5962

6063
/////////////////////////////
6164

@@ -110,9 +113,8 @@ namespace mamba::util
110113
the mutex types.
111114
*/
112115
template <Mutex... M>
113-
requires(sizeof...(M) > 1)
114-
[[nodiscard]]
115-
auto lock_as_readonly(M&... mutex)
116+
requires(sizeof...(M) > 1) and (SharedMutex<M> or ...)
117+
[[nodiscard]] auto lock_as_readonly(M&... mutex)
116118
{
117119
return std::make_tuple(lock_as_readonly(mutex)...);
118120
}
@@ -233,11 +235,12 @@ namespace mamba::util
233235

234236
/** Thread-safe value storage.
235237
236-
Holds an object which access is always implying a lock to an associated mutex.
237-
The only access to the object without a lock are "unsafe" functions, which are marked as
238-
such. Also provides ways to lock the access to the object in scopes. Mainly used when a
239-
value needs to be protected by a mutex and we want to make sure the code always does the
240-
right locking mechanism.
238+
Holds an object which access is always implying a lock to an associated mutex. The only
239+
access to the object without a lock are "unsafe" functions, which are named as such. Also
240+
provides ways to lock the access to the object for a whole scope.
241+
242+
Mainly used when a value needs to be protected by a mutex and we want to make sure the code
243+
always does the right locking mechanism.
241244
242245
If the mutex type satisfies `SharedMutex`, the locks will be shared if using `const`
243246
functions, enabling cheaper read-only access to the object in that context.
@@ -247,6 +250,8 @@ namespace mamba::util
247250
a bigger scope. `synchronize()` explicitly only builds such scoped-lock and provides it
248251
for scoped usage of the object.
249252
253+
This type is as move-enabled and copy-enabled as it's stored object's type.
254+
250255
Note: this is inspired by boost::thread::synchronized_value and the C++ Concurrent TS 2
251256
paper, refer to these to compare the features and correctness.
252257
*/
@@ -261,26 +266,12 @@ namespace mamba::util
261266

262267
synchronized_value() noexcept(std::is_nothrow_default_constructible_v<T>);
263268

264-
265-
// NOTE FOR FUTURE DEVS: move operations could be implemented correctly by interpreting it
266-
// as moving the stored object after locking `other` first. However,
267-
// right now we have no need for this and it might be misleading semantics (we are
268-
// not moving the `synchronized_value` per-say, we have moving only it's value).
269-
// If the need comes, implement it correctly as a lock followed by a `T` move.
270-
// Do not move the `other.m_mutex`, do not `= default;` as these solutions are
271-
// incorrect.
272-
/** Moves are forbidden so that the mutex can protect the memory region represented
273-
by the stored object.
274-
275-
*/
276-
synchronized_value(synchronized_value&& other) noexcept = delete;
277-
synchronized_value& operator=(synchronized_value&& other) noexcept = delete;
278-
279269
/// Constructs with a provided value as initializer for the stored object.
280270
template <typename V>
281271
requires(not std::same_as<T, std::decay_t<V>>)
282272
and (not std::same_as<this_type, std::decay_t<V>>)
283-
and std::assignable_from<T&, V>
273+
and (not is_type_instance_of_v<std::decay_t<V>, synchronized_value>)
274+
and weakly_assignable_from<T&, V>
284275
synchronized_value(V&& value) noexcept
285276
: m_value(std::forward<V>(value))
286277
{
@@ -302,28 +293,87 @@ namespace mamba::util
302293

303294
/** Locks the provided `synchronized_value`'s mutex and copies it's stored object value
304295
to this instance's stored object.
305-
The lock is released before the end of the call.
306296
If `SharedMutex<M> == true`, the lock is a shared-lock for the provided
307297
`synchronized_value`'s mutex.
298+
The lock is released before the end of the call.
308299
*/
309300
synchronized_value(const synchronized_value& other);
310301

311-
/** Locks both mutexes and copies/move the value of the provided `synchronized_value`'s
312-
stored object to this instance's stored object. The lock is released before the end of
313-
the call. If `SharedMutex<M> == true`, the lock is a shared-lock for the provided
302+
/** Locks the provided `synchronized_value`'s mutex and moves it's stored object value
303+
into this instance's stored object.
304+
The lock is exclusive.
305+
The lock is released before the end of the call.
306+
*/
307+
synchronized_value(synchronized_value&& other) noexcept;
308+
309+
/** Locks the provided `synchronized_value`'s mutex and copies it's stored object value
310+
to this instance's stored object.
311+
If `SharedMutex<M> == true`, the lock is a shared-lock for the provided
312+
`synchronized_value`'s mutex.
313+
The lock is released before the end of the call.
314+
*/
315+
template <std::default_initializable U, Mutex OtherMutex>
316+
requires(not std::same_as<synchronized_value<T, M>, synchronized_value<U, OtherMutex>>)
317+
and std::constructible_from<T, U>
318+
synchronized_value(const synchronized_value<U, OtherMutex>& other);
319+
320+
/** Locks the provided `synchronized_value`'s mutex and moves it's stored object value
321+
into this instance's stored object.
322+
The lock is exclusive.
323+
The lock is released before the end of the call.
324+
*/
325+
template <std::default_initializable U, Mutex OtherMutex>
326+
requires(not std::same_as<synchronized_value<T, M>, synchronized_value<U, OtherMutex>>)
327+
and std::constructible_from<T, U&&>
328+
synchronized_value(synchronized_value<U, OtherMutex>&& other) noexcept;
329+
330+
/** Locks both mutexes and copies the value of the provided `synchronized_value`'s
331+
stored object to this instance's stored object.
332+
If `SharedMutex<M> == true`, the lock is a shared-lock for the provided
333+
`synchronized_value`'s mutex.
334+
The lock is released before the end of the call.
335+
*/
336+
auto operator=(const synchronized_value& other) -> synchronized_value&;
337+
338+
/** Locks both mutexes and moves the value of the provided `synchronized_value`'s
339+
stored object to this instance's stored object.
340+
For both, the lock is exclusive.
341+
The lock is released before the end of the call.
342+
Only available if a `U` instance can be moved into `T` instance.
343+
*/
344+
auto operator=(synchronized_value&& other) noexcept -> synchronized_value&;
345+
346+
/** Locks both mutexes and copies the value of the provided `synchronized_value`'s
347+
stored object to this instance's stored object.
348+
If `SharedMutex<M> == true`, the lock is a shared-lock for the provided
314349
`synchronized_value`'s mutex.
350+
The lock is released before the end of the call.
315351
*/
316352
template <std::default_initializable U, Mutex OtherMutex>
317-
requires std::assignable_from<T&, U>
353+
requires(not std::same_as<synchronized_value<T, M>, synchronized_value<U, OtherMutex>>)
354+
and weakly_assignable_from<T&, U>
318355
auto operator=(const synchronized_value<U, OtherMutex>& other) -> synchronized_value&;
319356

357+
358+
/** Locks both mutexes and moves the value of the provided `synchronized_value`'s
359+
stored object to this instance's stored object.
360+
For both, the lock is exclusive.
361+
The lock is released before the end of the call.
362+
Only available if a `U` instance can be moved into `T` instance.
363+
*/
364+
template <std::default_initializable U, Mutex OtherMutex>
365+
requires(not std::same_as<synchronized_value<T, M>, synchronized_value<U, OtherMutex>>)
366+
and weakly_assignable_from<T&, U&&>
367+
auto operator=(synchronized_value<U, OtherMutex>&& other) noexcept -> synchronized_value&;
368+
320369
/** Locks and assign the provided value to the stored object.
321370
The lock is released before the end of the call.
322371
*/
323372
template <typename V>
324373
requires(not std::same_as<T, std::decay_t<V>>)
325374
and (not std::same_as<this_type, std::decay_t<V>>)
326-
and std::assignable_from<T&, V>
375+
and (not is_type_instance_of_v<std::decay_t<V>, synchronized_value>)
376+
and weakly_assignable_from<T&, V>
327377
auto operator=(V&& value) noexcept -> synchronized_value&
328378
{
329379
// NOTE: when moving the definition outside the class,
@@ -350,8 +400,8 @@ namespace mamba::util
350400
auto value() const -> T;
351401

352402
/** Locks and return the value of the current object.
353-
The lock is released before the end of the call.
354403
If `SharedMutex<M> == true`, the lock is a shared-lock.
404+
The lock is released before the end of the call.
355405
*/
356406
[[nodiscard]]
357407
explicit operator T() const
@@ -396,8 +446,8 @@ namespace mamba::util
396446
/** Locks the mutex and returns a `scoped_locked_ptr` which will provide
397447
non-mutable access to the stored object, while holding the lock for it's whole
398448
lifetime, which usually for this call is for the time of the expression.
399-
The lock is released only once the returned object is destroyed.
400449
If `SharedMutex<M> == true`, the lock is a shared-lock.
450+
The lock is released only once the returned object is destroyed.
401451
402452
Example:
403453
synchronized_value<std::vector<int>> values;
@@ -427,8 +477,8 @@ namespace mamba::util
427477
/** Locks the mutex and returns a `scoped_locked_ptr` which will provide
428478
non-mutable access to the stored object, while holding the lock for it's whole
429479
lifetime.
430-
The lock is released only once the returned object is destroyed.
431480
If `SharedMutex<M> == true`, the lock is a shared-lock.
481+
The lock is released only once the returned object is destroyed.
432482
433483
This is mainly used to make sure the stored object doesnt change for a whole scope.
434484
Example:
@@ -468,7 +518,8 @@ namespace mamba::util
468518
/** Locks the mutex and calls the provided invocable, passing the non-mutable stored object
469519
and the other provided values as arguments.
470520
The lock is released after the provided invocable returns but before this function
471-
returns. If `SharedMutex<M> == true`, the lock is a shared-lock.
521+
returns.
522+
If `SharedMutex<M> == true`, the lock is a shared-lock.
472523
473524
This is mainly used to safely execute an already existing function taking the stored
474525
object as parameter. Example:
@@ -519,6 +570,9 @@ namespace mamba::util
519570

520571
T m_value;
521572
mutable M m_mutex;
573+
574+
template <std::default_initializable, Mutex>
575+
friend class synchronized_value;
522576
};
523577

524578
/** Locks all the provided `synchronized_value` objects using `.synchronize` and
@@ -560,9 +614,55 @@ namespace mamba::util
560614
m_value = other.m_value;
561615
}
562616

617+
template <std::default_initializable T, Mutex M>
618+
synchronized_value<T, M>::synchronized_value(synchronized_value&& other) noexcept
619+
{
620+
auto _ = lock_as_exclusive(other.m_mutex);
621+
m_value = std::move(other.m_value);
622+
}
623+
624+
template <std::default_initializable T, Mutex M>
625+
template <std::default_initializable U, Mutex OtherMutex>
626+
requires(not std::same_as<synchronized_value<T, M>, synchronized_value<U, OtherMutex>>)
627+
and std::constructible_from<T, U>
628+
synchronized_value<T, M>::synchronized_value(const synchronized_value<U, OtherMutex>& other)
629+
{
630+
auto _ = lock_as_readonly(other.m_mutex);
631+
m_value = other.m_value;
632+
}
633+
634+
template <std::default_initializable T, Mutex M>
635+
template <std::default_initializable U, Mutex OtherMutex>
636+
requires(not std::same_as<synchronized_value<T, M>, synchronized_value<U, OtherMutex>>)
637+
and std::constructible_from<T, U&&>
638+
synchronized_value<T, M>::synchronized_value(synchronized_value<U, OtherMutex>&& other) noexcept
639+
{
640+
auto _ = lock_as_exclusive(other.m_mutex);
641+
m_value = std::move(other.m_value);
642+
}
643+
644+
template <std::default_initializable T, Mutex M>
645+
auto synchronized_value<T, M>::operator=(const synchronized_value& other) -> synchronized_value&
646+
{
647+
auto this_lock [[maybe_unused]] = lock_as_exclusive(m_mutex);
648+
auto other_lock [[maybe_unused]] = lock_as_readonly(other.m_mutex);
649+
m_value = other.m_value;
650+
return *this;
651+
}
652+
653+
template <std::default_initializable T, Mutex M>
654+
auto synchronized_value<T, M>::operator=(synchronized_value&& other) noexcept
655+
-> synchronized_value&
656+
{
657+
auto _ = lock_as_exclusive(other.m_mutex, m_mutex);
658+
m_value = std::move(other.m_value);
659+
return *this;
660+
}
661+
563662
template <std::default_initializable T, Mutex M>
564663
template <std::default_initializable U, Mutex OtherMutex>
565-
requires std::assignable_from<T&, U>
664+
requires(not std::same_as<synchronized_value<T, M>, synchronized_value<U, OtherMutex>>)
665+
and weakly_assignable_from<T&, U>
566666
auto synchronized_value<T, M>::operator=(const synchronized_value<U, OtherMutex>& other)
567667
-> synchronized_value<T, M>&
568668
{
@@ -572,6 +672,18 @@ namespace mamba::util
572672
return *this;
573673
}
574674

675+
template <std::default_initializable T, Mutex M>
676+
template <std::default_initializable U, Mutex OtherMutex>
677+
requires(not std::same_as<synchronized_value<T, M>, synchronized_value<U, OtherMutex>>)
678+
and weakly_assignable_from<T&, U&&>
679+
auto synchronized_value<T, M>::operator=(synchronized_value<U, OtherMutex>&& other) noexcept
680+
-> synchronized_value<T, M>&
681+
{
682+
auto _ = lock_as_exclusive(other.m_mutex, m_mutex);
683+
m_value = std::move(other.m_value);
684+
return *this;
685+
}
686+
575687
template <std::default_initializable T, Mutex M>
576688
auto synchronized_value<T, M>::operator=(const T& value) noexcept -> synchronized_value&
577689
{

0 commit comments

Comments
 (0)