Placement new

Some of the statements about Rust in this chapter are dependent on the specifics of how the compiler optimizes various programs. Unless otherwise state, the results presented here are based on rustc 1.87 using the 2024 language edition.

The primary purposes of placement new in C++ are

You also might have ended up on this page looking for how to construct large values directly on the heap in Rust.

There is an open proposal for adding the features analogous to placement new in Rust, but the design of the features is still under discussion. In the meantime, for many of the use cases of placement new, there are either alternatives in safe Rust or approaches that use unsafe Rust that can accomplish the required behaviors.

Custom allocators and custom containers

It is uncommon to use placement new for the first reason because the major use cases are covered by using STL containers with custom allocators. Similarly, Rust's standard libraries can be used with custom allocators. However, in Rust the API for custom allocators is still unstable, and so they are only available when using the nightly compiler with a feature flag. The Rust Book has instructions on how to install the nightly toolchain and the The Rust Unstable Book has instructions on how to use unstable features.

For stable Rust, there are libraries that cover many of the uses of allocators. For example, bumpalo provides a safe interface to a bump allocation arena, a vector type using the arena, and other utility types using the arena.

For implementing custom collection types that involves separate allocation and initialization of memory, the chapters in the Rustonomicon on implementing Vec are a useful resource.

Memory-mapped registers and embedded development

If you are using Rust for embedded development, you may want to additionally read the Embedded Rust Book. The chapters on peripherals discuss how to work with structures that are located at a specific address in memory.

The Embedded rust Book also includes a chapter on advice for embedded C programmers using Rust for embedded development.

Performance and storage reuse

This use of placement new in C++ for the purpose of reusing storage can usually be replaced in Rust by a simple assignment. Because assignment in Rust is always a move, and in Rust moves do not leave behind objects that require destruction, the optimizer will usually produce code analogous to placement new for this use case. In some cases, this also depends on an RVO or NRVO optimization. While these optimizations are not guaranteed, they are reliable enough for common coding patterns, especially when combined with benchmarking the performance-sensitive code to confirm that the desired optimization was performed. Additionally, the generated assembly for specific functions can be examined using a tool like cargo-show-asm.

The Rust version of the following example relies on the optimizations to achieve the desired behavior.

#include <cstddef>
#include <new>

struct LargeWidget {
  std::size_t id;
};

template <typename T>
extern void blackBox(T &x);

void doWork(void *scratch) {
  for (std::size_t i = 0; i < 100; i++) {
    auto *w(new (scratch) LargeWidget{.id = i});
    // use w
    blackBox(w);
    w->~LargeWidget();
  }
}

int main() {
  alignas(alignof(LargeWidget)) char
      memory[sizeof(LargeWidget)];
  void *w = memory;
  doWork(w);
}
#[derive(Default)]
struct LargeWidget {
    id: usize,
}

fn do_work(w: &mut LargeWidget) {
    for i in 0..100 {
        *w = LargeWidget { id: i };
        // use w
        std::hint::black_box(&w);
    }
}

fn main() {
    let mut scratch = LargeWidget::default();
    do_work(&mut scratch);
}

Adding in a Drop implementation for LargeWidget does result in the drop function being called on each loop iteration, but makes the generated assembly much harder to read, and so has been omitted from the example.

Constructing large values on the heap

new in C++ constructs objects directly in dynamic storage, and placement new constructs them directly in the provided location. In Rust, Box::new is a normal function, so the value is constructed on the stack and then moved to the heap (or to the storage provided by the custom allocator).

While the initial construction of the value on the stack can sometimes be optimized away, in order to guarantee that the stack is not used for the large value requires the use of unsafe Rust and MaybeUninit. Additionally, the mechanisms available for initializing a value on the heap do not guarantee that the values will not be created on the stack and then moved to the heap. Instead, they just make it possible to incrementally initialize a structure (either field-by-field or element-by-element), so that the entire structure does not have to be on the stack at once. The same optimizations do apply, however, and so the additional copies might be avoided.

#include <iostream>
#include <memory>

int main() {
  constexpr unsigned int SIZE = 8000000;
  std::unique_ptr b = std::make_unique<
      std::array<unsigned int, SIZE>>();
  for (std::size_t i; i < SIZE; ++i) {
    (*b)[i] = 42;
  }

  // use b so that it isn't optimized away
  for (std::size_t i; i < SIZE; ++i) {
    std::cout << (*b)[i] << std::endl;
  }
}
fn main() {
    const SIZE: usize = 8_000_000;

    // optimization here makes it not overflow
    // the stack with opt-level=2
    let mut b = Box::new([0; SIZE]);
    for i in 0..SIZE {
        b[i] = 42;
    }

    // use b so that it isn't optimized away
    std::hint::black_box(&b);
}

On the other hand, directly defining the array as [42; SIZE] does result in the value being first constructed on the stack, which produces an error when run.

fn main() {
    const SIZE: usize = 8_000_000;

    let b = Box::new([42; SIZE]);

    // use b so that it isn't optimized away
    std::hint::black_box(&b);
}
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Aborted (core dumped)

While construction of the values directly on the heap is not possible to enforce, it is possible to incrementally construct the value by using unsafe Rust, which avoids overflowing the stack. This technique relies on both MaybeUninit and addr_of_mut!.

fn main() {
    const SIZE: usize = 8_000_000;
    let mut b = Box::<[i32; SIZE]>::new_uninit();
    let bptr = b.as_mut_ptr();
    for i in 0..SIZE {
        unsafe {
            std::ptr::addr_of_mut!(((*bptr)[i])).write(42);
        }
    }

    let b2 = unsafe { b.assume_init() };

    for i in 0..SIZE {
        println!("{}", b2[i]);
    }
}

Depending on what is need, this particular use can be generalized.

#![allow(unused)]
fn main() {
fn init_with<T, const SIZE: usize>(
    f: impl Fn(usize) -> T,
) -> Box<[T; SIZE]> {
    let mut b = Box::<[T; SIZE]>::new_uninit();
    let bptr = b.as_mut_ptr();
    for i in 0..SIZE {
        unsafe {
            std::ptr::addr_of_mut!(((*bptr)[i]))
                .write(f(i));
        }
    }

    unsafe { b.assume_init() }
}
}

Note that a more idiomatic way to deal with a large array on the heap is to represent it as either a boxed slice or a vector instead of a boxed array, in which case using iterators to define the value avoids constructing it on the stack, and does not require the use of unsafe Rust.

#![allow(unused)]
fn main() {
fn init_with<T, const SIZE: usize>(
    f: impl Fn(usize) -> T,
) -> Box<[T]> {
    (0..SIZE).map(f).collect()
}
}