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.
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,
Arthur