It is often the case that you have a mutex or other lockable object which protects access to a complex variable, and you want to read the variable’s value (possibly modifying it in the process) while holding the lock, but then operate on the value outside the lock.
The traditional way is to do something like this:
// Assume we have these member variables std::mutex m_mutex; Widget m_widget; // Get a copy of the widget Widget widget; { auto guard = std::lock_guard(m_mutex); widget = m_widget; } ⟦ use the variable "widget" ⟧
This does suffer from the problem of running the Widget
constructor for an object that we’re going to overwrite anyway. The compiler will also have to deal with the possibility that the lock_
constructor throws an exception, forcing the destruction of a freshly-constructed Widget
.
I like to use this alternate pattern, using an immediately-invoked lambda that returns a value.
// Get a copy of the widget Widget widget = [&] { auto guard = std::lock_guard(m_mutex); return m_widget; }(); ⟦ use the variable "widget" ⟧
Thanks to the magic of copy elision (in this specific form known as Return Value Optimization, or RVO), we never have to construct a dummy Widget. Instead, the widget
variable directly receives a copy of m_widget
under the lock, but the value survives the lock.
This also can be used to move a value out of a lock-protected object, so that the value can destruct outside the lock.
// Move the widget into our variable Widget widget = [&] { auto guard = std::lock_guard(m_mutex); return std::move(m_widget); }(); // Allow the widget to destruct outside the lock
Or to exchange the value in the object while under the lock and operate on the old value outside the lock.
Widget widget = [&] { auto guard = std::lock_guard(m_mutex); return std::exchange(m_widget, {}); }(); // Allow the widget to destruct outside the lock
If you do this more than a few times, you may want to write a helper.
Widget CopySavedWidget() { auto guard = std::lock_guard(m_mutex); return m_widget; } template<typename T> Widget ExchangeSavedWidget(T&& value) { auto guard = std::lock_guard(m_mutex); return std::exchange(m_widget, std::forward<T>(value)): }
Respectfully, IMO, that (copy-construction vs default-construction-plus-copy-assignment) is not the major problem with the code.
The problem is calling potentially unknown code while holding a lock.
And even if the details of all internal operations for Widget are known today, do we want to introduce a dependency on how they are ?
Please see Mr. Herb Sutter's 2007 Article "Avoid Calling Unknown Code While Inside a Critical Section":
https://herbsutter.com/2007/11/06/effective-concurrency-avoid-calling-unknown-code-while-inside-a-critical-section/
My modest suggestion (if I may make one) is to handle such Widgets-which-need-copying-while-in-a-critical-region (which often appear in "thread-safe" implementations of the Command design pattern and the Subject-Observer design pattern) via the likes of shared_ptr, i.e....
The assumption here is that the operation under the lock is not unknown code. You are operating on private members of your own class.
It’s fascinating how C++ folks overcomplicate their language and standard library semantics to later look for better but, maybe, not so obvious ways to do otherwise very simple stuff. Helps me to stop complaining about the difficulties of the profession while working outside this ecosystem.
“This does suffer from the problem of running the Widget constructor for an object that we’re going to overwrite anyway.” – could use an optional. Probably inferior in this case, but can be useful in other cases
Alternate pattern:
auto lock = lock_from(mutex);
lock.take();
Widget widget = m_widget;
lock.release();
//…
lock.take();
// Can even do a write back under other lock; if the code pattern makes this sane. (In some cases it will, in some not.)
lock.release();
I’m accustomed to a slightly stronger guard for a lock that has take and release methods, and is smart enough to release the lock on destruction if it currently owns it.
And the standard has a tool for that: std::unique_lock
(see also: https://stackoverflow.com/questions/43019598/stdlock-guard-or-stdscoped-lock )
I really want to say you can avoid the lambda and just use a comma expression… But I’m going to guess that the lambda will result in less head scratching.
You presumably mean writing something like this:
The "obvious" question that I would have upon encountering this code, after figuring out that you meant to write the comma operator and did not misplace the closing parenthesis, would be whether the temporary lock guard is destroyed after the evaluation of the RHS, or after the Widget copy constructor returns. Reading through cppreference, I think it is the latter, so the code appears to be fine as far as I can tell, but temporaries are confusing enough without giving them side effects, so I would be very unhappy if I saw this...
I’ve done stuff like
m_referenceMember(CriticalSectionLock(lock), assert(IsValid(referenceProtectedByLock)), referenceProtectedByLock)
in initializer lists before just to avoid having to create some helper functions for debug assertions. Thoughts?