Date: Tue, 1 Feb 2022 12:00:05 +0100
Hello Gašper,
Thank you for the first reply!
I had a read of some of these proposals, but I was not entirely satisfied.
It's true my paper is lacking a comparison section, I'll add it when time
permits :)
About the maybe_destroy:
This is an annoying pattern, I admit. However, I believe it's tackled
properly in the proposal:
- Relocation should only happen with the "reloc" keyword. This is the only
safe way to do relocation (at least in the proposal :p).
- The reloc keyword can only relocate plain values. If a reference is
passed to reloc, it yields a compilation error, especially for this reason.
- One of the goals of the reloc keyword is to prevent the call to the
destructor.
- People are heavily discouraged from calling the relocation constructor
directly, especially because it does not prevent object destruction by
regular means. Exactly like how people are discouraged from calling the
destructor of an object explicitly, unless they know what they are doing.
This determent is emphasized by the use of const_cast that is required to
cast a regular reference to a relocation reference. const_cast was chosen
because of the sense of caution it conveys.
- The relocation constructor can still be called directly (given it has
public visibility) to handle complex memory management cases for
programmers that know what they are doing (for instance, std::vector
implementation?). This is similar to destructors today: although they
appear in class definition, they must not be called explicitly unless you
know what you are doing.
The use of reloc makes your code snippet safe:
bool maybe_destroy(T x) {
if (rand() % 2) { reloc x; return true; }
return false;
}
We had to remove the reference otherwise the reloc call will yield a
compilation error.
If users still want to stick with calling the relocation construction
explicitly (which is not how relocation is done in this proposal, again
relocation should only happen with the reloc operator), then
bool maybe_destroy(T& x) {
if (rand() % 2) { T{const_cast<T~>(x)}; return true; }
return false;
}
will indeed lead to the object being destructed twice (i.e. the second time
when it reaches its end of scope at the call site). However, given C++
philosophy "*protect against Murphy*, *not Machiavelli*", this is a code
written knowing it would lead to disaster, which is very similar to:
bool maybe_destroy(T& x) {
if (rand() % 2) { x.~T(); return true; }
return false;
}
which is permitted today (both snippets conditionally call a destructor on
the object).
I believe this answers your question, as the proposal provides a safe way
to do relocation via the reloc keyword. The maybe_destroy pattern will not
be encouraged by the proposal. The proposal introduces no vulnerability
that is not already there in the language. Programmers can still screw
things up but that is always the case in C++.
Sébastien Bini
On Tue, Feb 1, 2022 at 11:04 AM Gašper Ažman <gasper.azman_at_[hidden]> wrote:
> Hi Sebastien,
>
> you sure made a pretty long write-up! What I'm missing on the first
> skim-through is a thorough review of the currently published papers in the
> space and answers to the previously surfaced objections.
>
> Some of the papers in this space:
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1144r5.html
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4158.pdf
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1029r3.pdf
> http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0023r0.pdf
>
> I also couldn't find any way that your proposal handles maybe-destroy
> references. For instance, how do you handle
>
> bool maybe_destroy(T& x) {
> if (rand() % 2) { T(static_cast<T~>(x)); return true; }
> return false;
> }
>
> The compiler will still emit the destructor call for x in the caller of
> maybe_destroy - there's nothing in the interface that communicates whether
> an object has been destroyed or not, *to the compiler*. Being able to
> handle this case has been the major stumbling block for pretty much every
> proposal in this space.
>
> G
>
> On Tue, Feb 1, 2022 at 9:26 AM Sébastien Bini via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> Hello everyone,
>>
>> I've worked on a proposal in the last months to introduce relocation (a
>> destructive move) in C++. I wrote the proposal (you will find it enclosed
>> with this email) but it's quite large so I'll write a small overview here.
>>
>> Relocation allows to "move" one object into another one. It is similar to
>> move constructors, except that relocation guarantees that moved objects are
>> destructed right after and cannot be reused.
>>
>> This proposal is motivated by:
>> - the confusing and often not properly implemented "moved-from" state,
>> introduced by move constructor.
>> - relocation is simpler to implement than move semantics as the relocated
>> object can be left in a dirty invalid state.
>> - const variables cannot be moved with C++ move semantics. But they can
>> "relocated" with this proposal.
>>
>> This introduces three main additions to the language:
>>
>> 1. Relocation reference
>>
>> A new type of reference, called "relocation reference". A relocation
>> reference on T is denoted by T~. This reference is mainly introduced
>> because of the new constructor.
>>
>> 2. Relocation constructor
>>
>> We introduce a new constructor, the relocation constructor:
>> class T {
>> T(T~ other) noexcept;
>> };
>>
>> This constructor has a new feature no other C++ constructor has: *it
>> acts as a destructor with regards to its parameter* ("other" in the code
>> sample). Hence when this constructor is called, a new instance is built as
>> always, but the instance it was built from is destructed. This means that
>> the destructor of the moved instance must not be called (otherwise the
>> instance would be destructed twice).
>>
>> This constructor is usually quite straightforward to implement, you need
>> simply to copy all data-members from other into the new instance.
>>
>> For instance, implementing the relocation constructor for std::unique_ptr
>> is simple. You simply need to copy the internal pointer to the new
>> instance. *The moved instance can be left untouched, and the memory it
>> still owns won't be deleted as the moved instance destructor will not be
>> called*.
>> In fact there is so little to do that the default implementation does the
>> job:
>> unique_ptr(unique_ptr~) noexcept = default;
>>
>> 3. The reloc operator
>>
>> Lastly, we introduce a new unary operator: reloc. It will usually be used
>> like this: auto y = reloc x;
>> This relocates x into y, leaving x in a destructed state.
>> This new operator (a) handles the construction of the new instance (will
>> use the relocation constructor, the move constructor or the copy
>> constructor, picked in that order), (b) ensures the destruction of the
>> relocated instance and (c) prevents any further use in the code of the
>> relocated instance.
>>
>> Consider the following scenario:
>> const T x;
>> auto y = reloc x;
>> // std::cout << x << std::endl;
>>
>> The second line builds y from x:
>> - Case 1: the type of x provides a relocation constructor: The relocation
>> constructor is called. At the end of the expression x is considered
>> destructed because of the relocation constructor. The destructor of x will
>> not be called when its end of scope is reached.
>> - Case 2: the type of x does not provide a relocation constructor, but a
>> move or copy constructor: The move constructor is called if it exists (if
>> not the copy constructor is called) to construct y from x. reloc must
>> ensure the destruction of x, so the destructor of x is called at the end of
>> the evaluation of the second line.
>>
>> The third line attempts to reuse a variable that was relocated.
>> Uncommenting this line will raise a compile error. reloc forbids further
>> mention of a name that resolves to a relocated object.
>>
>> This is it, thank you for reading :)
>>
>> Best regards,
>> Sébastien Bini
>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>
>
Thank you for the first reply!
I had a read of some of these proposals, but I was not entirely satisfied.
It's true my paper is lacking a comparison section, I'll add it when time
permits :)
About the maybe_destroy:
This is an annoying pattern, I admit. However, I believe it's tackled
properly in the proposal:
- Relocation should only happen with the "reloc" keyword. This is the only
safe way to do relocation (at least in the proposal :p).
- The reloc keyword can only relocate plain values. If a reference is
passed to reloc, it yields a compilation error, especially for this reason.
- One of the goals of the reloc keyword is to prevent the call to the
destructor.
- People are heavily discouraged from calling the relocation constructor
directly, especially because it does not prevent object destruction by
regular means. Exactly like how people are discouraged from calling the
destructor of an object explicitly, unless they know what they are doing.
This determent is emphasized by the use of const_cast that is required to
cast a regular reference to a relocation reference. const_cast was chosen
because of the sense of caution it conveys.
- The relocation constructor can still be called directly (given it has
public visibility) to handle complex memory management cases for
programmers that know what they are doing (for instance, std::vector
implementation?). This is similar to destructors today: although they
appear in class definition, they must not be called explicitly unless you
know what you are doing.
The use of reloc makes your code snippet safe:
bool maybe_destroy(T x) {
if (rand() % 2) { reloc x; return true; }
return false;
}
We had to remove the reference otherwise the reloc call will yield a
compilation error.
If users still want to stick with calling the relocation construction
explicitly (which is not how relocation is done in this proposal, again
relocation should only happen with the reloc operator), then
bool maybe_destroy(T& x) {
if (rand() % 2) { T{const_cast<T~>(x)}; return true; }
return false;
}
will indeed lead to the object being destructed twice (i.e. the second time
when it reaches its end of scope at the call site). However, given C++
philosophy "*protect against Murphy*, *not Machiavelli*", this is a code
written knowing it would lead to disaster, which is very similar to:
bool maybe_destroy(T& x) {
if (rand() % 2) { x.~T(); return true; }
return false;
}
which is permitted today (both snippets conditionally call a destructor on
the object).
I believe this answers your question, as the proposal provides a safe way
to do relocation via the reloc keyword. The maybe_destroy pattern will not
be encouraged by the proposal. The proposal introduces no vulnerability
that is not already there in the language. Programmers can still screw
things up but that is always the case in C++.
Sébastien Bini
On Tue, Feb 1, 2022 at 11:04 AM Gašper Ažman <gasper.azman_at_[hidden]> wrote:
> Hi Sebastien,
>
> you sure made a pretty long write-up! What I'm missing on the first
> skim-through is a thorough review of the currently published papers in the
> space and answers to the previously surfaced objections.
>
> Some of the papers in this space:
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1144r5.html
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4158.pdf
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1029r3.pdf
> http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0023r0.pdf
>
> I also couldn't find any way that your proposal handles maybe-destroy
> references. For instance, how do you handle
>
> bool maybe_destroy(T& x) {
> if (rand() % 2) { T(static_cast<T~>(x)); return true; }
> return false;
> }
>
> The compiler will still emit the destructor call for x in the caller of
> maybe_destroy - there's nothing in the interface that communicates whether
> an object has been destroyed or not, *to the compiler*. Being able to
> handle this case has been the major stumbling block for pretty much every
> proposal in this space.
>
> G
>
> On Tue, Feb 1, 2022 at 9:26 AM Sébastien Bini via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> Hello everyone,
>>
>> I've worked on a proposal in the last months to introduce relocation (a
>> destructive move) in C++. I wrote the proposal (you will find it enclosed
>> with this email) but it's quite large so I'll write a small overview here.
>>
>> Relocation allows to "move" one object into another one. It is similar to
>> move constructors, except that relocation guarantees that moved objects are
>> destructed right after and cannot be reused.
>>
>> This proposal is motivated by:
>> - the confusing and often not properly implemented "moved-from" state,
>> introduced by move constructor.
>> - relocation is simpler to implement than move semantics as the relocated
>> object can be left in a dirty invalid state.
>> - const variables cannot be moved with C++ move semantics. But they can
>> "relocated" with this proposal.
>>
>> This introduces three main additions to the language:
>>
>> 1. Relocation reference
>>
>> A new type of reference, called "relocation reference". A relocation
>> reference on T is denoted by T~. This reference is mainly introduced
>> because of the new constructor.
>>
>> 2. Relocation constructor
>>
>> We introduce a new constructor, the relocation constructor:
>> class T {
>> T(T~ other) noexcept;
>> };
>>
>> This constructor has a new feature no other C++ constructor has: *it
>> acts as a destructor with regards to its parameter* ("other" in the code
>> sample). Hence when this constructor is called, a new instance is built as
>> always, but the instance it was built from is destructed. This means that
>> the destructor of the moved instance must not be called (otherwise the
>> instance would be destructed twice).
>>
>> This constructor is usually quite straightforward to implement, you need
>> simply to copy all data-members from other into the new instance.
>>
>> For instance, implementing the relocation constructor for std::unique_ptr
>> is simple. You simply need to copy the internal pointer to the new
>> instance. *The moved instance can be left untouched, and the memory it
>> still owns won't be deleted as the moved instance destructor will not be
>> called*.
>> In fact there is so little to do that the default implementation does the
>> job:
>> unique_ptr(unique_ptr~) noexcept = default;
>>
>> 3. The reloc operator
>>
>> Lastly, we introduce a new unary operator: reloc. It will usually be used
>> like this: auto y = reloc x;
>> This relocates x into y, leaving x in a destructed state.
>> This new operator (a) handles the construction of the new instance (will
>> use the relocation constructor, the move constructor or the copy
>> constructor, picked in that order), (b) ensures the destruction of the
>> relocated instance and (c) prevents any further use in the code of the
>> relocated instance.
>>
>> Consider the following scenario:
>> const T x;
>> auto y = reloc x;
>> // std::cout << x << std::endl;
>>
>> The second line builds y from x:
>> - Case 1: the type of x provides a relocation constructor: The relocation
>> constructor is called. At the end of the expression x is considered
>> destructed because of the relocation constructor. The destructor of x will
>> not be called when its end of scope is reached.
>> - Case 2: the type of x does not provide a relocation constructor, but a
>> move or copy constructor: The move constructor is called if it exists (if
>> not the copy constructor is called) to construct y from x. reloc must
>> ensure the destruction of x, so the destructor of x is called at the end of
>> the evaluation of the second line.
>>
>> The third line attempts to reuse a variable that was relocated.
>> Uncommenting this line will raise a compile error. reloc forbids further
>> mention of a name that resolves to a relocated object.
>>
>> This is it, thank you for reading :)
>>
>> Best regards,
>> Sébastien Bini
>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>
>
Received on 2022-02-01 11:00:18