You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
277 lines
10 KiB
277 lines
10 KiB
//! A value-wrapping mutex / rw-lock
|
|
#pragma once
|
|
#include <mutex>
|
|
#include <shared_mutex>
|
|
#include <memory>
|
|
#include <utility>
|
|
#include <concepts>
|
|
|
|
namespace fx::mutex {
|
|
template<typename F, typename T>
|
|
concept Delegate = std::is_invocable_v<std::add_rvalue_reference_t<F>, T>;
|
|
|
|
template<typename T>
|
|
struct storage_traits {
|
|
using value_type = std::decay_t<T>;
|
|
typedef std::unique_ptr<value_type> storage_type;
|
|
|
|
template<typename... Args>
|
|
requires(std::is_constructible_v<value_type, Args...>)
|
|
constexpr static storage_type make_storage(Args&&... args) noexcept(std::is_nothrow_constructible_v<value_type, Args...>)
|
|
{
|
|
return std::make_unique<value_type>(std::forward<Args>(args)...);
|
|
}
|
|
|
|
constexpr static storage_type make_storage() noexcept(std::is_nothrow_default_constructible_v<value_type>) requires(std::is_default_constructible_v<value_type>)
|
|
{ return std::make_unique<value_type>(); }
|
|
|
|
};
|
|
|
|
namespace details [[gnu::visibility("hidden")]] {
|
|
template<typename T>
|
|
concept has_storage = requires{ typename storage_traits<T>::storage_type; };
|
|
|
|
template<typename T>
|
|
concept has_valid_storage = has_storage<T> and requires(storage_traits<T>::storage_type const& value) {
|
|
{ value.get() } noexcept -> std::convertible_to< typename std::add_pointer_t<typename storage_traits<T>::value_type> >;
|
|
};
|
|
|
|
template<typename T, typename... Args>
|
|
concept can_construct = has_storage<T> and requires(Args&&... args) {
|
|
{ storage_traits<T>::template make_storage<Args...>(std::forward<Args>(args)...) }
|
|
-> std::convertible_to< typename storage_traits<T>::storage_type >;
|
|
};
|
|
template<typename T>
|
|
concept can_default_construct = has_storage<T> and requires{
|
|
{ storage_traits<T>::make_storage() }
|
|
-> std::convertible_to< typename storage_traits<T>::storage_type >;
|
|
};
|
|
}
|
|
|
|
template<typename M, typename T>
|
|
class Mutex {
|
|
static_assert(details::has_valid_storage<T>, "Invalid storage_traits<T> spec for stored type");
|
|
|
|
using value_type = storage_traits<T>::value_type;
|
|
using storage_type = storage_traits<T>::storage_type;
|
|
|
|
|
|
storage_type m_value;
|
|
public:
|
|
using mutex_type = std::decay_t<M>;
|
|
|
|
constexpr explicit Mutex(std::convertible_to<storage_type> auto&& up) noexcept
|
|
: m_value(std::move(up)) {}
|
|
|
|
constexpr explicit Mutex() noexcept requires(details::can_default_construct<T>)
|
|
: m_value(storage_traits<T>::make_storage()) {}
|
|
constexpr Mutex(T&& value) noexcept(std::is_nothrow_move_constructible_v<value_type>)
|
|
: m_value(storage_traits<T>::make_storage(std::forward<T>(value)))
|
|
//, m_mutex(std::forward<M>(mutex))
|
|
{}
|
|
|
|
Mutex(const Mutex&) = delete;
|
|
Mutex(Mutex&&) = delete;
|
|
Mutex& operator=(Mutex const&) = delete;
|
|
Mutex& operator=(Mutex &&) = delete;
|
|
|
|
[[gnu::returns_nonnull, gnu::artificial]]
|
|
inline T* get_ptr_unsafe() const noexcept { return get_raw_ptr(); }
|
|
[[gnu::returns_nonnull, gnu::artificial]]
|
|
inline T const* get_ptr() const noexcept { return static_cast<T const*>(get_raw_ptr()); }
|
|
|
|
inline virtual mutex_type& get_mutex() const noexcept { return m_mutex; }
|
|
protected:
|
|
[[gnu::returns_nonnull]]
|
|
inline virtual value_type* get_raw_ptr() const noexcept {
|
|
value_type* p = m_value.get();
|
|
if(!p) __builtin_unreachable();
|
|
return p;
|
|
}
|
|
|
|
inline T& value() & noexcept { return *get_raw_ptr(); }
|
|
inline T const& value() const& noexcept { return *get_raw_ptr(); }
|
|
inline T&& value() && noexcept { return std::move(*get_raw_ptr()); }
|
|
inline T const&& value() const&& noexcept { return std::move(*get_raw_ptr()); }
|
|
|
|
|
|
mutable mutex_type m_mutex{};
|
|
};
|
|
|
|
/// For callback passed to `with_lock()` & like functions that copies & moves the value of type `T`.
|
|
template<typename T>
|
|
constexpr auto copy_value(T const& v) noexcept(std::is_nothrow_copy_constructible_v<T>)
|
|
{
|
|
return v;
|
|
}
|
|
|
|
template<typename T>
|
|
constexpr auto move_value(T& v) noexcept(std::is_nothrow_move_constructible_v<T>)
|
|
{
|
|
return std::move(v);
|
|
}
|
|
|
|
/// For callback passed to `with_lock()` & like functions that does nothing and returns `void`.
|
|
[[gnu::artificial, gnu::always_inline]]
|
|
constexpr void nothing(const auto&) noexcept {}
|
|
|
|
[[gnu::artificial, gnu::always_inline]]
|
|
constexpr void nothing(auto&) noexcept {}
|
|
|
|
/// Creates a callback for `with_lock()` & like functions that returns an already given value of type `T`.
|
|
///
|
|
/// If `T` is not trivially-copy-constructible, the value returned from the callback will be move-constructed from the captured `value`.
|
|
/// `value` is moved into the callback via a forward to a decayed `U`.
|
|
/// If the wish is to pass the rvalue-reference down the chain, having `with_lock()` return the moved value directly, use `return_ref<T>()` instead.
|
|
template<typename T, std::convertible_to<T> U = T>
|
|
constexpr auto return_value(U&& value) noexcept(std::is_nothrow_convertible_v<U, T>) {
|
|
if constexpr(std::is_trivially_copy_constructible_v<T>) {
|
|
return [value = std::forward<U>(value)] (auto const&) noexcept(std::is_nothrow_copy_constructible_v<T>) -> T {
|
|
return value;
|
|
};
|
|
} else
|
|
return [value = std::forward<U>(value)] (auto const&) mutable noexcept(std::is_nothrow_move_constructible_v<T>) -> T {
|
|
return std::move(value);
|
|
};
|
|
}
|
|
|
|
/// Creates a callback for `with_lock()` & like functions that passes a value *through* the callback and constructs it when the callback returns.
|
|
/// So an rvalue-reference given to `.with_lock(return_ref<T>(std::move(obj)))` will construct the object `T` from a forwarding-reference of the passed rvalue-reference `std::move(obj)` only at the *end* of `.with_lock()` when it returns before releasing the lock.)
|
|
///
|
|
/// This is in contrast to `return_value<T>()`, which captures the passed forwarding-reference *by value* in the callback itself and moves out of it when the callback returns, so the above example would construct `T` from an intermediary `U` which is the decayed-type of the expression `std::move(obj)`. T
|
|
/// herefore a potential 2nd construction is avoided with this method, however the lifetime of `value` must be taken into account when using this callback, as `value` is captured only by forwarding its reference.
|
|
///
|
|
/// Unlike `return_value<T>()`, The lifetime of the returned callback is bound to `U`.
|
|
template<typename T, std::convertible_to<T> U = T>
|
|
constexpr auto return_ref(U&& value) noexcept {
|
|
return [&] (auto const&) noexcept(std::is_nothrow_convertible_v<U, T>) -> T { return std::forward<U>(value); };
|
|
}
|
|
}
|
|
|
|
namespace fx {
|
|
template<typename T>
|
|
struct Mutex : public mutex::Mutex<std::mutex, T> {
|
|
using mutex::Mutex<std::mutex, T>::Mutex;
|
|
|
|
template<mutex::Delegate<T&> F>
|
|
inline decltype(auto) with_lock(F&& f) & noexcept(std::is_nothrow_invocable_v<F, T&>)
|
|
{
|
|
std::unique_lock ul{ this->get_mutex() };
|
|
return std::forward<F>(f)(this->value());
|
|
}
|
|
|
|
template<mutex::Delegate<T const&> F>
|
|
inline decltype(auto) with_lock(F&& f) const& noexcept(std::is_nothrow_invocable_v<F, T const&>)
|
|
{
|
|
std::unique_lock ul{ this->get_mutex() };
|
|
return std::forward<F>(f)(this->value());
|
|
}
|
|
|
|
template<mutex::Delegate<T&&> F>
|
|
inline decltype(auto) with_lock(F&& f) && noexcept(std::is_nothrow_invocable_v<F, T&&>)
|
|
{
|
|
// Assume this is owned by caller. No lock required.
|
|
//std::unique_lock ul{ this->get_mutex() };
|
|
return std::forward<F>(f)(std::move(this->value()));
|
|
}
|
|
|
|
template<mutex::Delegate<T const&&> F>
|
|
inline decltype(auto) with_lock(F&& f) const&& noexcept(std::is_nothrow_invocable_v<F, T const&&>)
|
|
{
|
|
std::unique_lock ul{ this->get_mutex() };
|
|
return std::forward<F>(f)(std::move(this->value()));
|
|
}
|
|
|
|
template<mutex::Delegate<T> F>
|
|
inline decltype(auto) with_unique_lock(F&& f) const noexcept(std::is_nothrow_invocable_v<F, T>)
|
|
{
|
|
std::unique_lock ul{ this->get_mutex() };
|
|
if constexpr(std::is_invocable_v<decltype(f), T, decltype(ul)>) {
|
|
return std::forward<F>(f)(*this->get_raw_ptr(), ul);
|
|
} else return std::forward<F>(f)(*this->get_raw_ptr());
|
|
}
|
|
};
|
|
|
|
namespace shared {
|
|
template<typename T>
|
|
struct Mutex : public mutex::Mutex<std::shared_mutex, T> {
|
|
using mutex::Mutex<std::shared_mutex, T>::Mutex;
|
|
|
|
template<mutex::Delegate<T&> F>
|
|
inline decltype(auto) with_lock(F&& f) & noexcept(std::is_nothrow_invocable_v<F, T&>)
|
|
{
|
|
std::lock_guard ul{ this->get_mutex() };
|
|
return std::forward<F>(f)(this->value());
|
|
}
|
|
|
|
template<mutex::Delegate<T const&> F>
|
|
inline decltype(auto) with_lock(F&& f) const& noexcept(std::is_nothrow_invocable_v<F, T const&>)
|
|
{
|
|
std::shared_lock ul{ this->get_mutex() };
|
|
return std::forward<F>(f)(this->value());
|
|
}
|
|
|
|
template<mutex::Delegate<T&&> F>
|
|
inline decltype(auto) with_lock(F&& f) && noexcept(std::is_nothrow_invocable_v<F, T&&>)
|
|
{
|
|
//std::shared_lock ul{ this->get_mutex() };
|
|
return std::forward<F>(f)(std::move(this->value()));
|
|
}
|
|
|
|
template<mutex::Delegate<T const&&> F>
|
|
inline decltype(auto) with_lock(F&& f) const&& noexcept(std::is_nothrow_invocable_v<F, T const&&>)
|
|
{
|
|
std::shared_lock ul{ this->get_mutex() };
|
|
return std::forward<F>(f)(std::move(this->value()));
|
|
}
|
|
|
|
template<mutex::Delegate<T&> F>
|
|
inline decltype(auto) with_unique_lock(F&& f) const noexcept(std::is_nothrow_invocable_v<F, T>)
|
|
{
|
|
std::unique_lock ul{ this->get_mutex() };
|
|
if constexpr(std::is_invocable_v<F, T, decltype(ul)>) {
|
|
return std::forward<F>(f)(*this->get_raw_ptr(), ul);
|
|
} else return std::forward<F>(f)(*this->get_raw_ptr());
|
|
}
|
|
|
|
template<mutex::Delegate<T const&> F>
|
|
inline decltype(auto) with_shared_lock(F&& f) const noexcept(std::is_nothrow_invocable_v<F, T>)
|
|
{
|
|
std::shared_lock ul{ this->get_mutex() };
|
|
if constexpr(std::is_invocable_v<F, T, decltype(ul)>) {
|
|
return std::forward<F>(f)(*this->get_raw_ptr(), ul);
|
|
} else return std::forward<F>(f)(*this->get_raw_ptr());
|
|
}
|
|
};
|
|
}
|
|
template<typename T>
|
|
using SharedMutex = shared::Mutex<T>;
|
|
#if 0
|
|
template<typename T>
|
|
class Mutex {
|
|
using value_type = std::decay_t<T>;
|
|
mutable value_type m_value;
|
|
protected:
|
|
std::mutex m_mutex{}; // TODO: Factor this out into template parameter to allow for `SharedMutex<T>` (or `shared::Mutex<T>`) e.g as partial-spec for / inheritance from `fx::mutex::Wrapped<std::{shared_,}mutex, T>`
|
|
public:
|
|
constexpr Mutex() noexcept requires(std::is_default_constructible_v<value_type>)
|
|
: m_value{} {}
|
|
constexpr Mutex(T&& value) noexcept
|
|
: m_value{std::forward<T>(value)} {}
|
|
|
|
Mutex(const Mutex&) = delete;
|
|
Mutex& operator=(
|
|
|
|
Mutex(Mutex&&) = default;
|
|
Mutex
|
|
};
|
|
|
|
namespace shared {
|
|
template<typename T>
|
|
#error class Mutex /* See above TODO about dual-impl */
|
|
}
|
|
template<typename T>
|
|
using SharedMutex = shared::Mutex<T>;
|
|
#endif
|
|
}
|