Date: Wed, 21 Sep 2022 15:51:25 +0100
On Wed, 21 Sept 2022 at 11:28, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:
> Hi all,
>
> I almost want to propose a magic library function that takes a prvalue, a
>>>> list of immediate bases and pointers to data members, and returns those
>>>> subobjects as a destructurable class type. Something like:
>>>>
>>>> auto [b, x, arr] = std::decompose<B, &D::x_, &D::arr_>(reloc obj);
>>>>
>>>> It would check that the returned bases and data members are direct,
>>>> distinct, non-overlapping and relocatable, and destroy any subobjects not
>>>> mentioned. Then you would be able to return that `std::decompose` result
>>>> directly, or perform further processing it.
>>>>
>>>
>>> This looks good, but how are you able to mix type (B) and non-type
>>> (&D::x) parameters in the function declaration?
>>>
>>
>> Oh, cheating. Actually, it looks like there's some renewed momentum on
>> P1985 Universal Template Parameters, so that would help here. Otherwise the
>> non-type parameters could be passed by function argument, although that's
>> less elegant.
>>
>> One annoying thing about this is that there's no proof that the caller
>> has the right to access base B of D. We could mandate that by fiat, but
>> it'd be nicer if there were a way to denote a specific base-of-derived,
>> e.g. with the same syntax as a pointer to data member. That would be a lot
>> more work, though, so just mandating that the base be accessible and
>> unambiguous in the context of the caller might have to do.
>>
>
> I know what you mean and I don't know if that might do. I've seen and
> written code that passes the pointer to some data member as a template
> parameter to some static free function in the same TU. This is a good way
> to factorise code that performs the same set of operations on different
> data members. For instance:
>
> template <auto Member> static void Do(D& self);
> void D::do_a() { Do<&D::_a>(*this); }
> void D::do_b() { Do<&D::_b>(*this); }
>
> My point is that `Do` cannot access data members of T under normal
> circumstances, but since the address is supplied as a template parameter,
> it works. Now back to std::decompose. If done in `Do` then it could not
> access the base class of `D`, and there is no similar way to grant it
> access.
>
> That may have to do as I cannot think of something better. Besides, if at
> some point the language supports pointers to base members, we can still put
> them into std::decompose.
>
std::decompose would need to be magic (compiler-implemented), to be able to
call destructors on nonrelocated subobjects, so it would be able to bypass
access checks anyway; it might be possible to implement it in the language
with reflection once that arrives, but it's widely expected that reflection
will also allow bypassing access checks.
So the danger is actually that std::decompose would be too powerful and
allow arbitrary code to access private/protected base subobjects of
unrelated class types; but this can be prevented by requiring the compiler
to look at the context of the call to determine whether the caller is
allowed to access that base class subobject.
> If `operator T()` were prvalue qualified that would help prevent bugs,
> using deducing this on the object parameter:
> >
> > T relocating_wrapper<T>::operator T(this relocating_wrapper self) {
> return self.opt_.pop(); }
> >
> > Users would have to go to considerable lengths to misuse it and the
> resulting code would have a manifest use-after-move bug. Yes, there's still
> a double relocation but that should be elidable.
>
> That code is already okay w/t C++23 right?
>
Absolutely, that's perfectly legal C++23.
> Well, this is a good argument for `reloc` to DTRT on references: that is,
> `reloc r` should behave on reference typed variables r as
> static_cast<decltype(r)>(r) while ending the lifetime of the reference and
> removing it from scope.
>
> Indeed that looks better.
>
> > Fair enough, though I think it'd be worth mentioning in a "further
> directions" section.
>
> You make me doubt. How large would that chunk of the proposal be?
>
If it isn't mentioned in the proposal, it will be raised in discussion.
It's always worth showing what has been considered to be inside and outside
scope.
At minimum, I guess all template parameter pack functions that capture its
> parameter pack with a forwarding reference will need to be reconsidered...
> to be eventually replaced by the decltype(auto)... syntax, plus changing
> std::forward to reloc.
>
Yes, this would be a large Library upgrade effort. Mostly mechanical,
though.
> That would also open the door to the non-variadic syntax:
> vector<T>::push_back(decltype(auto) x).
>
No, because currently `push_back` isn't a template but a lvalue-xvalue
overload set. It would be beneficial to add prvalue to that overload
set: vector<T>::push_back(T
value), given that overload resolution between lvalue/xvalue/prvalue is
established as unambiguous.
And what about decltype(Concept)... syntax?
>
Spelled `Concept decltype(auto)`. This is currently legal for e.g. variable
declarations:
std::same_as<int> decltype(auto) i = 99;
std::same_as<int&> decltype(auto) r = (i);
std::same_as<int> auto j = (i);
Also, another technicality now that I am in the writing of the proposal, is
> it okay to consider reloc as a new unary operator, or does it need to be a
> keyword? I have heard here and there that new keywords are difficult to be
> approved, that's why I am asking. Personally I don't see why reloc would
> need to be a keyword.
>
Every operator token is currently a keyword; there are four identifiers
with special meaning (final, override, import and module) but they are not
operators.
The problem with having it as an operator that is not a keyword is parsing;
is `reloc x;` a discarded-value relocation expression (destroying x) or is
it the declaration of a variable `x` with type `reloc`? If `reloc` is not
allowed as an identifier, then that's basically the same as making it a
keyword.
FWIW, C has historically handled this by introducing new keywords with
_Reserved names and requiring code to opt-in to friendlier names by
#including a header that implicitly #defines the new keyword to the
_Reserved name. But that's not been workable in C++ since so much of our
code is in headers and so it would leak into code that includes those
headers. Perhaps modules would make it possible to opt-into a new keyword.
wrote:
> Hi all,
>
> I almost want to propose a magic library function that takes a prvalue, a
>>>> list of immediate bases and pointers to data members, and returns those
>>>> subobjects as a destructurable class type. Something like:
>>>>
>>>> auto [b, x, arr] = std::decompose<B, &D::x_, &D::arr_>(reloc obj);
>>>>
>>>> It would check that the returned bases and data members are direct,
>>>> distinct, non-overlapping and relocatable, and destroy any subobjects not
>>>> mentioned. Then you would be able to return that `std::decompose` result
>>>> directly, or perform further processing it.
>>>>
>>>
>>> This looks good, but how are you able to mix type (B) and non-type
>>> (&D::x) parameters in the function declaration?
>>>
>>
>> Oh, cheating. Actually, it looks like there's some renewed momentum on
>> P1985 Universal Template Parameters, so that would help here. Otherwise the
>> non-type parameters could be passed by function argument, although that's
>> less elegant.
>>
>> One annoying thing about this is that there's no proof that the caller
>> has the right to access base B of D. We could mandate that by fiat, but
>> it'd be nicer if there were a way to denote a specific base-of-derived,
>> e.g. with the same syntax as a pointer to data member. That would be a lot
>> more work, though, so just mandating that the base be accessible and
>> unambiguous in the context of the caller might have to do.
>>
>
> I know what you mean and I don't know if that might do. I've seen and
> written code that passes the pointer to some data member as a template
> parameter to some static free function in the same TU. This is a good way
> to factorise code that performs the same set of operations on different
> data members. For instance:
>
> template <auto Member> static void Do(D& self);
> void D::do_a() { Do<&D::_a>(*this); }
> void D::do_b() { Do<&D::_b>(*this); }
>
> My point is that `Do` cannot access data members of T under normal
> circumstances, but since the address is supplied as a template parameter,
> it works. Now back to std::decompose. If done in `Do` then it could not
> access the base class of `D`, and there is no similar way to grant it
> access.
>
> That may have to do as I cannot think of something better. Besides, if at
> some point the language supports pointers to base members, we can still put
> them into std::decompose.
>
std::decompose would need to be magic (compiler-implemented), to be able to
call destructors on nonrelocated subobjects, so it would be able to bypass
access checks anyway; it might be possible to implement it in the language
with reflection once that arrives, but it's widely expected that reflection
will also allow bypassing access checks.
So the danger is actually that std::decompose would be too powerful and
allow arbitrary code to access private/protected base subobjects of
unrelated class types; but this can be prevented by requiring the compiler
to look at the context of the call to determine whether the caller is
allowed to access that base class subobject.
> If `operator T()` were prvalue qualified that would help prevent bugs,
> using deducing this on the object parameter:
> >
> > T relocating_wrapper<T>::operator T(this relocating_wrapper self) {
> return self.opt_.pop(); }
> >
> > Users would have to go to considerable lengths to misuse it and the
> resulting code would have a manifest use-after-move bug. Yes, there's still
> a double relocation but that should be elidable.
>
> That code is already okay w/t C++23 right?
>
Absolutely, that's perfectly legal C++23.
> Well, this is a good argument for `reloc` to DTRT on references: that is,
> `reloc r` should behave on reference typed variables r as
> static_cast<decltype(r)>(r) while ending the lifetime of the reference and
> removing it from scope.
>
> Indeed that looks better.
>
> > Fair enough, though I think it'd be worth mentioning in a "further
> directions" section.
>
> You make me doubt. How large would that chunk of the proposal be?
>
If it isn't mentioned in the proposal, it will be raised in discussion.
It's always worth showing what has been considered to be inside and outside
scope.
At minimum, I guess all template parameter pack functions that capture its
> parameter pack with a forwarding reference will need to be reconsidered...
> to be eventually replaced by the decltype(auto)... syntax, plus changing
> std::forward to reloc.
>
Yes, this would be a large Library upgrade effort. Mostly mechanical,
though.
> That would also open the door to the non-variadic syntax:
> vector<T>::push_back(decltype(auto) x).
>
No, because currently `push_back` isn't a template but a lvalue-xvalue
overload set. It would be beneficial to add prvalue to that overload
set: vector<T>::push_back(T
value), given that overload resolution between lvalue/xvalue/prvalue is
established as unambiguous.
And what about decltype(Concept)... syntax?
>
Spelled `Concept decltype(auto)`. This is currently legal for e.g. variable
declarations:
std::same_as<int> decltype(auto) i = 99;
std::same_as<int&> decltype(auto) r = (i);
std::same_as<int> auto j = (i);
Also, another technicality now that I am in the writing of the proposal, is
> it okay to consider reloc as a new unary operator, or does it need to be a
> keyword? I have heard here and there that new keywords are difficult to be
> approved, that's why I am asking. Personally I don't see why reloc would
> need to be a keyword.
>
Every operator token is currently a keyword; there are four identifiers
with special meaning (final, override, import and module) but they are not
operators.
The problem with having it as an operator that is not a keyword is parsing;
is `reloc x;` a discarded-value relocation expression (destroying x) or is
it the declaration of a variable `x` with type `reloc`? If `reloc` is not
allowed as an identifier, then that's basically the same as making it a
keyword.
FWIW, C has historically handled this by introducing new keywords with
_Reserved names and requiring code to opt-in to friendlier names by
#including a header that implicitly #defines the new keyword to the
_Reserved name. But that's not been workable in C++ since so much of our
code is in headers and so it would leak into code that includes those
headers. Perhaps modules would make it possible to opt-into a new keyword.
Received on 2022-09-21 14:51:38