C++ Logo


Advanced search

Re: [std-proposals] Relocation in C++

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Mon, 2 May 2022 10:51:24 -0400
On Mon, May 2, 2022 at 10:01 AM Giuseppe D'Angelo via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> On 02/05/2022 14:41, Marcin Jaczewski via Std-Proposals wrote:
> > I think even there should be a type trait that could inform the user
> > that a given type has a wide or narrow contract after a move. [...]
> > require std::valid_after_move_v<T>
> Do you have any example of where this trait would actually be
> meaningful? Is there some algorithm or data structure that could do
> things "better" (for any value of better) if they knew that the type
> they're operating upon promises validity after a move?

(I haven't been following this thread closely at all for the past few
months, but I'm trying to get back up to speed now.)
+1 to Giuseppe's skepticism; and in fact this property is problematic for a
few more reasons (see below). But first the good news:

> (Note that the default of such a trait must be `false`, because
> implicitly generated move operations leave objects in partially-formed
> states. This would put some burden on users, as they have to remember to
> specialize the trait.)

The good news is that that's not *necessarily* the case. P1144
std::is_trivially_relocatable solves this problem by letting the compiler
deduce the property for a class recursively from its subobjects'
properties, in the same way as it deduces other is_trivially_fooable
traits. If Marcin's trait wasn't already problematic for other reasons (see
below), then Marcin might reasonably propose that the compiler should just
deduce it.
Certainly programmers should *not* be encouraged to specialize random type
traits out of namespace std.

Besides, is validity alone sufficient? What does it even mean? Consider
> the flat_map example from one of the sources I quoted,
> > template <typename Key, typename Value,
> > typename KeyContainer = vector<Key>,
> > typename ValueContainer = vector<Value>>
> > class flat_map
> > {
> > KeyContainer keys;
> > ValueContainer values; // these two always have same size
> > };
> [... flat_map's] move constructor cannot be simply defaulted because
> otherwise the
> source flat_map could be left partially-formed (say, if you use
> non-stdlib containers that are partially-formed on move).

(Hm, I suppose I'm just amplifying what Giuseppe ended up saying below.
Well, I've already written it... :))
The problem is even much worse than that! Even if you know that
KeyContainer and ValueContainer are individually well-formed after move,
you've still got a problem.

    class Vector; // becomes empty upon move (typical for std::vector)
    class Cxx03Vector; // keeps its old value upon move (i.e. move is
tantamount to copy)
    using FM = flat_map<Vector, Cxx03Vector>;
    static_assert(valid_after_move_v<FM>); // UH-OH!
    FM fm = {{1, 1}, {2, 2}};
    auto dummy = FM(std::move(fm));
    // now `fm` is in a bad state, because fm.keys().size() !=

In other words: `flat_map` doesn't care that its keys and values containers
are *individually* valid after move. It cares that they have *matching
behaviors* after move.
The former is the kind of yes/no answer that type traits are good at; but
it's not helpful to flat_map.
The latter would theoretically be helpful, but it's very hard to express.
Probably equivalent to the Halting Problem, since it's asking about
equivalence of *behaviors* rather than equivalence of *data*.

This issue with flat_map (and unordered_set and other invariant-preserving
containers) was the subject of my CppCon 2019 talk "Mostly Invalid:
flat_map, Exception Guarantees, and the STL,"
which is a fleshed-out version of this blog post from Kona 2019:

[...] nothing prevents MyVector<int> to reset itself to an empty container
> on
> move (valid), and MyVector<double> to reset itself to a container with
> {1.0, 2.0, 3.0} inside (valid as well). Each container is valid after
> move, but the particular _combination_ breaks flat_map's invariants.
> So, in short, validity alone is not going to be enough; you need
> validity AND some extra conditions ("the moved-from container is valid
> and empty", or maybe, generalizing, "the moved-from value is in the same
> state as a default constructed instance"), all of which will have to be
> opt-in...

This is one reason move semantics are hard to compose, but "destructive
move"/"relocation" semantics are easier. After a relocation, the source
object is destroyed, so you don't have to worry about what state it's in.
You don't get into these situations where you have to ask whether some
invariant holds between two relocated-from objects, because those objects
don't exist anymore! Any two *destroyed* objects are in compatible states,
by definition.
On the flip side, as I believe we've seen upthread, "destructive
move"/"relocation" semantics apply basically only to objects with dynamic
lifetime (so that the programmer can choose to relocate-from an object *instead
of* destroying it). Trying to shoehorn relocation into places with
non-dynamic lifetime (relocating-from static-lifetime global variables,
relocating-from arbitrary automatic-lifetime stack variables) turns out to
be very problematic.

my $.02,

Received on 2022-05-02 14:51:36