2839. Self-move-assignment of library types, again

Section: 16.4.6.15 [lib.types.movedfrom], 16.4.5.9 [res.on.arguments], 23.2.2 [container.requirements.general] Status: C++23 Submitter: Tim Song Opened: 2016-12-09 Last modified: 2023-11-22

Priority: 2

View all issues with C++23 status.

Discussion:

LWG 2468's resolution added to MoveAssignable the requirement to tolerate self-move-assignment, but that does nothing for library types that aren't explicitly specified to meet MoveAssignable other than make those types not meet MoveAssignable any longer.

To realize the intent here, we need to carve out an exception to 16.4.5.9 [res.on.arguments]'s restriction for move assignment operators and specify that self-move-assignment results in valid but unspecified state unless otherwise specified. The proposed wording below adds that to 16.4.6.15 [lib.types.movedfrom] since it seems to fit well with the theme of the current paragraph in that section.

In addition, to address the issue with 23.2.2 [container.requirements.general] noted in LWG 2468's discussion, the requirement tables in that subclause will need to be edited in a way similar to LWG 2468.

[2017-01-27 Telecon]

Priority 2

[2018-1-26 issues processing telecon]

Status to 'Open'; Howard to reword using 'MoveAssignable'.

Previous resolution [SUPERSEDED]:

This wording is relative to N4618.

  1. Add a new paragraph at the end of 16.4.6.15 [lib.types.movedfrom]:

    -1- Objects of types defined in the C++ standard library may be moved from (12.8). Move operations may be explicitly specified or implicitly generated. Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.

    -?- An object of a type defined in the C++ standard library may be move-assigned (11.4.6 [class.copy.assign]) to itself. Such an assignment places the object in a valid but unspecified state unless otherwise specified.

  2. Add a note at the end of 16.4.5.9 [res.on.arguments]/1, bullet 3, as indicated:

    -1- Each of the following applies to all arguments to functions defined in the C++ standard library, unless explicitly stated otherwise.

    1. (1.1) — […]

    2. (1.2) — […]

    3. (1.3) — If a function argument binds to an rvalue reference parameter, the implementation may assume that this parameter is a unique reference to this argument. [Note: If the parameter is a generic parameter of the form T&& and an lvalue of type A is bound, the argument binds to an lvalue reference (14.8.2.1) and thus is not covered by the previous sentence. — end note] [Note: If a program casts an lvalue to an xvalue while passing that lvalue to a library function (e.g. by calling the function with the argument std::move(x)), the program is effectively asking that function to treat that lvalue as a temporary. The implementation is free to optimize away aliasing checks which might be needed if the argument was an lvalue. — end note] [Note: This does not apply to the argument passed to a move assignment operator (16.4.6.15 [lib.types.movedfrom]). — end note]

  3. Edit Table 83 "Container requirements" in 23.2.2 [container.requirements.general] as indicated:

    Table 83 — Container requirements
    Expression Return type Operational
    semantics
    Assertion/note
    pre-/post-condition
    Complexity
    a = rv T& All existing elements of a
    are either move
    assigned to or
    destroyed
    post: If a and rv do not refer to the same object,
    a shall be equal to the value that
    rv had before this assignment
    linear
  4. Edit Table 86 "Allocator-aware container requirements" in 23.2.2 [container.requirements.general] as indicated:

    Table 86 — Allocator-aware container requirements
    Expression Return type Assertion/note
    pre-/post-condition
    Complexity
    a = rv T& Requires: If allocator_traits<allocator_type
    >::propagate_on_container_move_assignment::value

    is false, T is MoveInsertable
    into X and MoveAssignable.
    All existing elements of a are either
    move assigned to or destroyed.
    post: If a and rv do not refer
    to the same object,
    a shall be equal
    to the value that rv had before this assignment
    linear

[2018-08-16, Howard comments and provides updated wording]

I agreed to provide proposed wording for LWG 2839 that was reworded to use MoveAssignable. The advantage of this is that MoveAssignable specifies the self-assignment case, thus we do not need to repeat ourselves.

[2018-08-23 Batavia Issues processing]

Howard and Tim to discuss a revised P/R.

Previous resolution [SUPERSEDED]:

