C++ Logo

std-proposals

Advanced search

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

From: Giuseppe D'Angelo <giuseppe.dangelo_at_[hidden]>
Date: Mon, 2 May 2022 16:01:11 +0200
Hello,

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.
> Probably it can't be compiler provided but class creator could
> specialize it to signal others how this type should be used.
>
> This is important as generic libraries would like to have a wide
> contract even if the user given type does not have it.
> With this trait it easy to "fix" given type:
>
> ```
> if constexpr (std::invalid_after_move_v<T>) t = {};
> ```
>
> or if can't work in some contexts:
>
> ```
> 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?

(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.)

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
> };

If this class wants "valid but unspecified" after a move, this forces
this class to have a user-defined move constructor:

> flat_map(flat_map &&other) noexcept(~~~)
> : keys(std::move(other.keys)),
> values(std::move(other.values))
> {
> other.keys = KeyContainer(); // or equivalently: keys(std::exchange(other.keys, {})), keys(std::take(other.keys)) (P2226), other.keys.clear(), ...
> other.values = ValueContainer();
> }

This 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).

With the trait above, could you do

> flat_map(flat_map &&other)
> requires (valid_after_move_v<KeyContainer>
> && valid_after_move_v<ValueContainer>)
> = default;

?

I'm inclined to say no, because again with something like

   flat_map<int, double, MyVector<int>, MyVector<double>>

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


My 2 c,
-- 
Giuseppe D'Angelo

Received on 2022-05-02 14:01:14