Object construction in C++
A constructor in C++ initialises its object in an already allocated memory location pointed to by the implicit and hidden this
pointer. This design decision allows for a number of niceties:
- The same constructor function can be used for direct stack allocation, direct heap allocation, or other in-place allocation with an explicit address.
- Copy elision in assignment (
auto foo = Foo{5}
) is also possible.
Return-value optimisation (for unnamed temporary values in the return statement) and named return-value optimisation (for other locally constructed objects returned by value) are also supported in C++, and at least made easier to implement thanks to this design.
Because the final object is already allocated beforehand an arbitrary deeply nested object construction will not produce a cascade of copy operations propagating bottom-up. Each memory location is written to once, and possibly in linear order, not repeatedly copied layer by layer up the composition hierarchy.
An opportunity for Rust
Hidden out
argument
It would be nice if something similar to this could be supported and standardised in Rust. If Rust supported some sort of out
argument then the same optimisations and functionality could be implemented. The out
argument would be similar to a normal mutable pointer but with the assumption that the variable is uninitialised. Each individual field in that object would, by construction, be initialised:
fn new(a: i32) -> Foo {
Foo {
x: a,
y: Bar::new(a),
}
}
into:
fn new2(x: i32, out: *mut Foo) {
unsafe {
std::ptr::write(out.x, a);
Bar::new2(a, &mut out.y);
}
}
RVO transformations
Any function that returns by value is eligible for the hidden out
-argument optimisation. Instead of returning the value the function is transformed to write into the supplied memory location. Primitives are simply written to the memory location. Because the value of the function is written to the provided place it no longer needs to be āreturnedā. This kind of transformation should be able to deal with both named and unnamed return-value optimisation, as well as copy elision in assignments.
Fallible and infallible initialisations
A constructor in C++ may only fail by throwing an exception. This trick is of course used to hide the unhappy path. A more rusty way could be to look for an instance of the Try
trait. For example:
fn try_new(a: i32) -> Option<Foo>
could be transformed into:
fn try_new2(a: i32, place: *mut Foo) -> Option<()>
The call-side would be unchanged:
let foo = Foo::try_new(5)?;
let foo = box Foo::try_new(5)?;
container.push_into(|| Foo::try_new(5)?);
Or perhaps an explicit choice would have to be made?
let foo <- Foo::try_new(5)?;