C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Relax requirements on moved from objects in standard library

From: Jonathan Wakely <cxx_at_[hidden]>
Date: Mon, 16 Dec 2024 19:17:59 +0000
On Mon, 16 Dec 2024, 18:16 Paul Meckel via Std-Proposals, <
std-proposals_at_[hidden]> wrote:

> 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?
>

Count me very strongly against.

This goes in the opposite direction from recent changes to make e.g.
self-move-assignment valid, because there are situations where it occurs in
reasonable code, and defending against it is not practical in all cases.

There's no burden for vendors to make e.g. std::vector maintain its
invariants after a move. To destroy a std::vector at least the begin and
begin+capacity pointers must be valid, so that the destructor knows whether
there is anything to deallocate, and the way those destructors are
implemented today they also rely on the begin+size pointer being valid too.
Your suggestion wouldn't really change anything, the destructor would still
need to check for a non-zero capacity, so it would still need to be in a
valid state.

You would either need a single move+destroy operation (i.e. relocation) or
store some state in the object that tells the destructor the object is
radioactive but is already empty and so the destructor should not even
inspect the existing members.

It would also break existing code which works fine today, even if you think
it's wrong.

Received on 2024-12-16 19:19:16