placing functions
— 2025-07-08

  1. what are placing functions?
  2. a basic desugaring
  3. thinking in placing functions
  4. prior art in rust
  5. q&a
    1. what about placing arguments?
    2. what about borrows / local lifetime extensions?
    3. what about pinning?
    4. are annotations necessary?
    5. what about self-referential types?
    6. why not directly rely on init<t> or &out?
    7. can placing functions be nested?
  6. 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:

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:

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:

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:

  1. Placing functions: so we can construct a type in a stable position in memory
  2. Self-lifetimes: so you can declare that some field borrows from some other field.
  3. 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:

  1. 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.
  2. 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.
  3. 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.

1

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.