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
- situations where storage allocation is separate from
initialization such as in the
implementation of
std::vector
or memory pools, - situations where the structures need to be placed at a specific memory location, e.g., for working with memory-mapped registers, and
- storage reuse for performance reasons.
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.
Click here to leave us feedback about this page.#![allow(unused)] fn main() { fn init_with<T, const SIZE: usize>( f: impl Fn(usize) -> T, ) -> Box<[T]> { (0..SIZE).map(f).collect() } }