Date: Fri, 4 Mar 2022 15:03:33 +0100
śr., 2 mar 2022 o 22:57 Edward Catmur <ecatmur_at_[hidden]> napisał(a):
>
> On Wed, 2 Mar 2022 at 12:33, Maciej Cencora via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>> Lol. I am not a proponent of Rust, I have never written a single line
>> of code in that language, nor did I read any book about it.
>>
>> If you think I am wrong, then please show me how this mentioned issue
>> can be solved without a language level construct?
>
>
> It can (and should) be solved in the library, as we solve many problems in the library for which other languages would need a language level construct.
Perhaps the performance part could be solved in a library, but
certainly not the usability part which is all I am arguing for.
>
> The problem with a `relocate` operator per se is that in real-world usage it can be arbitrarily complicated to determine whether a variable has been relocated from and that following uses should be rejected; we can't use linear/affine types or Rust's lifetime analysis. Instead, we should solve this in the library: use `std::optional<T>` and give it a (hypothetical; bikeshed) `T std::optional<T>::pop()` method that returns a prvalue relocated from the contained object (or moved/copied followed by destroy, for non-relocatable types), and sets the optional to disengaged. Then, identifying programs where the optional is used after it has been disengaged is a matter of QOI, and can be solved by static analysis and testing with sanitizers or wide-contract methods. And in many cases the engaged flag of the optional would be optimized out of the program binary (again, we are historically and presently happy to rely on the optimizer).
I don't really see how using std::optional<T> fixes any problem I have
mentioned. People still need to move into, so they call std::move, and
the source variable is still in a scope, with a possibility of being
misused, resulting in UB.
Why do you say that whether a variable has been relocated from is hard
to determine?
If the user wrote: "move/relocate x" then after this statement x is
relocated, period.
>
> For efficiency (trivial std::unique_ptr, std::list with allocating default ctor) and for never-empty types (gsl::not_null) we will need a way to write or (preferably) default the relocation operation on class types; this could be accomplished via (suitably) qualified (or perhaps specified) member functions, writing the relocation operation as a qualified conversion function to the same (class) type and allowing other other destructive operations on prvalues (e.g. std::unique_ptr::release) to obviate calling the destructor. Coming up with a syntax (other than `= default`, on the relocation operation itself) that is guaranteed leak-free is a bit tricky, but I'm working on it (from time to time).
>
> The relocation operation would automatically be called on return of an id-expression or similar (considered equivalent to and preferred over move-and-destroy), but there would also need to be a way for library code (such as `optional`) to invoke the relocate operation on objects whose lifetime it manages, where currently it might call the move constructor followed by the destructor. A (magic) library function `T relocate_or_move_and_destroy_at(T*)` would be sufficient for (Standard and third-party) library authors, while sufficiently off-putting to end users that they would be steered to use `std::optional`; the overconfident would be free to use `alignas(T) std::byte buf[sizeof(T)]`.
A magic library function is a language level solution (just hidden
behind a function), otherwise people would have already implemented it
in their libraries. Also again with such an API, if source object is
e.g. an automatic variable it is alive, and using it after a call to
the proposed magic function will still lead to UB. So it doesn't fix
the problems I raised.
>
> The library should similarly offer a type trait `is_trivially_relocatable` (*not* `is_relocatable`, since there would be no way for user code to invoke the relocation operation directly without fallback to move-and-destroy), allowing (user and library) code to apply all 3 of the memcpy/memmove optimizations mentioned by Arthur; `any` and `swap` would use the trait directly, and `vector` via `uninitialized_relocate_or_move_and_destroy_n`. Clearly, scalars (and arrays thereof, etc) would be trivially relocatable, as would aggregates with all trivially relocatable members; (other) user-defined special member functions would disable the relocation operation on class types, but it could be reenabled (trivially, if appropriate) with `= default` syntax.
>
> Finally, note that there is no need for a relocating assignment operator; any class for which it would be trivial will be itself trivial (assignment can't be trivial for `std::unique_ptr` because any existing resource has to be deleted); std::list doesn't care since both sides already have their sentinel node allocated; and gsl::not_null can write its assignment operator to take its argument by value and swap (the issue of delayed destruction of by-value arguments being sufficiently abstruse not to be worth worrying about). Considering Arthur's criteria, `any` and `swap` are already covered by trivial relocatability, and `vector` can't benefit since destructors would need to be called on either the source or target range anyway.
>
> On Wed, 2 Mar 2022 at 12:33, Maciej Cencora via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>> Lol. I am not a proponent of Rust, I have never written a single line
>> of code in that language, nor did I read any book about it.
>>
>> If you think I am wrong, then please show me how this mentioned issue
>> can be solved without a language level construct?
>
>
> It can (and should) be solved in the library, as we solve many problems in the library for which other languages would need a language level construct.
Perhaps the performance part could be solved in a library, but
certainly not the usability part which is all I am arguing for.
>
> The problem with a `relocate` operator per se is that in real-world usage it can be arbitrarily complicated to determine whether a variable has been relocated from and that following uses should be rejected; we can't use linear/affine types or Rust's lifetime analysis. Instead, we should solve this in the library: use `std::optional<T>` and give it a (hypothetical; bikeshed) `T std::optional<T>::pop()` method that returns a prvalue relocated from the contained object (or moved/copied followed by destroy, for non-relocatable types), and sets the optional to disengaged. Then, identifying programs where the optional is used after it has been disengaged is a matter of QOI, and can be solved by static analysis and testing with sanitizers or wide-contract methods. And in many cases the engaged flag of the optional would be optimized out of the program binary (again, we are historically and presently happy to rely on the optimizer).
I don't really see how using std::optional<T> fixes any problem I have
mentioned. People still need to move into, so they call std::move, and
the source variable is still in a scope, with a possibility of being
misused, resulting in UB.
Why do you say that whether a variable has been relocated from is hard
to determine?
If the user wrote: "move/relocate x" then after this statement x is
relocated, period.
>
> For efficiency (trivial std::unique_ptr, std::list with allocating default ctor) and for never-empty types (gsl::not_null) we will need a way to write or (preferably) default the relocation operation on class types; this could be accomplished via (suitably) qualified (or perhaps specified) member functions, writing the relocation operation as a qualified conversion function to the same (class) type and allowing other other destructive operations on prvalues (e.g. std::unique_ptr::release) to obviate calling the destructor. Coming up with a syntax (other than `= default`, on the relocation operation itself) that is guaranteed leak-free is a bit tricky, but I'm working on it (from time to time).
>
> The relocation operation would automatically be called on return of an id-expression or similar (considered equivalent to and preferred over move-and-destroy), but there would also need to be a way for library code (such as `optional`) to invoke the relocate operation on objects whose lifetime it manages, where currently it might call the move constructor followed by the destructor. A (magic) library function `T relocate_or_move_and_destroy_at(T*)` would be sufficient for (Standard and third-party) library authors, while sufficiently off-putting to end users that they would be steered to use `std::optional`; the overconfident would be free to use `alignas(T) std::byte buf[sizeof(T)]`.
A magic library function is a language level solution (just hidden
behind a function), otherwise people would have already implemented it
in their libraries. Also again with such an API, if source object is
e.g. an automatic variable it is alive, and using it after a call to
the proposed magic function will still lead to UB. So it doesn't fix
the problems I raised.
>
> The library should similarly offer a type trait `is_trivially_relocatable` (*not* `is_relocatable`, since there would be no way for user code to invoke the relocation operation directly without fallback to move-and-destroy), allowing (user and library) code to apply all 3 of the memcpy/memmove optimizations mentioned by Arthur; `any` and `swap` would use the trait directly, and `vector` via `uninitialized_relocate_or_move_and_destroy_n`. Clearly, scalars (and arrays thereof, etc) would be trivially relocatable, as would aggregates with all trivially relocatable members; (other) user-defined special member functions would disable the relocation operation on class types, but it could be reenabled (trivially, if appropriate) with `= default` syntax.
>
> Finally, note that there is no need for a relocating assignment operator; any class for which it would be trivial will be itself trivial (assignment can't be trivial for `std::unique_ptr` because any existing resource has to be deleted); std::list doesn't care since both sides already have their sentinel node allocated; and gsl::not_null can write its assignment operator to take its argument by value and swap (the issue of delayed destruction of by-value arguments being sufficiently abstruse not to be worth worrying about). Considering Arthur's criteria, `any` and `swap` are already covered by trivial relocatability, and `vector` can't benefit since destructors would need to be called on either the source or target range anyway.
Received on 2022-03-04 14:03:46