Document Number: D2345R1
Date: 2021-04-02
Reply-to: Sean Parent, sean.parent@stlab.cc
Audience: LWG & LEWG

Table of Contents

Introduction

The C++ Standard Library requirements are overly restrictive regarding the state of a moved-from object. The strong requirements impose an unnecessary burden on implementers and imposes a performance impact of user-defined operations.

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.

The paper by Geoffrey Romer, P2027R0, was brought to my attention along with the resulting discussion. I’m in the process of reviewing that body of work, and will comment more once I have had a chance to understand the approach and objections to that paper.

Document Conventions

This is the proposed wording for the standard. There may be more than one proposed variant for the same section.

This is a quote from an existing document.

This is a comment or work in progress.

This document discusses requirements in multiple different contexts. The following terms are used when the meaning is otherwise ambiguous. The Standard requirements refer to the current, C++20, documented requirements. Implementation requirements refers to the actual requirements necessary to implement the library components. This may be weaker than the stated requirements. Finally there are the proposed requirements, this is the proposed wording to bring the Standard requirements more inline with the implementation requirements.

Motivation and Scope

Given an object, rv, which has been moved from, the C++201 Standard specifies the required postconditions of a moved-from object:

rv’s state is unspecified
[Note: rv must still meet the requirements of the library component that is using it. The operations listed in those requirements must work as specified whether rv has been moved from or not. — end note] — Table 28, p. 488 C++20 Standard.

The Standard requirement applies to both Cpp17MoveConstructible and Cpp17MoveAssignable. The note is not normative but does clarify that the requirements on a moved-from object are not relaxed.

In general, unless move is specified to make a copy, the Standard requirement is not achievable. For example, the sorting algorithms require comp(*i, *j) induce a strict weak ordering. Therefore, a moved-from object must be ordered with respect to every other value in the sequence with an arbitrary user-supplied comparison function. Only a value within the initial sequence could satisfy that requirement.

No implementation of the Standard Library will ever invoke a comparison, user-defined or otherwise, on an object that a library component itself moved-from during the course of the same operation. Such a comparison would not have meaning. However, the way the Standard requirement is frequently taught is that all operations required by the Standard Library must be total when used with a moved-from object. i.e. rv < a must be valid and induce a strict weak ordering for all possible values of a. However, even such a strong guarantee does not solve the issue for arbitrary operations passed to the Standard Library.

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.

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:

  • mf.~() (The language also requires this for implicitly moved objects)
  • mf = a
  • mf = move(a)
  • mf = move(mf)

The last implementation requirement only appears in std::swap() when invoked as swap(a, a). It 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<T> 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<T> as equivalent to reset(r.release()) which provides a stronger guarantees than any standard component implementation requires while satisfying the Standard requirements.

Non-Requirements

There is not a standard requirement to provide guarantees across operations that result in moved-from objects. For example:

1
2
3
T a[]{ v0, v1, v1, v2 };
(void)remove(begin(a), end(a), v1);
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.

Assuming v0 and v2 are in the domain of operator<() for sort() the following is guaranteed:

1
2
3
T a[]{ v0, v1, v1, v2 };
auto p = remove(begin(a), end(a), v1);
sort(begin(a), p);

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.

Technical Specifications

We need a general requirement regarding the domain of an operation. Borrowing from the text for input iterators:

Unless otherwise specified, there is a general precondition for all operations that the requirements hold for values within the domain of the operation.

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 above wording should appear in the Requirements section of the Library Introduction.

Given the above general requirement, we can then specify what operations must hold for a moved-from object.

A separate issue is if new library components going forward should make stronger guarantees than those required as they currently do.

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.

Table 28: Cpp17MoveConstructible requirements

Expression Assertion/note
pre-/post-condition
T u = rv; Postconditions: u is equivalent to the value of rv before the construction
T(rv) Postconditions: T(rv) is equivalent to the value of rv before the construction
common

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 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
  • The value of rv is otherwise unspecified

Table 28: Cpp17MoveAssignable requirements

Expression Return type Return value Assertion/note
pre-/post-condition
t = rv T& t

Preconditions: t and rv do not refer to the same object, or the object is a moved-from-value (see Cpp17MoveConstructible)

Postconditions:

  • If t and rv do not refer to the same object, t is equivalent to the value of rv before the assignment, otherwise the value of t is unspecified
  • If T meets the Cpp17Destructible requirements;
    • rv is in the domain of Cpp17Destructible
  • rv is in the domain of the lhs argument of Cpp17MoveAssignable
  • If rv meets the Cpp17CopyAssignable requirements;
    • rv is in the domain of the lhs argument of Cpp17CopyAssignable
  • The value of rv is otherwise unspecified

Option 2

Option 2 requires that a moved-from object can be used as an rhs argument to move-assignment always and the result of self-move-assignment is unspecified.

Table 28: Cpp17MoveConstructible requirements

Expression Assertion/note
pre-/post-condition
T u = rv; Postconditions: u is equivalent to the value of rv before the construction
T(rv) Postconditions: T(rv) is equivalent to the value of rv before the construction
common

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 Cpp17MoveAssignable
  • If T meets the Cpp17CopyAssignable requirements;
    • rv is in the domain of the lhs argument of Cpp17CopyAssignable
  • The value of rv is otherwise unspecified

Table 28: Cpp17MoveAssignable requirements

Expression Return type Return value Assertion/note
pre-/post-condition
t = rv T& t

Postconditions:

  • If t and rv do not refer to the same object, t is equivalent to the value of rv before the assignment, otherwise the value of t is unspecified
  • rv is in the domain of Cpp17MoveAssignable
  • If T meets the Cpp17Destructible requirements;
    • rv is in the domain of Cpp17Destructible
  • If rv meets the Cpp17CopyAssignable requirements;
    • rv is in the domain of the lhs argument of Cpp17CopyAssignable
  • The value of rv is otherwise unspecified

References

Sutter, Herb. “Move, Simply.” Sutter’s Mill, 21 Feb. 2020, herbsutter.com/2020/02/17/move-simply/.

Several links contained within the document. They will be listed here in a future draft along with any existing papers that come to my attention.

Acknowledgements

Thanks to Howard Hinnant, Herb Sutter, Jonathan Wakely, Nicolai Josuttis, Nicholas DeMarco, Eric Neibler, Dave Abrahams, and John Lakos for the many discussions and arguments that resulted in this paper.

  1. Similar wording with the same intent appears in every version of the C++ Standard since C++11.