This is a class that holds mutex and associated object. On checkout/access the mutex is locked while the stack object lives and a reference to object is obtained. Any suggestions or improvements is welcomed.
#include <mutex>
template<class T>
class MutexProtectedObject {
//this relies on benign use and it is just a helper rather than a foolproof solution - ie. anyone may still save a pointer to obj etc. and access it later
public:
class Access {
public:
Access(std::mutex& m, T& obj) : sl_(m), obj_(obj) {
}
T& obj_;
private:
std::scoped_lock<std::mutex> sl_;//mutex is locked here (by RAII)
};
Access access() {
return Access(mutex, obj);
}
MutexProtectedObject(const MutexProtectedObject&) = delete;
MutexProtectedObject& opreator=(const MutexProtectedObject&) = delete;
private:
std::mutex mutex;
T obj;
};
-
2\$\begingroup\$ Please make sure the posted code works correctly. I'd also suggest adding usage examples, and unit tests. \$\endgroup\$user673679– user6736792019年02月13日 13:35:01 +00:00Commented Feb 13, 2019 at 13:35
2 Answers 2
Access
I don't like the fact that Access
gives you a reference to the object directly. This leads to issues with somebody accidentally keeping a reference to the object after the Accesses
object is destroyed or accidentally making a copy of the object.
T& myRef = wrapped.access().obj_; // Now I have a reference to the object
// But the Accesses object is gone.
myRef.doStuffWithNoLock();
// or
T myCopy = wrapped.access().obj_; // I made a copy of the object.
How about Access
delegating all accesses via operator ->
? That way you don't leak the object to external code.
class Access
{
public:
T* operator->() {return &obj_;}
private:
T& obj_;
};
Now you can use Access
like it was a T*
:
auto foo = wrapper.access();
foo->fun1(args);
foo->fun2(args);
Initializer list
The order of initialization of members is the order of declaration. If you define the order deferently in the initializer list this can be confusing (most compilers will warn you about it).
class Access {
public:
Access(std::mutex& m, T& obj)
: sl_(m) // This is not the order they are
, obj_(obj) // initialized in. The obj_ will be done first
{}
T& obj_;
private:
std::scoped_lock<std::mutex> sl_;//mutex is locked here (by RAII)
};
Best practice is to keep the initializer list in the same order as the declaration order in the class.
-
\$\begingroup\$ Ah, that's much better than my (removed) suggestion. I couldn't see how to prevent the
T&
outliving theAccess
, but I think you've nailed it. We do still have the ability to retain anAccess
whoseT
has been destroyed; I don't know whether we can do anything about that. \$\endgroup\$Toby Speight– Toby Speight2019年02月13日 17:00:55 +00:00Commented Feb 13, 2019 at 17:00 -
\$\begingroup\$ I sort of arrived at the same conclusion about the member object access on my own after poking a bit around. Thank you. \$\endgroup\$darune– darune2019年02月13日 18:17:54 +00:00Commented Feb 13, 2019 at 18:17
There's no way to construct a MutexProtectedObject
unless T
is default-constructible. I recommend a forwarding constructor, something like (untested):
template<typename... Args>
MutexProtectedObject(Args&&... args)
: mutex{},
obj{std::forward<Args>(args)...}
{
}