C++ Logo

std-proposals

Advanced search

Relax requirements on moved from objects in standard library

From: Paul Meckel <paul_meckel_at_[hidden]>
Date: Mon, 16 Dec 2024 19:15:47 +0100
Hi.

I propose that the requirements of "moved-from" objects belonging to the
standard library are relaxed from "unspecified but valid" to
"unspecified, and potentially invalid" with "the use of their state
results in undefined behavior".

My reasoning for this proposal is this:

Moving from an object occurs in two scenarios only:

1. Moving from a temporary, in which case this doesn't apply
2. Moving explicitly, for example with std::move

In both cases, the programmer indicates that the moved-from object is no
longer important to them, and that it's resources may be used. As such
the only two valid operations on these objects should be the same two
valid operations as on uninitialized variables, as they are conceptually
the same: assignment and destruction. Anything else makes no sense, and
there is no use case.

The problem is that the c++ standard places requirements on these moved
from objects if they are types of the standard library. For example
std::vector must be in an "unspecified but valid state", where valid
means that invariants must hold for the moved from object. This in turn
means that all operations on that object have to be safe. .size() or
.push_back() need to perform as expected, even though using these
operations on a moved from object is nonsense.

This places a burden on the library implementers to ensure invariants
are not violated, and more importantly it violates the core principle of
c++ as a language: uncompromising performance, even at the cost of
safety. This is why uninitialized primitive type variables exist, even
though initializing them to a default value would prevent a lot of bugs.
c++ is about performance first, and any safety on top of that is
optional and at no cost if not used. This is why we have both operator[]
and member function .at() for std::array.

Inherently, querying the state of, or using the value of a moved from
object is semantically meaningless, as you just declared that you don't
care about it anymore. A case could be made to allow some member
functions, such as .clear() for vector, to remain valid even for moved
from objects as it effectively resets the state; but a more semantically
correct way would be reassignment of a new vector entirely.

Therefore moving from an object and leaving it in an invalid state, if
that saves performance, seems like a reasonable decision. Assigning 0 to
the size member of a vector adds only one extra instruction per move
(but so does default initializing primitive types, and that was deemed
too much also), but for std::list, this means a dynamic memory
allocation for a new sentinel node, which in turn may throw, which is
why the move operations for std::list are not noexcept, even though move
operations should be noexcept. So you did all that work, even
reallocating memory, on an object that you know that the programmer
intends to discard or reassign over. It makes no sense to perform that work.

Impact on existing code:

Code using moved-from object's state, such as relying on a vector being
empty after a move is already a logic error, and a bug, as the state of
the vector may not be empty. It is unspecified after all, even if in
practice it very often is empty.

Standard libraries do not need to implement these changes immediately,
as the current safe behavior is also valid for undefined behavior.

Correctly written code without the implicit logic error that comes with
using a moved-from object's state remain valid. Exceptions are those
which use a reset like function, such as .clear() for container types,
instead of a reassignment.


What do you think?

Best Regards

Paul

Received on 2024-12-16 18:15:51