This wording is relative to N4762.

  1. Add a new subsection to 16.4.6 [conforming] after 16.4.6.5 [member.functions]:

    Special members [conforming.special]

    Class types defined by the C++ standard library and specified to be default constructible, move constructible, copy constructible, move assignable, copy assignable, or destructible, shall meet the associated requirements Cpp17DefaultConstructible, Cpp17MoveConstructible, Cpp17CopyConstructible, Cpp17MoveAssignable, Cpp17CopyAssignable, and Cpp17Destructible, respectively (16.4.4.2 [utility.arg.requirements]).

[2020-06-06 Tim restores and updates P/R following 2020-05-29 telecon discussion]

The standard doesn't define phrases like "default constructible" used in the previous P/R. Moreover, the library provides a variety of wrapper types, and whether these types meet the semantic requirements of Cpp17Meowable (and maybe even syntactic, depending on how "copy constructible" is interpreted) depends on the property of their underlying wrapped types, which might not even be an object type (e.g., tuple or pair of references). This is a large can of worms (see LWG 2146) that we don't want to get into.

There is a suggestion in the telecon to blanket-exempt move-assignment operators from the 16.4.5.9 [res.on.arguments] 1.3 requirement. The revised wording below does not do so, as that would carve out not just self-move-assignment but also other aliasing scenarios in which the target object owns the source object. Whether such scenarios should be permitted is outside the scope of this issue, though notably assignable_from (18.4.8 [concept.assignable]) contains a note alluding to these cases and suggesting that they should be considered to be outside the domain of = entirely.

[2020-07-17; issue processing telecon]

LWG reviewed the latest proposed resolution. Unanimous consent to move to Ready.

[2020-11-09 Approved In November virtual meeting. Status changed: Ready → WP.]

Proposed resolution:

This wording is relative to N4861.

  1. Add a new paragraph at the end of 16.4.6.15 [lib.types.movedfrom]:

    -1- Objects of types defined in the C++ standard library may be moved from ( [clss.copy.ctor]). Move operations may be explicitly specified or implicitly generated. Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.

    -?- An object of a type defined in the C++ standard library may be move-assigned (11.4.6 [class.copy.assign]) to itself. Unless otherwise specified, such an assignment places the object in a valid but unspecified state.

  2. Edit 16.4.5.9 [res.on.arguments]/1, bullet 3, as indicated:

    -1- Each of the following applies to all arguments to functions defined in the C++ standard library, unless explicitly stated otherwise.

    1. (1.1) — […]

    2. (1.2) — […]

    3. (1.3) — If a function argument binds to an rvalue reference parameter, the implementation may assume that this parameter is a unique reference to this argument, except that the argument passed to a move-assignment operator may be a reference to *this (16.4.6.15 [lib.types.movedfrom]). [Note: If the type of a parameter is a generic parameter of the form T&& and an lvalue of type A is bound, the argument binds to an lvalue reference (13.10.3.2 [temp.deduct.call]) and thus is not covered by the previous sentence. forwarding reference (13.10.3.2 [temp.deduct.call]) that is deduced to an lvalue reference type, then the argument is not bound to an rvalue reference.end note] [Note: If a program casts an lvalue to an xvalue while passing that lvalue to a library function (e.g. by calling the function with the argument std::move(x)), the program is effectively asking that function to treat that lvalue as a temporary. The implementation is free to optimize away aliasing checks which might be needed if the argument was an lvalue. — end note]

  3. Edit Table 73 "Container requirements" in 23.2.2 [container.requirements.general] as indicated:

    Table 73 — Container requirements
    Expression Return type Operational
    semantics
    Assertion/note
    pre-/post-condition
    Complexity
    a = rv T& All existing elements of a are either move assigned to or destroyed Postconditions: If a and rv do not refer to the same object, a is equal to the value that rv had before this assignment. linear
  4. Edit Table 76 "Allocator-aware container requirements" in 23.2.2 [container.requirements.general] as indicated:

    Table 86 — Allocator-aware container requirements
    Expression Return type Assertion/note
    pre-/post-condition
    Complexity
    a = rv T& Preconditions: If allocator_traits<allocator_type>
    ::propagate_on_container_move_assignment::value
    is false,
    T is Cpp17MoveInsertable into X and Cpp17MoveAssignable.
    Effects: All existing elements of a are either move assigned to or destroyed.
    Postconditions: If a and rv do not refer to the same object, a is equal to the value that rv had before this assignment.
    linear