Impossible to be const-correct when combining data and it's lock?
c++, c++11, multithreading, mutable, thread-safety
Solution
Personally, I'd prefer a design where you don't have to lock manually, and the data is properly encapsulated in a way that you cannot actually access it without locking first.
One option is to have a friend function `apply` or something that does the locking, grabs the encapsulated data and passes it to a function object that is run with the lock held within it.
//! Applies a function to the contents of a locker_box
/*! Returns the function's result, if any */
template <typename Fun, typename T, typename BasicLockable>
ResultOf<Fun(T&)> apply(Fun&& fun, locker_box<T, BasicLockable>& box) {
std::lock_guard<BasicLockable> lock(box.lock);
return std::forward<Fun>(fun)(box.data);
}
//! Applies a function to the contents of a locker_box
/*! Returns the function's result, if any */
template <typename Fun, typename T, typename BasicLockable>
ResultOf<Fun(T const&)> apply(Fun&& fun, locker_box<T, BasicLockable> const& box) {
std::lock_guard<BasicLockable> lock(box.lock);
return std::forward<Fun>(fun)(box.data);
}
Usage then becomes:
void print_values() const
{
apply([](std::vector<int> const& the_vector) {
for(const int val : the_vector) {
std::cout << val << std::endl;
}
}, values_);
}
Alternatively, you can abuse range-based for loop to properly scope the lock and extract the value as a "single" operation. All that is needed is the proper set of iterators1:
for(auto&& the_vector : box.open()) {
// lock is held in this scope
// do our stuff normally
for(const int val : the_vector) {
std::cout << val << std::endl;
}
}
I think an explanation is in order. The general idea is that `open()` returns a RAII handle that acquires the lock on construction and releases it upon destruction. The range-based for loop will ensure this temporary lives for as long as that loop executes. This gives the proper lock scope.
That RAII handle also provides `begin()` and `end()` iterators for a range with the single contained value. This is how we can get at the protected data. The range-based loop takes care of doing the dereferencing for us and binding it to the loop variable. Since the range is a singleton, the "loop" will actually always run exactly once.
The `box` should not provide any other way to get at the data, so that it actually enforces interlocked access.
Of course one can stow away a reference to the data once the box is open, in a way that the reference is available after the box closes. But this is for protecting against Murphy, not Machiavelli.
The construct looks weird, so I wouldn't blame anyone for not wanting it. One one hand I want to use this because the semantics are perfect, but on the other hand I don't want to because this is not what range-based for is for. On the gripping hand this range-RAII hybrid technique is rather generic and can be easily abused for other ends, but I will leave that to your imagination/nightmares ;) Use at your own discretion.
1 Left as an exercise for the reader, but a short example of such a set of iterators can be found in my own locker_box implementation.
Problem
I've been looking at ways to combine a piece of data which will be accessed by multiple threads alongside the lock provisioned for thread-safety. I think I've got to a point where I don't think its possible to do this whilst maintaining const-correctness. Take the following class for example: ``` template <typename TType, typename TMutex> class basic_lockable_type { public: typedef TMutex lock_type; public: template <typename... TArgs> explicit basic_lockable_type(TArgs&&... args) : TType(std::forward<TArgs...>(args)...) {} TType& data() { return data_; } const TType& data() const { return data_; } void lock() { mutex_.lock(); } void unlock() { mutex_.unlock(); } private: TType data_; mutable TMutex mutex_; }; typedef basic_lockable_type<std::vector<int>, std::mutex> vector_with_lock; ``` In this I try to combine the data and lock, marking `mutex_` as `mutable`. Unfortunately this isn't enough as I see it because when used, `vector_with_lock` would have to be marked as `mutable` in order for a read operation to be performed from a `const` function which isn't entirely correct (`data_` should be `mutable` from a const). ``` void print_values() const { std::lock_guard<vector_with_lock> lock(values_); for(const int val : values_) { std::cout << val << std::endl; } } vector_with_lock values_; ``` Can anyone see anyway around this such that const-correctness is maintained whilst combining data and lock? Also, have I made any incorrect assumptions here?