diff --git a/_posts/2021-03-31-relaxing-requirements-of-moved-from-objects.md b/_posts/2021-03-31-relaxing-requirements-of-moved-from-objects.md index cae7a32..fd48370 100644 --- a/_posts/2021-03-31-relaxing-requirements-of-moved-from-objects.md +++ b/_posts/2021-03-31-relaxing-requirements-of-moved-from-objects.md @@ -43,7 +43,9 @@ The C++ Standard Library requirements are overly restrictive regarding the state The issue was recognized in Geoffrey Romer's paper, [P2027R0](#p2027). The approach outlined here differs in the following ways: - The proposed requirements are (slightly) stronger to support `swap(a, a)` -- The proposed requirements avoid introducing a new object state, _partially formed_, (see [Future Implications](#future-implications)) +- The proposed requirements avoid introducing a new object state, _partially formed_, (see [Future Implications](#future-implications)) + + This paper details the issue and presents some suggested wording to address it. Depending on the wording chosen, it may be possible to address the issue with a defect report retroactively. {: .comment } @@ -81,7 +83,9 @@ And from the requirements for the [`assignable_from` concept](https://eel.is/c++ > If `rhs` is a non-const xvalue, the resulting state of the object to which it refers is valid but unspecified ([\[lib.types.movedfrom\]](https://eel.is/c++draft/lib.types.movedfrom)). -The term _valid_ in this context does not impose any actual requirement on the moved from object since there is no requirements about what operations must be available on a valid object. The referenced definition for _valid but unspecified_ refers to a general guarantee of library types, but is not a library requirement. The only restriction on the moved from object appears to be that it is _unspecified_, which is to say there is no requirement. +The term _valid_ in this context does not impose any actual requirement on the moved-from object since there is no requirements about what operations must be available on a valid object. The referenced definition for _valid but unspecified_ refers to a general guarantee of library types, but is not a library requirement. The only restriction on the moved-from object appears to be that it is _unspecified_, which is to say there is no requirement. + + The Standard even notes that [assignment need not be a total function](https://eel.is/c++draft/concept.assignable#2). @@ -89,27 +93,35 @@ The Standard even notes that [assignment need not be a total function](https://e Geoffrey Romer's paper, [P2027R0](#p2027) makes the observation that _valid but unspecified_ does not compose. The result is that any composite object requires additional code to move the object into a valid state on move. + + Attempting to make all operations total with respect to moved-from objects imposes an unnecessary performance penalty and the implementation of such operations is error-prone. Examples and details are provided in an _Annoyance_ I wrote for the upcoming [_Embracing Modern C++ Safely_](#move-annoyance). An example directly from the standard library is detailed in the [Weaker Guarantees for Future Components] section. {:#bl-move-annoyance} -In [the discussion](https://lists.isocpp.org/lib-ext/2020/01/14004.php) of [P2027R0](#p2027) there is a lot of confusion about the difference between requirements and guarantees in the Standard. In short, the standard library _requirements_ impose a set of syntactic and semantic requirements on operations on arguments (both types and values) passed to a standard component. i.e., Give `std::find(f, l, v)`; it is required that `f` and `l` denotes a [_valid range_](https://eel.is/c++draft/iterator.requirements#def:range,valid). +In [the discussion](https://lists.isocpp.org/lib-ext/2020/01/14004.php) of [P2027R0](#p2027) there is a lot of confusion about the difference between requirements and guarantees in the Standard. In short, the standard library _requirements_ impose a set of syntactic and semantic requirements on operations on arguments (both types and values) passed to a standard component. i.e., Given `std::find(f, l, v)`, it is required that `f` and `l` denotes a [_valid range_](https://eel.is/c++draft/iterator.requirements#def:range,valid). A _guarantee_ is provided by a standard component, and may be conditional on requirements of the components arguments. i.e., `std::vector::operator==()` is (guaranteed to be) an equivalence relation iff `T` meets the `Cpp17EqualityComparable` [requirements](https://eel.is/c++draft/tab:container.req). The confusion in part comes from stating a guarantee as _satisfying_ a named requirement. -The standard guarantees regarding moved from objects are specified in [lib.types.movedfrom](https://eel.is/c++draft/lib.types.movedfrom), as being _valid but unspecified_. The wording proposed in this paper does not change the guarantees. I included some discussion in the [_Future Implications_][weaker-guarantees-for-future-components] section about why it may be desirable to revisit this terminology. + + +The standard guarantees regarding moved-from objects are specified in [lib.types.movedfrom](https://eel.is/c++draft/lib.types.movedfrom), as being _valid but unspecified_. The wording proposed in this paper does not change the guarantees. I included some discussion in the [_Future Implications_](#future-implications) section about why it may be desirable to revisit this terminology. + + ### Requirements of a Moved-From Object -All known standard library implementations only require the following operations on an object, `mf`, that the library moved from within an operation: +All known standard library implementations only require the following operations on an object, `mf`, from which the library has moved: - `mf.~()` (The language also requires this for implicitly moved objects) - `mf = a` - `mf = move(a)` - `mf = move(mf)` -The last implementation requirement comes from `std::swap()` when invoked as `swap(a, a)`. It is note worthy that self-move-assignment is only required in the case where the object has already been moved-from. Self-swap does appear in some older standard library implementations of `std::random_shuffle()`. This underscores the need to support self-swap by the standard requirements. +The last implementation requirement comes from `std::swap()` when invoked as `swap(a, a)`. It is note worthy that self-move-assignment is only required in the case where the object has already been moved-from. Self-swap does appear in some older standard library implementations of `std::random_shuffle()`. This underscores the need to support self-swap by the standard requirements. + +Supporting self-move-assignment for this narrow case imposes some additional complexity because `a = move(a)` is, in general, a contradiction and is not required by the implementation of any standard component. The implementation required postcondition of `a = move(b)` is that `a` holds the prior value of `b` and the value of `b` is unspecified, but may be guaranteed to be a specific value. For example, if `a` and `b` are both of type `my_unique_ptr` with the guarantee that `a` will hold the prior value of `b`, and `b` will be equal to `nullptr`. Then for the expression `a = move(a)`, the only way both of those guarantees could be satisfied is if `a` is already equal to `nullptr`. The current standard avoids this contradiction by defining the semantics of move assignment for `std::unique_ptr` as equivalent to `reset(r.release())` which provides a stronger guarantee than any standard component implementation requires while satisfying the Standard requirements. -Supporting self-move-assignment for this narrow case imposes some additional complexity because `a = move(a)` is, in general, a contradiction and is not required by the implementation of any standard component. The implementation required postcondition of `a = move(b)` is that `a` holds the prior value of `b` and the value of `b` is unspecified, but may be guaranteed to be a specific value. For example, if `a` and `b` are both of type `my_unique_ptr` with the guarantee that `a` will hold the prior value of `b`, and `b` will be equal to `nullptr`. Then for the expression `a = move(a)`, the only way both of those guarantees could be satisfied is if `a` is already equal to `nullptr`. The current standard avoids this contradiction by defining the postcondition of move assignment for `std::unique_ptr` as equivalent to `reset(r.release())` which provides a stronger guarantee than any standard component implementation requires while satisfying the Standard requirements. + ### Non-Requirements @@ -121,9 +133,10 @@ T a[]{ v0, v1, v1, v2 }; sort(begin(a), end(a)); ``` -After `remove()`, the last two objects at the end of `a` have unspecified values and may have been moved from. There is no requirement that these moved-from objects also satisfy the requirements of `sort()` by being in the domain of the operation `operator<()`, even if `v0`, `v1`, and `v2` are within the domain. The post conditions of `remove()` and the requirements of `sort()` are independent. An invocation of `sort()` for a particular type, `T`, may or may not be valid depending on the guarantees provided by `T`. +After `remove()`, the last two objects at the end of `a` have unspecified values and may have been moved from. There is no requirement that these moved-from objects also satisfy the requirements of `sort()` by being in the domain of the operation `operator<()`, even if `v0`, `v1`, and `v2` are within the domain. The postconditions of `remove()` and the requirements of `sort()` are independent. An invocation of `sort()` for a particular type, `T`, may or may not be valid depending on the guarantees provided by `T`. Assuming `v0` and `v2` are in the domain of `operator<()` for `sort()` the following is guaranteed: + ```cpp T a[]{ v0, v1, v1, v2 }; @@ -139,16 +152,21 @@ for (std::string line; std::getline(std::cin, line);) { } ``` -For the call to `std::getline()` to be valid with a moved from string it requires that `std::string()` _guarantees_ that `std::erase()` is valid on a moved from string. Changing the requirements of a moved from object does not change the guarantees of the standard components. +For the call to `std::getline()` to be valid with a moved-from string, `std::string()` must _guarantee_ that `std::erase()` is valid on a moved-from string. Changing the requirements of a moved-from object does not change the guarantees of the standard components. ## Impact on the Standard All components which are _Movable_ in the Standard Library currently satisfy the proposed requirements as stated by both options below. **Both options are non-breaking changes and relax the requirements.** With either option, it may be possible to adopt these options retroactively as part of addressing a defect since neither option is a breaking change. + + Concern has been raised that changing the documentation for requirements, especially the named requirements and concepts, would break existing code documentation that referenced the standard. However, taking a strict view of this would mean that the standard documentation could not be changed. For example, one of the libraries I work on has a `task<>` template which is documented as being ["Similar to `std::function` except it is not copyable and supports move-only and mutable callable targets..."](https://stlab.cc/libraries/concurrency/task/task/). Of note, this goes on to specify, "`stlab::task` satisfies the requirements of [MoveConstructible](https://en.cppreference.com/w/cpp/named_req/MoveConstructible) and [MoveAssignable](https://en.cppreference.com/w/cpp/named_req/MoveAssignable)." Weakening the requirements would mean that statement is still true. However, the concern over changing the documentation is one that should be considered in light of weakening the requirements retroactively as a defect. As that would mean even citing a specific version of the standard would break. + + + ## Technical Specifications We need a general requirement regarding _the domain of an operation_. Borrowing from [the text for input iterators](http://eel.is/c++draft/iterator.cpp17#input.iterators-2): @@ -156,7 +174,7 @@ We need a general requirement regarding _the domain of an operation_. Borrowing > Unless otherwise specified, there is a general precondition for all operations that the requirements hold for values within the _domain of the operation_. {: .proposed} -> The term _domain of the operation_ is used in the ordinary mathematical sense to denote the set of values over which an operation is (required to be) defined. This set can change over time. Each component may place additional requirements on the domain of an operation. These requirements can be inferred from the uses that a component makes of the operation and is generally constrained to those values accessible through the operation's arguments. +> The term _domain of the operation_ is used in the ordinary mathematical sense to denote the set of values over which an operation is (required to be) defined. This set can change over time. Each component may place additional requirements on the domain of an operation. These requirements can be inferred from the uses that a component makes of the operation and is generally constrained to those values accessible through the operation's arguments. {: .proposed} The above wording should appear in the [Requirements section of the Library Introduction](https://eel.is/c++draft/structure.requirements#8). @@ -167,7 +185,7 @@ Given the above general requirement, we can then specify what operations must ho ### Option 1 -Option 1 requires that a moved-from object can be used as an rhs argument to move-assignment only in the case that the object has been moved from and it is a self-move-assignment. It introduces a _moved-from-value_ to discuss the properties of the moved-from object without specifying a specific value and requires that self-move-assignment for the moved-from object is valid. The wording allows for `swap(a, a)` without allowing `a = move(a)` in general. +Option 1 requires that a moved-from object can be used as an rhs argument to move-assignment only in the case of self-move-assignment. It introduces a _moved-from-value_ to discuss the properties of the moved-from object without specifying a specific value and requires that self-move-assignment for the moved-from object is valid. The wording allows for `swap(a, a)` without allowing `move(a), b = move(a)` in general.

Table 28: _Cpp17MoveConstructible_ requirements @@ -180,11 +198,11 @@ Option 1 requires that a moved-from object can be used as an rhs argument to mov `T u = rv;` - _Postconditions:_ `u` is equivalent to the value of `rv` before the construction + _Postconditions:_ `u` is equivalent to `rv` before the construction `T(rv)` - _Postconditions:_ `T(rv)` is equivalent to the value of `rv` before the construction + _Postconditions:_ `T(rv)` is equivalent to `rv` before the construction _common_ @@ -193,7 +211,7 @@ _Postconditions:_ - If `T` meets the _Cpp17Destructible_ requirements; - `rv` is in the domain of _Cpp17Destructible_ - If `T` meets the _Cpp17MoveAssignable_ requirements; - - `rv` is in the domain of the lhs argument of _Cpp17MoveAssignable_ and, + - `rv` is in the domain of the lhs argument of _Cpp17MoveAssignable_ and, - `rv` is a _moved-from-value_, such that following a subsequent operation, `t = (T&&)(rv)`, where `t` and `rv` refer to the same object, `rv` still satisfies the postconditions of _Cpp17MoveConstructible_ - If `T` meets the _Cpp17CopyAssignable_ requirements; - `rv` is in the domain of the lhs argument of _Cpp17CopyAssignable_ @@ -382,7 +400,7 @@ Note that a new component could still provide stronger guarantees than is requir ### Class invariants -Although the proposal _Support for contract based programming in C++_, [P0542R5](#p0542), did not include class invariants, it is possible that a future version of the standard will. Class invariants are a way to state postconditions that apply to all (safe) operations on a type. For move, it would be desirable if class invariants where not validated for a moved from object by default, and more generally allow the declaration of _unsafe_ operations where invariants are not checked. One possible way this could be done would be with an attribute to declare if an invariant holds for a given argument: +Although the proposal _Support for contract based programming in C++_, [P0542R5](#p0542), did not include class invariants, it is possible that a future version of the standard will. Class invariants are a way to state postconditions that apply to all (safe) operations on a type. For move, it would be desirable if class invariants where not validated for a moved-from object by default, and more generally allow the declaration of _unsafe_ operations where invariants are not checked. One possible way this could be done would be with an attribute to declare if an invariant holds for a given argument: ```cpp // unsafe operation on doubly linked list @@ -398,7 +416,7 @@ Parent, Sean. _Move Annoyance_, Addison-Wesley, 31 Mar. 2021, . -Romer, Geoffrey. “Moved-from Objects Need Not Be Valid.” _C++ Standards Committee Papers_, ISO/IEC WG21, 10 Jan. 2020, . +Romer, Geoffrey. “Moved-from Objects Need Not Be Valid.” _C++ Standards Committee Papers_, ISO/IEC WG21, 10 Jan. 2020, . {: #p2027'} G. Dos Reis et. al. "Support for contract based programming in C++." _C++ Standards Committee Papers_, ISO/IEC WG21, 08 June 2018,