C++ Logo

std-proposals

Advanced search

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

From: Marcin Jaczewski <marcinjaczewski86_at_[hidden]>
Date: Mon, 2 May 2022 17:30:49 +0200
pon., 2 maj 2022 o 16:51 Arthur O'Dwyer via Std-Proposals
<std-proposals_at_[hidden]> napisaƂ(a):
>
> 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<Vector>);
> static_assert(valid_after_move_v<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() != fm.values().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.
>

"unspecified state" is not a property that is easy to combine.
Question is: should be even `= default` allowed for classes with
invariants?

This gives though: let's embrace this and allow partially-formed state
after move, but what will be consequences of this if applied globally?

Using any member function on partially-formed type is UB, and if you
used `[]` on an object with unspecified state ths is too UB.
But is there a difference between them? Yes, in case of the latter you
can always recover by querying `size()` to work as a guard,
In the case of the former no operation is permitted, and there is no
way to recover aside from assigning new value to it.

Right now the only place I know where this distinction could matter is
logging and serialization.

If we use the current model for move-out types you can always log to
file what values objects have, as this is log it does not matter if
sometimes some garbage will be printed out.
But if new use partially-formed logic then any way accessing type that
have some subject partially-formed make whole object partially-formed
even if that property was added only for debug prouporses and did not
take part in any object invariants.

In case of serialization this could happen if you want to make a
snapshot of the whole state and have the option to restore it after
that.


One conclusion I get from this is that always someone need do manual
cleanups, in the current model you need to do it in special functions,
in other cases by guaranteeing that partially-formed objects do not
leak (as one object like this can poison the whole class).

> 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,"
> https://www.youtube.com/watch?v=b9ZYM0d6htg
> which is a fleshed-out version of this blog post from Kona 2019:
> https://quuxplusone.github.io/blog/2019/02/24/container-adaptor-invariants/
>
>
>> [...] 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,
> Arthur
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

Received on 2022-05-02 15:31:01