Closed
Description
Old Proposal:
Have well-defined rules for copy eliding, and we sometimes allow what looks like copying non-copyable objects.
- During the semantic analysis of every expression, there is an additional field of provided context, which is the location to put the expression's result value.
- A function can declare a non-copyable return type. In this case, the function gets an additional, secret parameter that is a writable pointer to where it should write its return value.
- Here's how specific language constructs handle the result location:
- A
var
orconst
declaration creates a location, and passes that location to the initializer expression, if any. - An assignment statement uses the address of the left hand side as the result location for the right hand side.
- A function call or an operator that acts like a function (e.g.
+
,~
) creates a temporary storage location for each of its parameters/operands and provides that temporary storage as the result location when evaluating each parameter/operand expression. - The body of a function whose return type is copyable uses a special result location, such as a platform-specific register.
- The body of a function whose return type is non-copyable uses the secret result location pointer parameter as the result location.
- A return statement provides the function body's result location to the return expression.
- For a function call where the function's return type is non-copyable, the function call expression's result location is passed as the function's secret return value pointer parameter.
- For a function call where the function's return type is copyable, the result of the function is copied from where the function puts it (such as a platform-specific register) to the function call expression's result location.
- A block or branching control structure forwards its result location to whatever sub-expression determines its result value.
- A statement followed by a
;
in a block gets a void result location. - A defer statement provides a void result location to its expression.
- A struct, array, or enum initialization expression uses its result location in-place.
- Automatic error and maybe coercion happen in-place.
- A
Examples:
fn foo() -> u32 { // result location for function body is a special register
bar(); // function call gets a void result location, so bar() must not return anything (see #219).
var // varaible declaration gets a void result location.
a : u32 // creates location for a u32
= 1; // integer literal gets &a as result location
const b // creates a location for a TBD type
= baz(); // baz() gets &b as result location, and baz() determines the type of b
a = ( // result location is &a
b // result location is a temporary location of a TBD type provided by the + operator
+ // checks left and right types and produces the sum into &a, possibly doing automatic type coorsion first
baz() // result location is a temporary location of a TBD type provided by the + operator
);
var array // creates a location for a TBD type
= []u32 { // result location is &array, and now type of array is [TBD]u32.
1, // result location is &array[0], and array.len is at least 1
5, // result location is &array[1], and array.len is at least 2
};
1 // result location is the special register for the function body
}
struct BigStruct {
a: [2]SubStruct,
pub fn init(offset: u32) -> BigStruct { // result location is the secret parameter; let's call it result_location
BigStruct { // result location is still result_location
.a = []SubStruct { // result location is &result_location.a
SubStruct { // result location is &result_location.a[0]
.x = offset + 0, // result location is &result_location.a[0].x
// (note elaboration on the + operator is omitted here. see above.)
},
SubStruct { // result location is &result_location.a[1]
.x = offset + 1, // result location is &result_location.a[1].x
},
},
}
}
}
struct SubStruct {
x: u32,
}
fn main() {
var a // creates a location for a TBD type
= BigStruct.init(10); // result location secret parameter is &a
// equivalent to:
var b : BigStruct = undefined;
b.a[0].x = 10 + 0;
b.a[1].x = 10 + 1;
var c // creates a location for a TBD type
= if // result location is &c
(
something() // result location is a temporary location created by the if
) {
BigStruct.init(100) // result location is &c
} else {
BigStruct.init(200) // result location is &c
};
// equivalent to:
var d : BigStruct = undefined;
if (something()) {
d.a[0].x = 100 + 0;
d.a[1].x = 100 + 1;
} else {
d.a[0].x = 200 + 0;
d.a[1].x = 200 + 1;
}
var e // creates a location for a TBD type
= if (something()) {
a // ERROR: can't copy type BigStruct
} else {
b // ERROR: can't copy type BigStruct
};
}
Relative to what #83 originally proposed, we've got relaxed restrictions on returning non-copyable types from a function. Previously returning non-copyable types required use of a named return value. So do we still need named return values?
Here's a usecase for named return values:
struct PluginRegistry {
id_to_plugin: Hashtable(Id, &Plugin), // non-copyable
pub fn init() -> (result: PluginRegistry) {
result.id_to_plugin = Hashtable(Id, &Plugin).init();
result.register(base_plugin.id, &base_plugin);
}
pub fn register(self: &PluginRegistry, id: Id, plugin: &Plugin) {
self.id_to_plugin.put(id, plugin);
plugin.on_register();
}
}
We want to design PluginRegistry
to use the constructor-like pattern where you can assign from init()
, and we want to do something non-trivial with the object before we return it. In order to refer to the object, it has to be named; we wouldn't be able to call register()
if we did a return PluginRegistry { ... }
expression.