You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
How do you forward a non-last-use argument? It has to be done when destructuring aggregates. std::apply does this variadically.
#include<string>voidg(auto&& x);
structpair_t {
std::string x, y;
};
f: (forward pair : pair_t) = {
g(pair.x); // Not a forward. How do I forward?g(pair.y); // OK
}
The grammar already allows for move annotations in calls (e.g., g(move pair.x)), perhaps we also have to support forward annotations as in g(forward pair.x)?
What is the precedence on those? I think forward may have to be a postfix operator to attach at higher precedence. It needs to work on the parameter pair, not the data member .x.
Ah, that does not work then. These call annotations are soft keywords of the call site (expression_list in the parser), not part of the expression hierarchy. Then you have to explicitly use something similar to std::forward(pair).
I don't think you can use std::forward or any library call, as the parameter directive hides from you the true type of the parameter. You name the parameter and you just get an lvalue.
Why support forward at all? I thought auto in, auto inout and auto out were there to let us forget about &, &&, const etc. It should just do the right thing automatically.
That's not argument deduction, that's just changing the value category of an argument.
So by argument deduction you also mean deducing constness and whether it's a ref or a value?
I may be too inexperienced to know when this is necessary, can you give an example?
Anyways, definite last use is not a sufficient treatment for these semantics, as this ticket shows.
It could be, if we change the granularity to fields instead of whole objects.
Anyways, definite last use is not a sufficient treatment for these semantics, as this ticket shows.
It could be, if we change the granularity to fields instead of whole objects.
Correct me if I'm wrong, but I don't think fine-grain semantics for forward parameters makes sense, depending on the design goals of the language. Let me clarify:
According to cpp core guideline F.19, any forward parameter object used more than or less than exactly once in any static code path should be brought to the attention of the human in charge. (these usages should be "flag[ged]" as they put it)
If we are to enforce guideline F.19, it seems like using the forward parameter annotation syntax for this use case could work if forward parameters have two distinct interpretations depending on usage -- so that on any static code path, either:
the whole forward parameter object is used exactly once
all subobjects of the forward parameter object are used exactly once (and the forward parameter object is not used anywhere for other purposes than for subobject access (i.e. pair.x))
This feels like we're over-saturating forward parameter annotations semantics with more than what they are intended to be used for (i.e. we are putting single object forwarding and aggregate subobjects forwarding in the same bucket). Unless we resort to verifying that every subobject of a forward parameter (xor the parameter object itself) is used exactly once on every static code path to ensure mutually exclusive subobject forwarding, I think using a forward parameter annotation in this way should be a compile-time error. And even if we implemented it like that, it seems to me that using fine-grain semantics for forward parameter objects in this way would make the language harder to teach and reason about.
Looking into how std::apply is implemented, its std::tuple parameter has a variadic template argument list. The type of each element of the tuple knows its own value category. When called, all tuple members are forwarded to (and in-place, at the call site of) std::invoke, which takes variadic template forward parameters.
I don't know how frequently aggregate subobjects forwarding should be used in c++ for any purpose, or whether there are better alternatives in general (for example, by calling standard functions that hide the use of such forwarding instead of defining your own).
If this kind of forwarding is a narrow feature for users in general, then (in light of how std::tuple carries the value category for each of its element types) I'd be inclined to prefer being able to wrap aggregate subobjects into something like a template<typename A> struct cpp2::forward type.
The point of cpp2::forward here would be to encode forward semantics into the type system, similarly to how value categories are an integral part of type signatures. For an incomplete example:
namespacecpp2 {
template<typename A>
classforward {
A _a;
public:forward(auto&& a):
_a(std::forward<A>(a))
{}
... // (deleted) constructors and operator= omitted~forward() {
... // ensure parameter was consumed exactly once here?// iiuc this can and should be done at compile time (not necessarily implemented here)
}
A&& get() {
return std::forward<A>(_a);
}
... // more members if needed
};
forward(auto&& a) -> forward<decltype(a)>;
}
And then:
#include<cpp2util.h>
#include<string>voidg(auto&& x);
structpair_t {
cpp2::forward<std::string> x, y;
};
f: (pair : pair_t) = {
g(pair.x); // forwardsg(pair.y); // and forwards too!g(pair); // forwards as a copy in this case (pair is implicitly annotated with "in")// if we are to enforce guideline F.19, this code should not be allowed// to compile. Ideally, the constructor of `cpp2::forward` doesn't allow// an object to be forwarded more than once. Since `pair` is an in parameter,// it should use the copy constructors of `x` and `y`, which should in turn// attempt to forward. Since both `x` and `y` have already been forwarded,// according to guideline F.19, this should result in a compile-time error
}
My intention here is that the code generator would take care of calling .get() on the cpp2::forward objects. The name forward is just used as an example handle here to convey my intent. It could also be implemented using syntax instead of just being a helper class, but as I said above (in the case where the current parameter annotation facilities are reused without altering the syntax in any way) I'm not convinced this is not working against the design goals of the language.
As a side note, of course my point of view is highly dependent on whether guideline F.19 is intended to be strictly enforced at compile time.
I think @ljleb said it all. I don't currently aim to allow individual forwarding of members in separate non-fold expressions, so I'll close this. If there is demand for it we can always revisit it in the future. Thanks!
@ljleb is wrong about how tuple works and is misreading cpp core guideline F.19 (which is also broken). You have to forward the function parameter, not a subobject of the function parameter, using the forwarding template parameter. The std::apply implementation is clear on this.
The parameter __t is forwarded one time for each element, and then get extracts the member, keeping the same value category. The Cpp2 translator is generating broken code because the forward directive semantics are wrong.
For the C++ Core Guidelines: We'd love to know if F.19 is broken... can you please open an issue in the Guidelines repo to tell us how (and if possible suggest an improvement)? Thanks!
For paper d0708: I'd like to fix bugs in d0708... if the explanation of my intent below doesn't address your concerns, please let me know the forward language that you think isn't correct, and how the example is broken. Thanks!
For Cpp2/cppfront:
By default, the above code gen is what I intend for forward parameters. Given the two calls to discard in that code, only the last is a definite last use and so forwarded (by default). That's visible and clear for programmers to reason about. (Note that if instead of two separate function calls/uses this were rewritten as a fold-expression to expand the tuple value, then that would be a single last use and should effectively forward all the pieces. Cppfront doesn't yet support fold-expressions as I haven't considered them a priority but eventually it will.)
To opt-into a non-default, I've been considering forward expressions, as we have out and move expressions. I've done a simple enablement of that in the above commit 41673ef . With that, calling these two functions with an rvalue argument (and assuming discard takes a copy) results in the first default/implicit forwarding function copying from t.first and moving from t.second, and the second explicit forwarding function moving from both t.first and t.second:
// invoking each of these with an rvalue std::pair argument ...
apply_implicit_forward: (forward t: std::pair<X, X>) = {
copy_from(t.first); // copies
copy_from(t.second); // moves
}
apply_explicit_forward: (forward t: std::pair<X, X>) = {
copy_from(forward t.first); // moves
copy_from(forward t.second); // moves
}
The second function is new functionality that needs this commit, it wouldn't compile before.
BTW, I guess your final comment turned this from a wontfix to actually supporting the example, well played! :)
Your question at top was:
f: (forward pair : pair_t) = {
g(pair.x); // Not a forward. How do I forward?
g(pair.y); // OK
}
The above is still the default. To opt into to forwarding both pieces, the answer in this commit would be to forward the first (and it doesn't hurt to do the second too if you like):
apply_explicit_forward is broken when you pass an lvalue, because that gets forwarded to discard as an xvalue. This is an illegal use of decltype--CPP2_FOWARD(t.first) will strip the reference and discard then reference collapses && back on. You have to forward the parameter, and then do member-access. The CPP2_FORWARD macro is a footgun and should not be used because it compiles illegal uses like this. https://godbolt.org/z/5f3P6WEnc shows the successful rvalue case and broken lvalue case.
The forward operator needs to be highest precedence--higher than postfix-expression, even though it's where a unary-expression would go. It needs to apply to the parameter, not to a member-access of the parameter. Both subobjects should be forwarded like:
(forward t).first
(forward t).second.
Right now it's like:
forward (t.first)
(forward t).second. (due to last-definite-use)
See also the commit comments re grammar. I'm currently running with out, move, and forward all being an adjective on the entire argument (grammatically), though the implementation of forward applies std::forward specifically to the forwarded parameter... that's enough to get experience with the experiment, and then if experience shows it's used and the difference matters then I could parse forward differently. Thanks!
@JohelEGP True, and Barry is a source of good ideas. For Cpp2 though, one of my stakes in the ground is that declaration syntax is consistent with use syntax, so for Cpp2 I'd want to change the syntax in both places (declaration and use) or neither rather than introduce an asymmetry there.
Activity
neumannt commentedon Oct 17, 2022
The grammar already allows for
moveannotations in calls (e.g.,g(move pair.x)), perhaps we also have to supportforwardannotations as ing(forward pair.x)?seanbaxter commentedon Oct 17, 2022
What is the precedence on those? I think
forwardmay have to be a postfix operator to attach at higher precedence. It needs to work on the parameterpair, not the data member.x.neumannt commentedon Oct 17, 2022
Ah, that does not work then. These call annotations are soft keywords of the call site (
expression_listin the parser), not part of the expression hierarchy. Then you have to explicitly use something similar tostd::forward(pair).seanbaxter commentedon Oct 17, 2022
I don't think you can use
std::forwardor any library call, as the parameter directive hides from you the true type of the parameter. You name the parameter and you just get an lvalue.feature-engineer commentedon Oct 18, 2022
Why support forward at all? I thought
auto in,auto inoutandauto outwere there to let us forget about&,&&,constetc. It should just do the right thing automatically.seanbaxter commentedon Oct 18, 2022
Same reason it exists in C++, so you don't have to write the same function twice (for each forwarded parameter).
feature-engineer commentedon Oct 19, 2022
I may be missing something.
copyalready automatically deduces l-value and r-value,incould do it too, when would you need forward?seanbaxter commentedon Oct 19, 2022
copydoesn't deduce anything. It's just pass-by-value.feature-engineer commentedon Oct 23, 2022
Not according to the talk Herb gave. When there's a definite last use of a variable, it's passed as r-value as a copy argument. Watch at 1:27.
seanbaxter commentedon Oct 23, 2022
That's not argument deduction, that's just changing the value category of an argument.
Anyways, definite last use is not a sufficient treatment for these semantics, as this ticket shows.
feature-engineer commentedon Oct 24, 2022
So by argument deduction you also mean deducing constness and whether it's a ref or a value?
I may be too inexperienced to know when this is necessary, can you give an example?
It could be, if we change the granularity to fields instead of whole objects.
ljleb commentedon Nov 13, 2022
Correct me if I'm wrong, but I don't think fine-grain semantics for forward parameters makes sense, depending on the design goals of the language. Let me clarify:
According to cpp core guideline F.19, any forward parameter object used more than or less than exactly once in any static code path should be brought to the attention of the human in charge. (these usages should be "flag[ged]" as they put it)
If we are to enforce guideline F.19, it seems like using the
forwardparameter annotation syntax for this use case could work if forward parameters have two distinct interpretations depending on usage -- so that on any static code path, either:pair.x))This feels like we're over-saturating forward parameter annotations semantics with more than what they are intended to be used for (i.e. we are putting single object forwarding and aggregate subobjects forwarding in the same bucket). Unless we resort to verifying that every subobject of a forward parameter (xor the parameter object itself) is used exactly once on every static code path to ensure mutually exclusive subobject forwarding, I think using a forward parameter annotation in this way should be a compile-time error. And even if we implemented it like that, it seems to me that using fine-grain semantics for forward parameter objects in this way would make the language harder to teach and reason about.
Looking into how
std::applyis implemented, itsstd::tupleparameter has a variadic template argument list. The type of each element of the tuple knows its own value category. When called, all tuple members are forwarded to (and in-place, at the call site of)std::invoke, which takes variadic template forward parameters.I don't know how frequently aggregate subobjects forwarding should be used in c++ for any purpose, or whether there are better alternatives in general (for example, by calling standard functions that hide the use of such forwarding instead of defining your own).
If this kind of forwarding is a narrow feature for users in general, then (in light of how std::tuple carries the value category for each of its element types) I'd be inclined to prefer being able to wrap aggregate subobjects into something like a
template<typename A> struct cpp2::forwardtype.The point of
cpp2::forwardhere would be to encode forward semantics into the type system, similarly to how value categories are an integral part of type signatures. For an incomplete example:And then:
My intention here is that the code generator would take care of calling
.get()on thecpp2::forwardobjects. The nameforwardis just used as an example handle here to convey my intent. It could also be implemented using syntax instead of just being a helper class, but as I said above (in the case where the current parameter annotation facilities are reused without altering the syntax in any way) I'm not convinced this is not working against the design goals of the language.As a side note, of course my point of view is highly dependent on whether guideline F.19 is intended to be strictly enforced at compile time.
hsutter commentedon Dec 18, 2022
I think @ljleb said it all. I don't currently aim to allow individual forwarding of members in separate non-fold expressions, so I'll close this. If there is demand for it we can always revisit it in the future. Thanks!
seanbaxter commentedon Dec 18, 2022
@ljleb is wrong about how tuple works and is misreading cpp core guideline F.19 (which is also broken). You have to forward the function parameter, not a subobject of the function parameter, using the forwarding template parameter. The
std::applyimplementation is clear on this.The parameter
__tis forwarded one time for each element, and thengetextracts the member, keeping the same value category. The Cpp2 translator is generating broken code because theforwarddirective semantics are wrong.https://godbolt.org/z/d46n8EMrs
The forward language in D0708, which indicates definite last use, is not correct.
Try out explicit `forward` expressions, see #77 comment thread
hsutter commentedon Dec 19, 2022
@seanbaxter Thanks for the notes! Some thoughts:
For the C++ Core Guidelines: We'd love to know if F.19 is broken... can you please open an issue in the Guidelines repo to tell us how (and if possible suggest an improvement)? Thanks!
For paper d0708: I'd like to fix bugs in d0708... if the explanation of my intent below doesn't address your concerns, please let me know the forward language that you think isn't correct, and how the example is broken. Thanks!
For Cpp2/cppfront:
By default, the above code gen is what I intend for
forwardparameters. Given the two calls todiscardin that code, only the last is a definite last use and so forwarded (by default). That's visible and clear for programmers to reason about. (Note that if instead of two separate function calls/uses this were rewritten as a fold-expression to expand the tuple value, then that would be a single last use and should effectively forward all the pieces. Cppfront doesn't yet support fold-expressions as I haven't considered them a priority but eventually it will.)To opt-into a non-default, I've been considering
forwardexpressions, as we haveoutandmoveexpressions. I've done a simple enablement of that in the above commit 41673ef . With that, calling these two functions with an rvalue argument (and assumingdiscardtakes a copy) results in the first default/implicit forwarding function copying fromt.firstand moving fromt.second, and the second explicit forwarding function moving from botht.firstandt.second:The second function is new functionality that needs this commit, it wouldn't compile before.
See the new regression test
mixed-forwarding.cpp2for a full working example.Bug reports and constructive feedback are always welcome! Thanks.
hsutter commentedon Dec 19, 2022
BTW, I guess your final comment turned this from a
wontfixto actually supporting the example, well played! :)Your question at top was:
The above is still the default. To opt into to forwarding both pieces, the answer in this commit would be to
forwardthe first (and it doesn't hurt to do the second too if you like):At least that's the direction I had in mind, that this commit starts to explore. Thanks for the example!
seanbaxter commentedon Dec 19, 2022
Now it's broken in a different way. 41673ef translates:
apply_explicit_forward: (forward t: std::pair<X, X>) = { discard(forward t.first); discard(forward t.second); }into
apply_explicit_forward is broken when you pass an lvalue, because that gets forwarded to discard as an xvalue. This is an illegal use of decltype--CPP2_FOWARD(t.first) will strip the reference and
discardthen reference collapses&&back on. You have to forward the parameter, and then do member-access. The CPP2_FORWARD macro is a footgun and should not be used because it compiles illegal uses like this.https://godbolt.org/z/5f3P6WEnc shows the successful rvalue case and broken lvalue case.
The
forwardoperator needs to be highest precedence--higher than postfix-expression, even though it's where a unary-expression would go. It needs to apply to the parameter, not to a member-access of the parameter. Both subobjects should be forwarded like:(forward t).first
(forward t).second.
Right now it's like:
forward (t.first)
(forward t).second. (due to last-definite-use)
JohelEGP commentedon Dec 19, 2022
An alternative to precedence is the forwarding operator,
t>>.first, which might be more in-line with the postfix operator philosophy of Cpp2.Try out explicit `forward` expressions, see hsutter#77 comment thread
hsutter commentedon Dec 21, 2022
Agreed, commit c39a94c should address this. Now it translates
to
See also the commit comments re grammar. I'm currently running with
out,move, andforwardall being an adjective on the entire argument (grammatically), though the implementation offorwardappliesstd::forwardspecifically to the forwarded parameter... that's enough to get experience with the experiment, and then if experience shows it's used and the difference matters then I could parseforwarddifferently. Thanks!@JohelEGP True, and Barry is a source of good ideas. For Cpp2 though, one of my stakes in the ground is that declaration syntax is consistent with use syntax, so for Cpp2 I'd want to change the syntax in both places (declaration and use) or neither rather than introduce an asymmetry there.
Try out explicit `forward` expressions, see hsutter#77 comment thread