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
{{ message }}
Repository navigation
[SUGGESTION] Named return values can possibly make out parameters redundant #540
The callee must treat the memory location as uninitialized, and must initialize it before the function returns.
This is a very useful pattern, but notably, this is very similar to how return values already work in most ABIs. (In most ABIs, whenever a return type requires more than a few registers, the caller must provide a pointer to the memory location that the return value should be written to.)
Consequently, I propose making a slight tweak to out parameters to unify them with the notion of return values. The end result would be that cpp2 has one fewer concept, without sacrificing expressive power or performance. In fact, it will likely make cpp2 programs more performant, because the proposed solution implies NRVO.
out parameters are moved to the right side of the ->. In other words, they are treated as part of the return type.
The caller uses the syntax s1, s2 = foo(), instead of foo(s1, s2).
But notably, the compilation strategy remains the same:
Small return values are returned via registers.
Large return values are written directly to a memory location provided by the caller.
Basically, what we end up with is an intuitive syntax for guaranteed NRVO. Accordingly, it should be possible to return immovable types (e.g. std::atomic) via this approach. (Because in the style of C++17, we're not "optimizing away" a move. Instead, we're saying that no moves are required.) Ultimately, this is equivalent to out parameters—all we've done is change the syntax.
The likely benefits of this approach include:
Better composability. With return values, it is possible to nest function calls (e.g. g(h(...), j(...))), but with out parameters (where functions return void) it is not.
Fewer concepts. We wouldn't need out parameters. Instead, we would just need to allow return values to be given names. (Indeed, cpp2 already has syntax for this.)
C++ would finally have the equivalent of "guaranteed NRVO"—a useful performance optimization.
So what would be left of your suggestion is: "remove out parameters to reduce concept count".
But I don't know if that's desirable, given that out represents "out parameters" (F.21).
Named (multiple) return values can't replace the "Exception" in F.21,
but out parameters can, so I doubt it's worth removing them.
EDIT: Actually, the above can be done with inout parameters.
Actually, I don't think the current implementation of named return values can guarantee copy elision.
IIRC, it essentially lowers the named return values like out parameters, but on the callee.
So it really does assign to local variables, and doesn't return a prvalue.
Herb has previously stated his intention to support named parameters.
It might make sense to wait until then to be able to support copy elision on named return values.
Although that would still require reworking the current named return values feature.
Yeah, the big difference between this proposal and the current semantics for named return values in cpp2 is that in the latter case, the return variables are not interpreted as references to a memory location provided by the caller. This is a missed opportunity, IMO.
But once you make that adjustment, out parameters seemingly become redundant. (Which is a good thing if true, because it would constitute a simplification.)
Thanks! Note that Cpp2 does have two different features here (both of which Cpp1 also has, but here they're generalized with more language support and the ability to declare intent):
out parameters, which can accept an initialized or uninitialized argument (not just an unione). For an uninitialized argument, any function with an out argument acts as another (delegating) constructor. For an initialized argument, it assigns over the existing value.
Return values, which always produce a new value. These are always initialized by the callee, and always returned by value (if there are multiple return values they are in a generated struct returned by value).
They are similar but do have different use cases. I agree that more often you would just use return values, but out parameters are also useful and cover cases return values don't.
Doing more for copy elision in the implementation of multiple return values is interesting though, good suggestion.
Your comment seems to mostly focus on describing how out parameters and return values work currently, and that description is consistent with my understanding. But you don't seem to have provided any reasoning as to why the two constructs cannot be merged into one.
Do you not believe it is possible to merge the two constructs into one, as I have proposed? It looks very possible to me.
I don't see either what the difference in use cases is.
In the caller I can assign the return value of a function to either an initialized variable or an uninitialized one, same as I can pass either as an out argument.
In the callee, the body has to produce a new value for each out parameter, same as for the returned one.
I believe the important aspect is that named return values have a fixed spot in memory, their scope starts and ends within the scope of the function call.
Out parameters may be declared in another scope, and initialised with a function call within a smaller scope.
On 14 July 2023 00:54:50 Jorge Canizales ***@***.***> wrote:
I don't see either what the difference in use cases is.
In the caller I can assign the return value of a function to either an initialized variable or an uninitialized one, same as I can pass either as an out argument.
In the callee, the body has to produce a new value for each out parameter, same as for the returned one.
—
Reply to this email directly, view it on GitHub<#540 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AALUZQNN7VP42YIGGDBTKKTXQCDEPANCNFSM6AAAAAA2IHSZYA>.
You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
Surely the stack location where the out parameter is declared can be in a longer lived scope than the L value created by the return. Won't this affect where the stack portion of the values memory exists, can't this prevent potential move or copy assignments?
On 14 July 2023 08:28:09 Nick Smith ***@***.***> wrote:
named return values [...] their scope starts and ends within the scope of the function call. Out parameters may be declared in another scope.
I think you're mixing up the notions of variables and memory locations:
* The scope of both NRVs and out parameters (the variables) is that of the callee's function body.
* With guaranteed NRVO, the memory locations that NRVs and out parameters refer to are provided by the caller.
So there is no distinction there.
—
Reply to this email directly, view it on GitHub<#540 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AALUZQJVMVZIU7G4ZIK7DFDXQDYIDANCNFSM6AAAAAA2IHSZYA>.
You are receiving this because you commented.Message ID: ***@***.***>
I'm not sure exactly what you're asking, but I can give you a blanket answer: a function that declares a named return value will behave exactly how a function that declares an out parameter will behave.
In my original post, I mentioned that what I am proposing is essentially just to adjust the syntax of out parameters such that they look like (and compose like) return values.
So by definition, the two features will behave the same.
I'm not sure what you mean. C++1 doesn't have out parameters, it just has references. (References can be used to simulate out parameters, but the compiler can't tell the difference.) You can pass a reference to a C++1 function call the same as always.
outerScopeString: string; // uninitialised
{
stringInitFunc(outerScopeString); // via out param
}
//now have an initialised string in another scope with no move or copy
Vs
{
lValueString = stringInitFunc()
// must move or copy this string elsewhere or it will go out of scope and destruct
}
These are different capabilities that maybe important in non-trivial use cases
On 14 July 2023 08:45:51 Nick Smith ***@***.***> wrote:
I'm not sure exactly what you're asking, but I can give you a blanket answer: a function that declares a named return value will behave exactly how a function that declares an out parameter will behave.
In my original post, I mentioned that what I am proposing is essentially just to adjust the syntax of out parameters such that they look like (and compose like) return values.
So by definition, the two features will behave the same.
—
Reply to this email directly, view it on GitHub<#540 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AALUZQP52F5Q3KZLEGXUY73XQD2KZANCNFSM6AAAAAA2IHSZYA>.
You are receiving this because you commented.Message ID: ***@***.***>
@SebastianTroy Irrespective of whether the function accepts an out parameter or returns a value, you can store the result in a variable declared in an outer scope:
outerScopeString: string; // uninitialised
{
outerScopeString = stringInitFunc(); // via named return value
}
With NRVO, this doesn't require any copying or moving. So it behaves exactly the same as an out parameter.
Out and inout (not referring to cppfront) parameters were born out of legacy requirements as far as I can tell. The problems out parameters are a solution for are much better solved with new features; structured bindings for multiple returns (and of course cppfront's solution for multiple returns) and return value optimization.
All these call sites work today, and I assume we'd want them to continue working:
x: widget;
x = cpp1_func();
y: widget;
y = cpp2_func();
x = cpp2_func();
y = cpp1_func();
I think the suggestion is to change the Cpp1 code gen for cpp2_func to emit Cpp1 auto cpp2_func( widget& ) (or similarly widget*, same idea). Then when calling it, we would have to transform the call sites to make the Cpp1 code gen for the calls be cpp2_func( y ) and cpp2_func( x ), but not make that transformation for the call sites to cpp1_func`... is that right?
Currently, name lookup and overload resolution are done in the Cpp1 compiler though, so we can't tell those call sites apart. A future smarter Cpp2 compiler could still do that transformation as a calling convention detail.
FWIW, as a thought experiment, what if there were no return values at all and only out parameters were allowed, either as they are currently implemented or possibly with an updated implementation -- what would/wouldn't work in the use cases you have in mind?
what would/wouldn't work in the use cases you have in mind?
This one's easy. You can easily/naturally chain function calls when f(x) represents the output values (g(f(x))), but not when they're written result: T; f(x, out result)
@hsutter Yes, I think the code transformation would work roughly as you describe. But you wouldn't need to apply it to every cpp2 function definition and call. You'd only need to apply it to definitions that use named return values:
cpp2_func: () -> (w: widget) = {...}
And you're right: this implies that the cpp2 compiler needs to be able to tell which calls are to NRV functions.
That said, GCC and Clang have recently implemented guaranteed NRVO (following P2025), so if you're compiling your C++ code using those compilers, you can do a much simpler code transformation: declare the return variable in the first line of the function body, rather than in the signature:
// cpp2 code
cpp2_func: () -> (w: widget) = {
w = foo();
}
// translated into cpp1, assuming NRVO is available
auto cpp2_func() -> widget {
auto w = foo();
return w; // We also need to make sure that returns are explicit
}
This transformation is easier to implement, because it means that call sites don't need to be altered.
Of course, if you need this transformation to guarantee that no copies/moves are performed, P2025 (or something similar) would need to be standardized. Until then, the only transformation that is portable is the one you described.
// translated into cpp1, assuming NRVO is availablestructcpp2_func__ret {
widget w;
}
autocpp2_func() -> cpp2_func__ret {
auto res = cpp2_func__ret{foo()};
return res; // We also need to make sure that returns are explicit
}
Although to not require that all data members are initialized at the same time
(to not break today's use cases for the existing feature):
// cpp2 code
cpp2_func: (b: bool) -> (w: widget, v: widget) = {
w = foo();
stuff();
if b {
v = bar();
} else {
v = baz();
}
}
// translated into cpp1, assuming NRVO is availablestructcpp2_func__ret {
union { widget w; };
union { widget v; };
}
autocpp2_func(boolconst& b) -> cpp2_func__ret {
auto res = cpp2_func__ret{};
res.w = foo();
stuff();
if (b) {
res.v = bar();
} else {
res.v = baz();
}
return res; // We also need to make sure that returns are explicit
}
Different from today is that those initializations are not runtime checked.
FWIW, as a thought experiment, what if there were no return values at all and only out parameters were allowed, either as they are currently implemented or possibly with an updated implementation -- what would/wouldn't work in the use cases you have in mind?
operator=: (out this, …) would be unified with any other f: (out x, …):
x: t = (…);
x = (…);
x: t = f(…); // Now possible.
x = f(…);
Whereas on the main branch:
// `operator=` remains the same.
x: t; // Can't initialize with `f(…)`.
f(out x);
what would/wouldn't work in the use cases you have in mind?
This one's easy. You can easily/naturally chain function calls when f(x) represents the output values (g(f(x))), but not when they're written result: T; f(x, out result)
A follow-up that's pertinent to the multiple named return values case, is that it opens the language syntax to the possibility of splatting when chaining function calls. This is something other languages have had for a while (e.g. in Julia: f(g(x)...) will fill up multiple parameters of f correctly if g returns a named tuple).
Repository owner locked and limited conversation to collaborators on Aug 30, 2023
Activity
JohelEGP commentedon Jul 13, 2023
Cpp2 already has what you're suggesting:
So what would be left of your suggestion is: "remove
outparameters to reduce concept count".But I don't know if that's desirable, given that
outrepresents "out parameters" (F.21).Named (multiple) return values can't replace the "Exception" in F.21,
but
outparameters can, so I doubt it's worth removing them.EDIT: Actually, the above can be done with
inoutparameters.JohelEGP commentedon Jul 13, 2023
Actually, I don't think the current implementation of named return values can guarantee copy elision.
IIRC, it essentially lowers the named return values like
outparameters, but on the callee.So it really does assign to local variables, and doesn't return a prvalue.
Herb has previously stated his intention to support named parameters.
It might make sense to wait until then to be able to support copy elision on named return values.
Although that would still require reworking the current named return values feature.
nmsmith commentedon Jul 13, 2023
Yeah, the big difference between this proposal and the current semantics for named return values in cpp2 is that in the latter case, the return variables are not interpreted as references to a memory location provided by the caller. This is a missed opportunity, IMO.
But once you make that adjustment,
outparameters seemingly become redundant. (Which is a good thing if true, because it would constitute a simplification.)hsutter commentedon Jul 13, 2023
Thanks! Note that Cpp2 does have two different features here (both of which Cpp1 also has, but here they're generalized with more language support and the ability to declare intent):
outparameters, which can accept an initialized or uninitialized argument (not just an unione). For an uninitialized argument, any function with anoutargument acts as another (delegating) constructor. For an initialized argument, it assigns over the existing value.They are similar but do have different use cases. I agree that more often you would just use return values, but
outparameters are also useful and cover cases return values don't.Doing more for copy elision in the implementation of multiple return values is interesting though, good suggestion.
nmsmith commentedon Jul 13, 2023
@hsutter Thank you for your reply!
Your comment seems to mostly focus on describing how
outparameters and return values work currently, and that description is consistent with my understanding. But you don't seem to have provided any reasoning as to why the two constructs cannot be merged into one.Do you not believe it is possible to merge the two constructs into one, as I have proposed? It looks very possible to me.
jcanizales commentedon Jul 13, 2023
I don't see either what the difference in use cases is.
In the caller I can assign the return value of a function to either an initialized variable or an uninitialized one, same as I can pass either as an out argument.
In the callee, the body has to produce a new value for each out parameter, same as for the returned one.
SebastianTroy commentedon Jul 14, 2023
nmsmith commentedon Jul 14, 2023
I think you're mixing up the notions of variables and memory locations:
outparameters (the variables) is that of the callee's function body.outparameters refer to are provided by the caller.So there is no distinction there.
SebastianTroy commentedon Jul 14, 2023
nmsmith commentedon Jul 14, 2023
I'm not sure exactly what you're asking, but I can give you a blanket answer: a function that declares a named return value will behave exactly how a function that declares an
outparameter will behave.In my original post, I mentioned that what I am proposing is essentially just to adjust the syntax of
outparameters such that they look like (and compose like) return values.So by definition, the two features will behave the same.
realgdman commentedon Jul 14, 2023
How about interoperability with c++1? Suppose I need to call some API function with
outparam.nmsmith commentedon Jul 14, 2023
I'm not sure what you mean. C++1 doesn't have
outparameters, it just has references. (References can be used to simulateoutparameters, but the compiler can't tell the difference.) You can pass a reference to a C++1 function call the same as always.SebastianTroy commentedon Jul 14, 2023
nmsmith commentedon Jul 14, 2023
@SebastianTroy Irrespective of whether the function accepts an
outparameter or returns a value, you can store the result in a variable declared in an outer scope:With NRVO, this doesn't require any copying or moving. So it behaves exactly the same as an
outparameter.JohelEGP commentedon Jul 14, 2023
It seems like #123 touches upon this, too:
hsutter commentedon Jul 14, 2023
Thanks. I'm thinking about this, it's good feedback and I'm happy to reduce concept count where possible.
Disclaimer: The following is "thinking out loud"... I could be making a simple thinko here.
Given two functions:
All these call sites work today, and I assume we'd want them to continue working:
I think the suggestion is to change the Cpp1 code gen for
cpp2_functo emit Cpp1auto cpp2_func( widget& )(or similarlywidget*, same idea). Then when calling it, we would have to transform the call sites to make the Cpp1 code gen for the calls becpp2_func( y )andcpp2_func( x), but not make that transformation for the call sites tocpp1_func`... is that right?Currently, name lookup and overload resolution are done in the Cpp1 compiler though, so we can't tell those call sites apart. A future smarter Cpp2 compiler could still do that transformation as a calling convention detail.
FWIW, as a thought experiment, what if there were no return values at all and only
outparameters were allowed, either as they are currently implemented or possibly with an updated implementation -- what would/wouldn't work in the use cases you have in mind?jcanizales commentedon Jul 14, 2023
This one's easy. You can easily/naturally chain function calls when
f(x)represents the output values (g(f(x))), but not when they're writtenresult: T; f(x, out result)nmsmith commentedon Jul 15, 2023
@hsutter Yes, I think the code transformation would work roughly as you describe. But you wouldn't need to apply it to every cpp2 function definition and call. You'd only need to apply it to definitions that use named return values:
And you're right: this implies that the cpp2 compiler needs to be able to tell which calls are to NRV functions.
That said, GCC and Clang have recently implemented guaranteed NRVO (following P2025), so if you're compiling your C++ code using those compilers, you can do a much simpler code transformation: declare the return variable in the first line of the function body, rather than in the signature:
This transformation is easier to implement, because it means that call sites don't need to be altered.
Of course, if you need this transformation to guarantee that no copies/moves are performed, P2025 (or something similar) would need to be standardized. Until then, the only transformation that is portable is the one you described.
JohelEGP commentedon Jul 15, 2023
I think this is a more correct translation:
Although to not require that all data members are initialized at the same time
(to not break today's use cases for the existing feature):
Different from today is that those initializations are not runtime checked.
JohelEGP commentedon Jul 15, 2023
FWIW, this works (https://compiler-explorer.com/z/oKEd86qfY):
I don't know what else could be broken by switching to anonymous union members.
JohelEGP commentedon Jul 15, 2023
Summarizing Herb's #540 (comment):
Although the issue author's reply #540 (comment) is:
Because (N)RVO happens via the return value,
as the sample code that follows it suggests,
it should have been something like:
operator=: (out this, …)would be unified with any otherf: (out x, …):Whereas on the
mainbranch:jcanizales commentedon Jul 17, 2023
A follow-up that's pertinent to the multiple named return values case, is that it opens the language syntax to the possibility of splatting when chaining function calls. This is something other languages have had for a while (e.g. in Julia:
f(g(x)...)will fill up multiple parameters offcorrectly ifgreturns a named tuple).