2104. unique_lock move-assignment should not be noexcept

Section: 33.4.4.3 [thread.lock.unique] Status: C++14 Submitter: Anthony Williams Opened: 2011-11-27 Last modified: 2017-07-06

Priority: Not Prioritized

View all issues with C++14 status.

Discussion:

I just noticed that the unique_lock move-assignment operator is declared noexcept. This function may call unlock() on the wrapped mutex, which may throw.

Suggested change: remove the noexcept specification from unique_lock::operator=(unique_lock&&) in 33.4.4.3 [thread.lock.unique] and 33.4.4.3.1 [thread.lock.unique.cons].

Daniel:

I think the situation is actually a bit more complex as it initially looks.

First, the effects of the move-assignment operator are (emphasize mine):

Effects: If owns calls pm->unlock().

Now according to the BasicLockable requirements:

m.unlock()

3 Requires: The current execution agent shall hold a lock on m.

4 Effects: Releases a lock on m held by the current execution agent.

Throws: Nothing.

This shows that unlock itself is a function with narrow contract and for this reasons no unlock function of a mutex or lock itself does have a noexcept specifier according to our mental model.

Now the move-assignment operator attempts to satisfy these requirement of the function and calls it only when it assumes that the conditions are ok, so from the view-point of the caller of the move-assignment operator it looks as if the move-assignment operator would in total a function with a wide contract.

The problem with this analysis so far is, that it depends on the assumed correctness of the state "owns".

Looking at the construction or state-changing functions, there do exist several ones that depend on caller-code satisfying the requirements and there is one guy, who looks most suspicious:

unique_lock(mutex_type& m, adopt_lock_t);

11 Requires: The calling thread own the mutex.
[…]
13 Postconditions: pm == &m and owns == true.

because this function does not even call lock() (which may, but is not required to throw an exception if the calling thread does already own the mutex). So we have in fact still a move-assignment operator that might throw an exception, if the mutex was either constructed or used (call of lock) incorrectly.

The correct fix seems to me to also add a "Throws: Nothing" element to the move-assignment operator, because using it correctly shall not throw an exception.

[Issaquah 2014-02-11: Move to Immediate after SG1 review]

Proposed resolution:

This wording is relative to the FDIS.

  1. Change 33.4.4.3 [thread.lock.unique], class template unique_lock synopsis as indicated:

    namespace std {
      template <class Mutex>
      class unique_lock {
      public:
        typedef Mutex mutex_type;
        […]
        unique_lock(unique_lock&& u) noexcept;
        unique_lock& operator=(unique_lock&& u) noexcept;
        […]
      };
    }
    
  2. Change 33.4.4.3.1 [thread.lock.unique.cons] around p22 as indicated:

    unique_lock& operator=(unique_lock&& u) noexcept;
    

    -22- Effects: If owns calls pm->unlock().

    -23- Postconditions: pm == u_p.pm and owns == u_p.owns (where u_p is the state of u just prior to this construction), u.pm == 0 and u.owns == false.

    -24- [Note: With a recursive mutex it is possible for both *this and u to own the same mutex before the assignment. In this case, *this will own the mutex after the assignment and u will not. — end note]

    -??- Throws: Nothing.