Probably many people had to work with multithreaded applications with C++ and can understand how messy can be fine-grained locking of objects.
So once in a while I came to idea of implementing some proxy objects using a so-called "drill down" property of operator->
and RAII in mind.
Yes. There is some implementations that do something like that but I didn't find something that fully satisfy me or at least something battle tested but not bloated.
Here I want to show and discuss working prototype for shared locking of objects that works as simple as that:
<...>
class dummy {
public:
dummy(std::string p) : p_(p) { }
std::string to_string() const { return p_; }
void set_string(std::string str) { p_ = str; }
private:
std::string p_;
};
<...>
// there it is
shared_lock_wrap<dummy> data("hello");
{
auto p = data.get();
// p->set_string("hello, world"); // will cause an error
std::cout << "ro access: " << p->to_string() << std::endl;
}
auto p = data.get_mutable();
p->set_string("hello, stackexchange");
std::cout << "rw access: " << p->to_string() << std::endl;
<...>
If you're still interested there is implementation of shared_lock_wrap
that was used earlier. This should work with any decent C++14 compiler (I used GCC 8.2):
template<
typename T,
typename _TMtx,
template<typename> class _TLock,
template<typename> class _TMutLock
>
class lock_wrap_impl {
template<typename W>
using mimic_unique = typename std::conditional<
std::is_move_constructible<W>::value,
W,
std::unique_ptr<W>
>::type;
using mutex_type = std::unique_ptr<_TMtx>;
using lock_type = mimic_unique<_TLock<_TMtx> >;
using mutable_lock_type = mimic_unique<_TMutLock<_TMtx> >;
template<bool M, typename W>
using mimic_const = typename std::conditional<M, W, const W>::type;
template<typename L>
using is_mutable = typename std::is_same<L, mutable_lock_type>;
template<bool M=true>
using ptr_type = const std::shared_ptr<mimic_const<M, T> >;
public:
template<typename L>
class proxy {
public:
ptr_type<is_mutable<L>::value > operator->() { return obj_; }
proxy(proxy<L>&& other) :
obj_(other.obj_),
lock_(std::move(other.lock_))
{}
private:
friend lock_wrap_impl<T, _TMtx, _TLock, _TMutLock>;
proxy(ptr_type<> obj, L&& lock) :
obj_(obj),
lock_(std::move(lock))
{}
ptr_type<> obj_;
L lock_;
};
lock_wrap_impl(T&& obj) :
obj_(&obj),
mtx_(mutex_type(new _TMtx))
{}
template<typename ...Args>
lock_wrap_impl(Args&& ...args) :
obj_(new T(std::forward<Args>(args)...)),
mtx_(mutex_type(new _TMtx))
{}
lock_wrap_impl(lock_wrap_impl<T, _TMtx, _TLock, _TMutLock>&& other) :
obj_(std::move(other.obj_)),
mtx_(std::move(other.mtx_))
{}
/**
* For types that ARE move constructible
* e.g std::shared_lock
*/
template<typename Q = _TLock<_TMtx> >
proxy<lock_type> get(typename std::enable_if<std::is_move_constructible<Q>::value>::type* = 0) {
return proxy<lock_type>(obj_, lock_type(*mtx_));
}
template<typename Q = _TMutLock<_TMtx> >
proxy<mutable_lock_type> get_mutable(typename std::enable_if<std::is_move_constructible<Q>::value>::type* = 0) {
return proxy<mutable_lock_type>(obj_, mutable_lock_type(mtx_));
}
/**
* For types that aren't move constructible
*/
template<typename Q = _TLock<_TMtx> >
proxy<lock_type> get(typename std::enable_if<!std::is_move_constructible<Q>::value>::type* = 0) {
return proxy<lock_type>(obj_, lock_type(new Q(*mtx_)));
}
template<typename Q = _TMutLock<_TMtx> >
proxy<mutable_lock_type> get_mutable(typename std::enable_if<!std::is_move_constructible<Q>::value>::type* = 0) {
return proxy<mutable_lock_type>(obj_, mutable_lock_type(new Q(*mtx_)));
}
private:
ptr_type<> obj_;
mutex_type mtx_;
};
template<typename T>
using shared_lock_wrap = lock_wrap_impl<T, std::shared_timed_mutex, std::shared_lock, std::unique_lock>;
Worth to note that I'm aware that there's some drawbacks: I prefer to use shared locks (e.g std::shared_mutex
/shared_timed_mutex
or boost
alternatives) but kept in mind that there are mutexes/lockers types that are "exclusively lockable" (like std::lock_guard
with std::mutex
or boost::scoped_lock
) but latter doesn't work with this exact implementation due to equality of _TLock
and _TMutLock
. Though it works fine with shared locks so maybe it will be fixed at some point but that is not what I personally needed.
So what do you guys think about it? About idea or code structure in general or basically anything that you think. Maybe you have some thoughts about how it can be optimized.
Personally I'm mostly prefer golang
for developing but due to job or some embedded applications I also work with C++ or (occasionally plain C).
std::lock_guard
(/* For types that aren't move constructible e.g. std::lock_guard*/
) as a feature, and then you say it isn't working in your text. You should edit your code to retain only what's working (edit it before you get an answer). \$\endgroup\$_TMutLock
but just not with std::mutex) \$\endgroup\$