This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

4172. unique_lock self-move-assignment is broken

Section: 32.6.5.4.2 [thread.lock.unique.cons], 32.6.5.5.2 [thread.lock.shared.cons] Status: New Submitter: Casey Carter Opened: 2024-11-13 Last modified: 2024-11-18

Priority: Not Prioritized

View all other issues in [thread.lock.unique.cons].

View all issues with New status.

Discussion:

The postconditions in 32.6.5.4.2 [thread.lock.unique.cons] paragraph 19:

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.
contradict themselves if *this and the parameter u refer to the same object. (Presumably "this construction" means the assignment, and it is copy-pasta from the move constructor postconditions.) Apparently unique_lock didn't get the memo that we require well-defined behavior from self-move-assignment as of LWG 2839(i).

Also, the move assignment operator doesn't specify what it returns.

[2024-11-18; Casey expands the PR to cover shared_lock]

shared_lock has the same problems, and can be fixed in the same way.

Proposed resolution:

This wording is relative to N4993.

Drafting Note: I've chosen to use the move-into-temporary-and-swap idiom here to keep things short and sweet. Since move construction, swap, and destruction are all noexcept, I've promoted move assignment from "Throws: Nothing" to noexcept as well. This is consistent with eliminating the implicit narrow contract condition that *this and u refer to distinct objects.
  1. In the class synopsis in 32.6.5.4.1 [thread.lock.unique.general], annotate the move assignment operator as noexcept:

    
      namespace std {
        template<class Mutex>
        class unique_lock {
          [...]
          unique_lock& operator=(unique_lock&& u) noexcept;
          [...]
        };
      }
    
  2. Modify 32.6.5.4.2 [thread.lock.unique.cons] as follows:

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

    -18- Effects: If owns calls pm->unlock(). Equivalent to: unique_lock{std::move(u)}.swap(*this).

    -?- Returns: *this.

    -19- 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.

    -20- [Note 1: 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]

    -21- Throws: Nothing.

  3. Modify 32.6.5.5.2 [thread.lock.shared.cons] as follows:

    
    shared_lock& operator=(shared_lock&& sl) noexcept;
    

    -17- Effects: If owns calls pm->unlock_shared(). Equivalent to: shared_lock{std::move(sl)}.swap(*this).

    -?- Returns: *this.

    -18- Postconditions: pm == sl_p.pm and owns == sl_p.owns (where sl_p is the state of sl just prior to this assignment), sl.pm == nullptr and sl.owns == false.