Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>There are two separate concerns (thread-safety and exception-safety) and it seems best to address them separately. To allow constructors taking another object as argument to acquire a lock while initializing the members, it is necessary to factor the data members into a separate class anyway: This way a lock can be acquired while the subobject is initialized and the class maintaining the actual data can ignore any concurrency issues. Thus, the class will be split into two parts: <code>class A</code> to deal with concurrency issues and <code>class A_unlocked</code> to maintain the data. Since the member functions of <code>A_unlocked</code> don't have any concurrency protection, they shouldn't be directly exposed in the interface and, thus, <code>A_unlocked</code> is made a private member of <code>A</code>.</p> <p>Creating an exception-safe assignment operator is straight forward, leveraging the copy constructor. The argument is copied and the members are swapped:</p> <pre><code>A_unlocked&amp; A_unlocked::operator= (A_unlocked const&amp; other) { A_unlocked(other).swap(*this); return *this; } </code></pre> <p>Of course, this means that a suitable copy constructor and a <code>swap()</code> member are implemented. Dealing with the allocation of multiple resources, e.g., multiple objects allocated on the heap, is easiest done by having a suitable resource handler for each of the objects. Without the use of resource handlers it becomes quickly very messy to correctly clean up all resources in case an exception is thrown. For the purpose of maintaining heap allocated memory <code>std::unique_ptr&lt;T&gt;</code> (or <code>std::auto_ptr&lt;T&gt;</code> if you can't use C++ 2011) is a suitable choice. The code below just copies the pointed to objects although there isn't much point in allocating the objects on the heap rather than making them members. In a real example the objects would probably implement a <code>clone()</code> method or some other mechanism to create an object of the correct type:</p> <pre><code>class A_unlocked { private: std::unique_ptr&lt;B&gt; pb; std::unique_ptr&lt;C&gt; pc; // ... public: A_unlocked(/*...*/); A_unlocked(A_unlocked const&amp; other); A_unlocked&amp; operator= (A_unlocked const&amp; other); void swap(A_unlocked&amp; other); // ... }; A_unlocked::A_unlocked(A_unlocked const&amp; other) : pb(new B(*other.pb)) , pc(new C(*other.pc)) { } void A_unlocked::swap(A_unlocked&amp; other) { using std::swap; swap(this-&gt;pb, other.pb); swap(this-&gt;pc, other.pc); } </code></pre> <p>For the thread-safety bit it is necessary to know that no other thread is messing with the copied object. The way to do this is using a mutex. That is, <code>class A</code> looks something like this:</p> <pre><code>class A { private: mutable std::mutex d_mutex; A_unlocked d_data; public: A(/*...*/); A(A const&amp; other); A&amp; operator= (A const&amp; other); // ... }; </code></pre> <p>Note, that all members of <code>A</code> will need to do some concurrency protection if the objects of type <code>A</code> are meant to be used without external locking. Since the mutex used to guard against concurrent access isn't really part of the object's state but needs to be changed even when reading the object's state, it is made <code>mutable</code>. With this in place, creating a copy constructor is straight forward:</p> <pre><code>A::A(A const&amp; other) : d_data((std::unique_lock&lt;std::mutex&gt;(other.d_mutex), other.d_data)) { } </code></pre> <p>This locks the argument's mutex and delegates to the member's copy constructor. The lock is automatically released at the end of the expression, independent of whether the copy was successful or threw an exception. The object being constructed doesn't need any locking because there is no way that another thread knows about this object, yet.</p> <p>The core logic of the assignment operator also just delegates to the base, using its assignment operator. The tricky bit is that there are two mutexes which need to be locked: the one for the object being assigned to and the one for the argument. Since another thread could assign the two objects in just the opposite way, there is a potential for dead-lock. Conveniently, the standard C++ library provides the <code>std::lock()</code> algorithm which acquires locks in an appropriate way that avoids dead-locks. One way to use this algorithm is to pass in unlocked <code>std::unique_lock&lt;std::mutex&gt;</code> objects, one for each mutex needed to be acquired:</p> <pre><code>A&amp; A::operator= (A const&amp; other) { if (this != &amp;other) { std::unique_lock&lt;std::mutex&gt; guard_this(this-&gt;d_mutex, std::defer_lock); std::unique_lock&lt;std::mutex&gt; guard_other(other.d_mutex, std::defer_lock); std::lock(guard_this, guard_other); *this-&gt;d_data = other.d_data; } return *this; } </code></pre> <p>If at any point during the assignment an exception is thrown, the lock guards will release the mutexes and the resource handlers will release any newly allocated resource. Thus, the above approach implements the strong exception guarantee. Interestingly, the copy assignment needs to do a self-assignment check to prevent locking the same mutex twice. Normally, I maintain that a necessary self-assignment check is an indication that the assignment operator isn't exception safe but I think the code above is exception safe.</p> <p>This is a major rewrite of the answer. Earlier versions of this answer were either prone to a lost update or to a dead-lock. Thanks to Yakk for pointing out the problems. Although the result of addressing the issues involves more code, I think each individual part of the code is actually simpler and can be investigated for correctness.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload