This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of WP 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: WP Submitter: Casey Carter Opened: 2024-11-13 Last modified: 2025-02-16

Priority: Not Prioritized

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

View all issues with WP 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.

[2025-02-07; Reflector poll]

Set status to Tentatively Ready after seven votes in favour during reflector poll.

"Should use parentheses not braces for the initializations." Jonathan volunteers to do that editorially after this gets approved.

[Hagenberg 2025-02-16; Status changed: Voting → WP.]

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.