Let's start with the code:
class A { using MutexType = std::mutex; using ReadLock = std::unique_lock<MutexType>; using WriteLock = std::unique_lock<MutexType>; mutable MutexType mut_; std::string field1_; std::string field2_; public: ...
I added some pretty far-fetched type aliases that we will not use in C ++ 11, but have become much more useful in C ++ 14. Be patient, we will get there.
Your question comes down to the following:
How to write a move constructor and move an assignment operator for this class?
We will start with the move constructor.
Move constructor
Note that the mutex element is mutable . Strictly speaking, this is not necessary for participants in the movement, but I assume that you also want to copy participants. If this is not the case, there is no need to make a mutable .
When building A you do not need to block this->mut_ . But you need to block mut_ object you are building from (move or copy). This can be done like this:
A(A&& a) { WriteLock rhs_lk(a.mut_); field1_ = std::move(a.field1_); field2_ = std::move(a.field2_); }
Note that we first had to construct this elements by default, and then assign their values only after a.mut_ locked.
Move destination
The move assignment operator is significantly more complicated because you do not know if any other stream refers to the lhs or rhs of the assignment expression. In general, you need to protect yourself from the following scenario:
Here is the move destination statement that correctly protects the scenario described above:
A& operator=(A&& a) { if (this != &a) { WriteLock lhs_lk(mut_, std::defer_lock); WriteLock rhs_lk(a.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); field1_ = std::move(a.field1_); field2_ = std::move(a.field2_); } return *this; }
Note that std::lock(m1, m2) should be used to lock two mutexes, and not just lock them one by one. If you block them one by one, then when two threads assign two objects in the opposite order, as shown above, you can get a dead end. The point of std::lock is to avoid this deadlock.
Copy constructor
You did not ask about the members of the copy, but now we can talk about them (if not you, someone will need them).
A(const A& a) { ReadLock rhs_lk(a.mut_); field1_ = a.field1_; field2_ = a.field2_; }
The copy constructor is similar to the move constructor, except that the WriteLock alias is used instead of ReadLock . Currently, both of these aliases are std::unique_lock<std::mutex> , and therefore this really does not matter.
But in C ++ 14 you will have the opportunity to say the following:
using MutexType = std::shared_timed_mutex; using ReadLock = std::shared_lock<MutexType>; using WriteLock = std::unique_lock<MutexType>;
It may be optimization, but not definitely. You will need to measure to determine if it is. But with this change, you can copy a construct from the same number in multiple threads at the same time. The C ++ 11 solution forces you to make such streams sequential, even if rhs does not change.
Copy Destination
For completeness, here is the copy assignment operator, which should be reasonably clear after reading the rest:
A& operator=(const A& a) { if (this != &a) { WriteLock lhs_lk(mut_, std::defer_lock); ReadLock rhs_lk(a.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); field1_ = a.field1_; field2_ = a.field2_; } return *this; }
Etc.
Any other members or free functions that access state A should also be protected if you expect multiple threads to be able to call them immediately. For example, here is swap :
friend void swap(A& x, A& y) { if (&x != &y) { WriteLock lhs_lk(x.mut_, std::defer_lock); WriteLock rhs_lk(y.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); using std::swap; swap(x.field1_, y.field1_); swap(x.field2_, y.field2_); } }
Please note that if you just depend on std::swap for the job, the lock will be inaccurate in detail, lock and unlock between the three moves that std::swap will execute internally.
In fact, thinking about swap may give you an idea of the API that might be required to provide thread-safe A , which in general would be different from the “unsafe” API, due to the “drill down lock” problem.
Also pay attention to the need for protection against "self-exchange". "self-swap" must be non-op. Without a self-test, it recursively blocks the same mutex. This could also be solved without a self-test using std::recursive_mutex for MutexType .
Update
In the comments below, Yakk is rather unhappy with the need to build things in copy and move constructs by default (and he has a point). If you are decisive enough about this problem, so that you are ready to spend memory on it, you can avoid it like this:
Add all the types of locks that you need as data items. These members must be in front of the protected data:
mutable MutexType mut_; ReadLock read_lock_; WriteLock write_lock_;
And then in the constructors (for example, in the copy constructor) do the following:
A(const A& a) : read_lock_(a.mut_) , field1_(a.field1_) , field2_(a.field2_) { read_lock_.unlock(); }
Oops, Jakk deleted his comment before I had the opportunity to complete this update. But he deserves recognition for pushing this issue and making a decision in this answer.
Update 2
And dyp came up with this good suggestion:
A(const A& a) : A(a, ReadLock(a.mut_)) {} private: A(const A& a, ReadLock rhs_lk) : field1_(a.field1_) , field2_(a.field2_) {}