-
Notifications
You must be signed in to change notification settings - Fork 265
Description
Refining Parameter Passing Semantics
Suggestions below attempt to:
- Extend semantic clarity to the forwarding of value category and cv-qualification.
- Extract passing mechanism control, re-focusing the categories on semantics.
- Present a practical option for call-site passing semantics visibility.
- Enable convenient selection between modifying and non-modifying overloads.
- Enhance clarity and consistency.
The forward category obscures semantics
The forward category models pass-by-forwarding-reference, followed by std::forward on last use of the parameter, allowing the compiler to perform overload resolution when passing the parameter on, based on the value category (l-value or r-value) and cv-qualification (const or not) of the argument that was passed in.
But such a forwarding mechanism is ambiguous with regard to whether it takes arguments to conditionally move or to conditionally modify. The move and inout categories are used for fundamentally different purposes, and forward obfuscates which one it's representing in a given instance.
Instead of forward:
Consider using "move?" (conditional move) when the argument will be moved or not, depending on whether or not the argument is an r-value.
Consider using "inout?" (conditional modification) when the argument will be modified or not, depending on whether or not the argument is const (see the suggestion for better naming below).
copy and in_ref are about passing mechanisms, not semantics
Herb's parameter passing paper gives multiple reasons why, when an argument is guaranteed not to be modified, the compiler should be allowed to decide whether to pass by value or reference. Ultimately though, it can't be argued that programmers should be prohibited from deciding themselves. So for forcing pass-by-value, cpp2 now has a copy passing category. And for forcing pass-by-const-reference, it now has in_ref. These undermine the philosophy of organizing the categories around semantics rather than passing mechanism.
Consider, instead, using modifiers to force a given passing mechanism.
foo: (/*in*/ bar: T) // The compiler chooses the passing mechanism. bar is const.
foo: (/*in*/ & bar: T) // Pass by reference. bar remains const.
foo: (/*in*/ = bar: T) // Pass by value. bar becomes a local variable.
Regardless of the passing mechanism used, "in" semantics is honored. Arguments are not modified.
Call-site Visibility
Herb 's parameter passing paper, poses the question of how parameter passing semantics can reasonably be made visible at the call site. Probably the most helpful information to make visible is argument modified and argument moved. While moves have the move keyword, modifications have no such call site indicator. A succinct syntactic solution, which is symmetrical (ie. consistent) between the two cases, may be a better approach.
Consider requiring postfix "+" in order to pass a modifiable l-value for inout or out (treating modifiable l-values as const arguments unless so marked):
swap(a+, b+); // a and b will be modified.
c: = mymap+[d]; // mymap may be modified. d is treated as read-only.
- If one were unaware that
map::operator []may modify the map it's called on, the compiler would make them aware when they tried to use it. To call it, they must explicitly mark the map for modification (mymap+), which then also makes it clear at a glance to the reader. - Or where overloads are available, this syntax enables convenient selection of modifying overloads (like the move keyword enables selection of moving overloads).
- Assignment operators and constructors unambiguously signal intent to modify their left hand operands (
a += b; c: = d;), so no further call-site visual indication is necessary. However, decoration should still be required when assignment operators are invoked as functions rather than used as operators (operator =(e+, f);).
Consider requiring postfix "-" in order to pass a modifiable l-value for move:
a: = b-; // b will be moved.
myvector.push_back(c-); // c will be moved.
- Using language syntax for this is already a positive change from a library function, but this approach minimizes syntactic impact at the call site.
- These call site visual indicators are consistent with each other, and together they probably represent the maximum semantic information that can be unobtrusively included at the call site.
Naming
While this is understandably a taboo topic, there are real issues with naming here. In an early talk on parameter passing, Herb mentioned that people often suggest that "out parameters" shouldn't be supported, and his parameter passing paper quotes examples which mistakenly use out for inout use-cases. These issues likely stem from common conceptions of "out parameters" which are at odds with the semantics of this category. The term is decades old and predates any annotation or linting scheme that could prohibit a function from reading an argument.
Mentions of moving objects, above, are paired with mentions of the move category, but mentions of modifying objects can't be similarly paired (resulting in the awkwardly named suggestion, "inout?", above).
If the categories were consistently named for actions to be taken on arguments (like move), an instructor could say something like, "A function may init, mod, move, or just read a given argument. It may mod or move conditionally. And the passing mechanism can be forced for reading." This language is more natural, consistent, expressive, and clear. It seems to better represent "What, not how" parameter passing. And it side-steps issues around historical understandings of "out parameters".
The impact of these suggestions probably can't be measured, but since ease of teaching, learning, and using the language is a primary goal of cpp2, I believe they should be considered.