placing functions
— 2025-07-08
- what are placing functions?
- a basic desugaring
- thinking in placing functions
- prior art in rust
- q&a
- conclusion
What are placing functions?
About a year ago I observed that in-place construction seems surprisingly simple. By separating the creating of the place in memory from writing the value to memory, it’s not that hard to see how we can turn that into a language feature. So about six months ago, that’s what I went ahead and did and created the placing crate: a proc-macro-based prototype for “placing functions”.
Placing functions are functions whose return type is constructed in the caller’s
stack frame rather than in the function’s stack frame. This means that the
address from the moment on construction is stable. Which may not only leads to
improved performance, it also serves as the foundation for a number of
useful features like supporting dyn
AFITs (Async Functions in Traits) 1.
In this post I’ll be explaining how placing functions desugar, why placing functions are the right solution for emplacement, and how placing functions integrate into the language. Not in as much detail as I’d do for an actual RFC, but more as a broad introduction to the idea of placing functions. And to get right into it, here is a basic example of how this would work:
struct Cat {
age: u8,
}
impl Cat {
#[placing] // ← Marks a function as "placing".
fn new(age: u8) -> Self {
Self { age } // ← Constructs `Self` in the caller's frame.
}
fn age(&self) -> &u8 {
&self.age
}
}
fn main() {
let cat = Cat::new(12); // ← `Cat` is constructed in-place.
assert_eq!(cat.age(), &12);
}
A basic desugaring
The purpose of the placing
crate is to prove that placing functions should not
be that hard to implement. I managed to implement a working prototype in a few hours over my winter holidays. In total I've spent maybe four or so days on the implementation. I’m not a compiler engineer though, and I expect that the lovely folks on
T-Compiler can probably recreate this in a fraction of the time it took me.
Since I don't know my way around the rustc frontend, I implemented the placing crate entirely using proc macros. The upside was that I could get something working more quickly. The downside is that proc macros don’t have access to type information, so I had to hack around that limitation. Which results in an API that requires a lot of proc macro attributes. But that’s ok for a proof of concept.
Let’s walk through the basic example I showed earlier, but this time using the proc macros. Starting by installing the placing crate:
$ cargo add placing
We can then import placing
and define our main struct Cat
. We need to
annotate this with the #[placing]
attribute macro because we need to change
the internal representation slightly. Here is what this looks like:
//! Original
use placing::placing;
#[placing]
pub struct Cat {
age: u8,
}
Let’s take a look at what the #[placing]
annotation expands to. As I said in
the intro: placing functions separate the creation of the memory location from
the initialization of the values in said location. For our type Cat
, the
location must be of type MaybeUninit<Cat>
. But because we want to keep the
type the same, even if we change how the internals work, we actually want to
keep Cat
as the outer type name and move the fields into an internal
MaybeUninit
:
//! Desugared
use std::mem::MaybeUninit;
/// This keeps the same external type,
/// but changes the internals to store the fields
/// in a `MaybeUninit`.
#[repr(transparent)]
pub struct Cat(MaybeUninit<InnerCat>);
/// These are the fields contained in the original
/// type `Cat`. But separated so that they can be
/// wrapped in a `MaybeUninit` internally.
struct InnerCat {
age: u8,
}
Our desugaring needs to add one last bit to our type definition to ensure it works correctly.
Because we are now holding a MaybeUninit
we have to make sure it calls its
destructors when dropped. This means our desugaring needs to generate a Drop
implementation
that calls through the MaybeUninit
.
//! Desugared
impl Drop for Cat {
fn drop(&mut self) {
// The constructors guarantee this will never
// be dropped before it has been initialized.
unsafe { self.0.assume_init_drop() }
}
}
Now that we have our type definition, let’s show how to implement the new
constructor. Because we don’t have access to type information the placing crate
requires annotations on both the impl block and the method:
//! Original
#[placing]
impl Cat {
#[placing]
fn new(age: u8) -> Self {
Self { age }
}
}
This is the trickiest part of the desugaring because it needs to split up the constructor into two parts. One to create the place, the other to initialize the values in-place. Conceptually that means rewriting the return type of the last line into writing into a mutable argument instead.
//! Desugared
use std::mem::MaybeUninit;
impl Cat {
/// Calling this function constructs the place to
/// initialize the type into. This is part of a
/// two-part constructor, and it must always be
/// followed by a call to `new_init`.
unsafe fn new_uninit() -> Self {
Self(MaybeUninit::uninit())
}
/// This initializes the values of the type in-place.
/// `new_init` must not be called more than once, and
/// must always follow a call to `new_uninit`.
unsafe fn new_init(&mut self, age: u8) {
let this = self.0.as_mut_ptr();
unsafe { (&raw mut (*this).age).write(age) };
}
}
Next we also have a getter function. All we need to do with that is teach it how to reach through the outer struct and into the inner fields. Here is the definition:
//! Original
#[placing]
impl Cat {
fn age(&self) -> u8 {
&self.age
}
}
And here is what it expands to:
//! Desugared
impl Cat {
fn age(&self) -> u8 {
let this = unsafe { self.0.assume_init_ref() };
this.age
}
}
With that we’re now ready to create an instance of Cat
in-place, and invoke
our getter. This is the only part that can’t be abstracted away, since Rust
macros have very strict scoping rules compared to if we implemented this
directly in the compiler. What we really wish we could do would be this:
let cat = placing!(Cat, new, 12);
But for now we have to call this manually instead, and so the invocation looks like this:
//! Original
fn main() {
let mut cat = unsafe { Cat::new_uninit() };
unsafe { cat.new_init() };
assert_eq!(cat.age(), &12);
}
As an aside: in Rust we now have the experimental super_let
feature which
makes it possible to construct types in the enclosing scope, but reference them
afterwards. This almost works for our use case, except it can only return
references, not owned types. That means the best we can do with that feature is the following (playground):
#![feature(super_let)]
macro_rules! new_cat {
($value:expr $(,)?) => {
{
super let mut cat = unsafe { Cat::new_uninit()) };
unsafe { Cat::new_init(&mut cat, $value) };
&mut cat // ← ❌ Returns by-ref rather than by-value
}
}
}
If we try and return an owned value it actually copies it - which is exactly
what we’re trying to avoid. We might be able to change that in the compiler
implementation. And to summarize: here is the placing
crate’s version of our original example:
//! Original
use placing::placing;
#[placing]
struct Cat {
age: u8,
}
#[placing]
impl Cat {
#[placing]
fn new(age: u8) -> Self {
Self { age }
}
fn age(&self) -> u8 {
&self.age
}
}
fn main() {
// `Cat` is constructed in-place.
let mut cat = unsafe { Cat::new_uninit() };
unsafe { cat.new_init() };
assert_eq!(cat.age(), &12);
}
And here is the same code with all the macros expanded:
//! Desugared
use std::mem::MaybeUninit;
#[repr(transparent)]
struct Cat(MaybeUninit<InnerCat>);
struct InnerCat {
age: u8,
}
impl Cat {
/// Creates an uninitialized place
unsafe fn new_uninit() -> Self {
Self(MaybeUninit::uninit())
}
/// Initializes the fields in-place
fn new_init(&mut self, age: u8) {
let this = self.0.as_mut_ptr();
unsafe { (&raw mut (*this).age).write(age) };
}
fn age(&self) -> u8 {
let this = unsafe { self.0.assume_init_ref() };
this.age
}
}
impl Drop for Cat {
fn drop(&mut self) {
unsafe { self.0.assume_init_drop() }
}
}
fn main() {
let mut cat = unsafe { Cat::new_uninit() };
unsafe { cat.new_init() };
assert_eq!(cat.age(), &12);
}
The placing crate also supports desugaring types that return Result
, Box
,
and Arc
. As well as includes support for nesting constructors, where you end
up calling a #[placing] fn
from a #[placing] fn
. This is why I’m fairly confident this should end up working out.
The main limitation of the crate is that it doesn’t yet support traits. I
started adding support for that, but ended up running out of time. This is the
reason why if you look at the codegen in the crate you’ll see a PLACING: bool
const
generic inserted everywhere. It isn’t particularly useful yet, but now you
know why it’s there.
Thinking in placing functions
Placing functions draw their inspiration from two other language features:
- Super Let (Rust Nightly): is an experimental feature that enables temporary lifetime extensions. This allows variables to be created in the enclosing scope.
- Guaranteed Copy Elision (C++ 17): sometimes also called “deferred temporary materialization” guarantees that structs are always constructed in the caller’s scope.
If super let
operates on block scopes, you can think of placing functions as
operating across function boundaries. And if guaranteed copy elision is an
automated guarantee that applies to all functions, you can think of placing
functions as only ever applying to functions that have been explicitly opted
into this feature.
The design of placing functions attempts to balance three core constraints:
- Control: in certain cases emplacement already happens through optimizations (e.g. inlining). An emplacement language feature must guarantee it emplaces, or else fail compilation.
- Integration: C++’s guaranteed copy elision shows just how broadly applicable emplacement really is. That means the initial upper bound for this language feature is every single function with a return type. That’s incredibly broad, and we need to ensure it integrates closely and easily with the rest of the language.
- Compatibility: Rust’s stdlib makes strong backwards-compatibility guarantees. We will want it to be able to make use of emplacement without breaking backwards compat. This means we cannot mint new traits just to support emplacement, or add new APIs specifically to emplace.
It would be a mistake to think of emplacement as a feature with narrow applicability; C++ provides evidence emplacement is relevant to almost every constructor. The right way to think about emplacement is to assume it has a maximally broad upper bound. But then start designing and implementing a minimal subset. While we may eventually find limitations or cases where emplacement isn’t possible; that will be something we prove out through implementation.
This is why I believe we should model “emplacement” more like an effect, and not
like a different kind of language feature. I think of placing functions as
somewhere between const functions and async functions. They change the codegen
of the function, not entirely unlike the generator transform we use for async
and gen
. But it never actually lowers to another type we can observe in the
type system, which makes it very similar to const
.
Prior Art in Rust
As I was getting ready to publish this post I ended up talking a little more with Sy Brand about placing functions, C++, and ABIs. It turns out: Rust already guarantees emplacement in a number of cases. Consider for example the following code:
pub struct A {
a: i64,
b: i64,
c: i64,
d: i64,
e: i64,
}
impl A {
pub fn new() -> A {
A {
a: 42,
b: 69,
c: 4269,
d: 6942,
e: 696942,
}
}
}
When we compile this for the SYSV ABI on x86, it outputs the following assembly (godbolt):
example::A::new::hd00831bc57a4b613:
mov rax, rdi
mov qword ptr [rdi], 42
mov qword ptr [rdi + 8], 69
mov qword ptr [rdi + 16], 4269
mov qword ptr [rdi + 24], 6942
mov qword ptr [rdi + 32], 696942
ret
This assembly is writing directly to pointer offsets provided to the function. In other words: this function is emplacing. And it’s actually guaranteed to do that, as defined in the x86 SYSV ABI spec, section 3.2.3:
[!quote] If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in
%rdi
as if it were the first argument to the function. In effect, this address becomes a “hidden” first argument This storage must not overlap any data visible to the callee through other names than this argument. On return%rax
will contain the address that has been passed in by the caller in%rdi
.
A hidden first argument that’s passed to functions? That sure sounds a lot like how the desugaring for placing functions is intended to work. In fact, C++’s guaranteed copy elision makes use of this very same feature. Section 3.2.3 states the following:
If a C++ object has either a non-trivial copy constructor or a non-trivial destructor 11, it is passed by invisible reference (the object is replaced in the parameter list by a pointer that has class INTEGER) 12.
This is all incredibly similar to what I’m proposing, but happening
automatically at the ABI level rather than transparently at the language level. It also begs the question: how easy it would be to modify rustc's ABI lowering code for x64 to just say "if the type was declared with the placing
attribute, it's always classified as MEMORY”?
Q&A
What about placing arguments?
So far this post has only discussed placing in relation to return types. For our
goal of preserving express compatibility with the existing stdlib, placing
return types are not enough. Back in March Eric Holk and Tyler Mandry argued
that we’ll also want to have some form of placing arguments (or at least the
capability that enables us to do this). So let’s use Box::new
as our example to
show why. Without placing arguments, the best we could do would be to define
some form of Box::new_with
function that takes a placing closure:
impl<T> Box<T> {
// The existing default constructor
fn new(x: T) -> Self { ... }
// The newly introduced `placing` constructor
fn new_with<F>(f: F) -> Self
where
F: #[placing] FnOnce() -> T,
{ ... }
}
The new_with
constructor is always preferable to the new
constructor
because it guarantees the absence of intermediate stack copies. This will lead
to an effective deprecation Box::new
. If not outright, then likely first by way of ”best practices”.
The way to solve this would be to enable Box::new
to act as the receiver of
values which need to be emplaced. This would be done by requiring annotations
not at the function level, but at the argument/return-type level. Keeping with our #[placing]
placeholder notation, we can imagine it looking something like this:
//! Original
impl<T> Box<T> {
// `Box::new` here takes type `T` and
// constructs it in-place on the heap
fn new(x: #[placing] T) -> Self { ... }
}
I expect the desugaring of this will likely look somewhat similar to Alice
Ryhl’s in-place initialization RFC,
desugaring to some form of impl Emplace
trait. But crucially: this would only
be observable within the implementation, and not to any of the callers.
//! Desugared
/// Write a value to a place.
trait Emplace<T> {
fn emplace(self, slot: *mut T);
}
impl<T> Box<T> {
fn new(x: impl Emplace<T>) -> Self {
let mut this = Box::<T>::new_uninit(); // 1. create the place
x.emplace(this.as_mut_ptr()); // 2. init the value
Ok(this.assume_init()) // 3. all done
}
}
Now the reason why I’ve put this under Q&A is because I haven’t yet figured out the finer language rules here since I haven’t implemented this yet. As a core design constraint: invoking a function that takes placing arguments should be no different from a regular function. This is needed to keep APIs backwards-compatible. What should make this special is that functions with placing arguments and placing return types should work together to emplace.
What about borrows / local lifetime extensions?
In her post on super let, Mara
provides a clear example for when temporary lifetime extensions are useful. Here
Writer::new
takes an &'a File
, and we need a feature like super let
to
create an instance of File
that outlives the scope of the block:
let writer = {
println!("opening file...");
let filename = "hello.txt";
super let file = File::create(filename).unwrap();
Writer::new(&file)
};
The return type Writer
here must have a lifetime to be able to reference
super let file
. But it can’t be a normal lifetime, since it doesn’t adhere to
the usual rules. Without specifying any of the concrete rules, this lifetime has
been dubbed 'super
. From the perspective of the block this behaves not unlike
'static
- though crucially it is not the same as 'static
.
Now the question is: how can we represent this block as a function instead?
Because it makes sense that we would want to eventually be able to factor out
functionality from blocks into functions. We’d probably want to do that using a
'super
lifetime, like so:
//! Original
fn create_writer(filename: &str) -> Writer<'super> {
println!("opening file...");
super let file = File::create(filename).unwrap();
Writer::new(&file)
}
Note how Writer
itself does not require a #[placing]
annotation: it’s okay
that we copy it out of the function. The only important part is that file
outlives the current scope. The desugaring for this is quite fun, even if we
can’t yet represent it in the type system. What we need to do here is ensure
that file
is constructed in-place in the caller’s scope. And once initialized
we can reference it using a blank/unsafe lifetime in our return type. I haven’t
actually checked this, but I believe this is a valid desugaring:
//! Desugared
fn create_writer<'a>(filename: &str, file: &'a mut MaybeUninit<File>) -> Writer<'a> {
println!("opening file...");
let file = unsafe { file.write(File::create(filename).unwrap()) };
Writer::new(file)
}
This however needs to be paired with a function prelude on invocation to create
the place for file
. We can therefore imagine the invocation of this function
looking something like this:
// with syntactic sugar
let writer = create_writer("hello.text");
// desugared
let mut file = MaybeUninit::uninit();
let writer = create_writer("hello.text", &mut file);
What about pinning?
Once we have emplacement in the language, most of the reasons for Pin
being
the way it is kind of fall away. But there is a gap between having emplacement,
and then also having !Move
, and so we do need to have some form of
compatibility with Pin
. Luckily the Pin
type is just a special-case of the
previous lifetime extension example.
What’s neat about placing functions is that it would allow us to replace the std::pin::pin!
macro with a pin
free-function, using the 'super
lifetime:
//! Original
pub fn pin<T>(t: T) -> Pin<&'super mut T> {
super let mut t = t;
unsafe { Pin::new_uninit(&mut t) }
}
All the desugaring needs to do to make this work is change the function to take an additional
slot MaybeUninit<T>
that we can write our value into. This allows us to extend
the lifetime, after which we can reference it in our return type like so:
//! Desugared
pub fn pin<T, 'a>(t: T, slot: &'a mut MaybeUninit<T>) -> Pin<&'a mut T> {
let mut t = unsafe { slot.write(t) };
unsafe { Pin::new_uninit(&mut t) }
}
Are annotations necessary?
RFC 2884: Placement by Return
proposed introducing C++’s Guaranteed Copy Elision rules to Rust almost as-is. I appreciate this
RFC because it has the right idea about the scope of the changes, and does it
solely by changing the meaning of return
. But where it runs into trouble is
that it only changes the meaning of return
. And so instead of adding
placement to e.g. Box::new
, it needs to add a new method Box::new_with
.
Fundamentally there are three kinds of placement we’re interested in:
- Placing return types: where we want a to avoid copying the type returned from a function. For example if we want a referentially-stable constructor.
- Placing function arguments: where we want to avoid copying an argument passed into a function. For example: to when constructing a type on the heap.
- Lifetime extensions: where we want to reference a local variable from a local type which will outlive the current scope (lifetime extension). For example: when pinning.
Even if C++’s Guaranteed Copy Elision should be our ultimate goal; annotations allow us to get there incrementally. Placing return types are fairly easy. Placing function arguments are a little harder. And lifetime extensions will be harder still. Being able to opt into this via explicit annotations means we can start small and gradually build up.
For something as broad as “functions with arguments or return types” that seems like the right way to start. And if for some reason this feature ends up being so successful we’ll want to annotate nearly every function with it, changing defaults seems like something that could be done over an edition if we wanted to.
What about self-referential types?
I’ve written at length about self-referential types before. Once we have placing functions, we end up with three components for generalized self-referential types are:
- Placing functions: so we can construct a type in a stable position in memory
- Self-lifetimes: so you can declare that some field borrows from some other field.
- Partial constructors: so you can start by initializing the owned data first, and initialize the references to that data second.
We’ve already seen placing functions. Self-lifetimes would allow fields to refer to data contained in other fields, which would probably look something like this:
struct Cat {
data: String,
name: &'self.data str, // ← references `self.data`
}
And partial constructors would allow you to construct types in multiple phases.
For example, here we first initialize the field data
in Cat
. And then we
take the string contained within, and do some crude parsing to interpret
everything up until the first space as the cat’s name:
fn new_cat(data: String) -> Cat {
let mut cat = Cat { data, .. };
cat.name = cat.data.split(' ').next().unwrap();
cat
}
Once we have this, we can of course combine it with the lifetime extension
examples we’ve shown before to ensure that the type remains in a stable memory
location. But crucially: this is also forward-compatible with alternative
mechanisms for referential stability such as the Move
auto-trait.
As a side note: partial constructors are basically the same feature as view
types and pattern
types. It’s
still the same general refinement feature, but now with an added rule that we
can go from a refinement type back to the original type by populating its
fields. Assigment here takes the place of an inverse match
if you will.
What’s nice about this design is that these features are all orthogonal, but complimentary. Emplacement is useful even without partial initialization. And partial initialization (refinement) is useful even without self-referential lifetimes. Features complimenting each other in this way to me is the hallmark of good language design. It means it generalizes beyond just a niche use case. But simultaneously becomes even more useful when combined with other features.
Why not directly rely on Init<T>
or &out
?
Both the Init
type and &out
parameters feature are backwards-incompatible to
add to existing types and interfaces. This is a problem, because placement is
broadly applicable: we know from C++ 17 that virtually every constructor wants to be
placing. And we can’t reasonably rewrite every function returning -> T
to instead
return -> Init<T>
or take &out T
.
// 1. Original signature
fn new_cat() -> Cat { ... }
// 2. Using `Init`, changes the signature
fn new_cat() -> Init<Cat> { ... }
// 3. Using `&out`, changes the signature
fn new_cat(cat: &out Cat) { ... }
// 3. Using `#[placing]`, preserves the signature
#[placing]
fn new_cat() -> Cat { ... }
That doesn’t mean that these designs are inherently broken or incorrect; far from it actually. But because they seem to assume a different scope for the design, it naturally means those designs are operating with a different set of design constraints - in turn leading to different designs.
I believe that RFC 2884: Placement by Return by Poignard Azur had the right idea. In order for Rust to be competitive with C++, we need to be able to guarantee most constructors can emplace. And in order to do that, we can’t require people to rewrite their code.
However, the Init RFC has
some great ideas about how to emplace function arguments. Which is something
that RFC 2884 didn’t have a good answer to. I believe that Rust’s strength lies
in its ability to distill different ideas and synthesize them into something
new. I believe that we can arrive at something truly great if we combine super let
, placement-by-return, Init
, and ensure it is backwards-compatible.
Can placing functions be nested?
Yes - placing functions called in a return position should be able to compose. For the language feature should be relatively straight-forward when calling one placing function inside of another in the return position:
struct Foo {}
#[placing]
fn inner() -> Foo {
Foo {} // ← 1. Constructed in the caller's scope
}
#[placing]
fn outer() -> Foo {
inner() // ← 2. Forwards the emplacement to its caller
}
This itself is reminiscent of C++’s guaranteed copy elision guarantees, which are composable through functions. That is because in C++ temporaries are only materialized into real objects at the end of a call chain, enabling arbitrarily deep composition. For Rust it’s important that we maintain this same property, including when wrapping and composing types:
struct Foo {}
#[placing]
fn inner() -> Foo {
Foo {} // ← 1. Constructed in the caller's scope
}
struct Bar(Foo)
#[placing]
fn outer() -> Bar {
Bar(inner()) // ← 2. Emplaces Bar in the caller, and Foo in Bar
}
In this example Bar
is constructed in the caller’s scope, and as part of
initialization it invokes and emplaces Foo
inside of it. I stopped working on
the placing crate before implementing this, but for this desugaring to work all
we need to do is for the “place” used by the inner
function to by placed
inside of the “place” used by the outer
function.
The most complex kind of composition is when we start involving temporaries. Consider the following example which constructs a type in the first function, mutates it in the second function, and finally uses it in a third function:
/// A Cat which is constructed in place and can meow.
struct Cat {}
impl Cat {
#[placing]
fn new(name: String) -> Self { .. }
fn set_name(&mut self) { .. }
fn meow(&self) { .. }
}
/// Construct a value.
#[placing]
fn first() -> Cat {
let name = "Nori".to_string();
Cat::new(name) // ← 1. Emplaced in the caller
}
/// Mutate the value.
#[placing]
fn second() -> Cat {
super let mut cat = first(); // ← 2. Emplaced in the caller
cat.set_name("Chashu".to_string()); // ← 3. Mutated
cat // ← 4. Logically returned
}
/// Use the value.
fn third() {
let cat = second(); // ← 5. Placed on-stack here
cat.meow();
}
We need to have some way to emplace temporaries that we later logically return
from the caller. The super let
feature seems like it would particularly well
for this. In the 'move
example we were returning a reference to a super let
value from a function. But I think it would make a lot of sense if we could use
super let
to return logically owned values from a function too, as shown here.
Conclusion
This post introduces a design for placing functions: a declarative addition to
Rust that enables types to be constructed in-place. Unlike alternative APIs such
as Init<T>
and &out
, placing functions are designed to keep function
signatures intact, enabling existing functions and APIs to be annotated as
retroactively. In other words: placing functions were designed to prioritize
backwards-compatibility.
The placing functionality has been prototyped in the placing crate. Even though this crate was designed with limited time and entirely using procedural macros, it should be sufficient to prove the feasibility of a language feature.
While placing functions are the most important placement-related feature, they are not the only one. There are three kinds of placing functions total that need to be addressed:
- Placing return types: where we want a to avoid copying the type returned from a function. For example if we want a referentially-stable constructor.
- Lifetime extensions: where we want to reference a local variable from a local type which will outlive the current scope (lifetime extension). For example: when pinning.
- Placing function arguments: where we want to avoid copying an argument passed into a function. For example: to when constructing a type on the heap.
A complete solution should address all three of these uses. Placing functions only directly enables placing return types, but in the discussion section we’ve also discussed how we could extend this to include lifetime extensions and placing function arguments.
When discussing emplacement it’s important to consider both intra-function and
inter-function variants. The experimental super let
feature only works
within functions today (intra-function). With placing functions working very
similarly, enabling similar functionality to work between functions too
(inter-function). A good design should make both variants easy, convenient, and
interoperable.
In this post we’ve also explained why the scope for emplacement is broad: in C++ constructors guarantee emplacement by default. And given Rust wants to have performance that’s competitive with C++, we have to assume that most functions will eventually want to make that guarantee as well. And the only realistic way to achieve that is if we can guarantee emplacement in a backwards-compatible way.
Thanks to Sy Brand for reviewing earlier copies of this post and especially providing valuable feedback and clarifications about both C++’s copy elisions feature, as well as how the SYSV ABI works.
I only had a hunch this was possible. Alice Ryhl has done the work to prove this can be done. Albeit for a slightly different, but very similar proposal.