This doesn't compile:
const fn f<T>(slot: &mut MaybeUninit<T>, value: Option<T>) {
if let Some(value) = value {
// use value somehow
slot.write(value);
}
}
error[E0493]: destructor of `Option<T>` cannot be evaluated at compile-time
--> src/lib.rs:38:42
|
38 | const fn f<T>(slot: &mut MaybeUninit<T>, value: Option<T>) {
| ^^^^^ the destructor for this type cannot be evaluated in constant functions
We can unsafely duplicate T if the value is Some<T>, and forget the Option<T> to make the compiler happy:
const fn f<T>(slot: &mut MaybeUninit<T>, value: Option<T>) {
if let Some(value) = value.as_ref() {
let value = unsafe { std::ptr::read(value) };
// use value somehow
slot.write(value);
}
std::mem::forget(value);
}
However, depending on what you do with the value a panic could cause a double free. So I thought let's forget the value first:
const fn option_manually_drop<T>(value: Option<T>) -> Option<std::mem::ManuallyDrop<T>> {
let out = match value.as_ref() {
Some(v) => Some(std::mem::ManuallyDrop::new(unsafe { std::ptr::read(v) })),
None => None,
};
std::mem::forget(value);
out
}
const fn f<T>(slot: &mut MaybeUninit<T>, value: Option<T>) {
if let Some(value) = option_manually_drop(value) {
let value = std::mem::ManuallyDrop::into_inner(value);
// use value somehow
slot.write(value);
}
}
Am I overcomplicating this? Is there a better solution/implementation?
1 Like
Oh Map on non-copy option in const context mentions using:
const fn map_wrapper<T>(option: Option<T>) -> Option<Wrapper<T>> {
match option {
// call unwrap() instead of destructuring
Some(_) => Some(Wrapper(option.unwrap())),
// call forget() instead of dropping
option @ None => {
mem::forget(option);
None
}
}
}
So we can avoid using unsafe:
const fn option_manually_drop<T>(value: Option<T>) -> Option<std::mem::ManuallyDrop<T>> {
if value.is_some() {
Some(std::mem::ManuallyDrop::new(value.unwrap()))
} else {
std::mem::forget(value);
None
}
}
const fn f<T>(slot: &mut MaybeUninit<T>, value: Option<T>) {
if let Some(value) = option_manually_drop(value) {
let value = std::mem::ManuallyDrop::into_inner(value);
// use value somehow
slot.write(value);
}
}
Trying out things myself, I came up with
use std::mem;
use std::mem::MaybeUninit;
const fn f<T>(slot: &mut MaybeUninit<T>, mut value: Option<T>) {
if value.is_some() {
slot.write(value.take().unwrap());
}
mem::forget(value); // only `mem::forget`s a `None`, but makes compiler happy
}
as something that’s accepted by the compiler. Looking now at your own reply, it looks not completely unlike what you’ve already found, but still maybe it’s slightly simpler.
3 Likes
Thanks.
For the actual implementation I was working on I now use:
#[inline]
const fn maybe_uninit_from_option<T>(mut value: Option<T>) -> (MaybeUninit<T>, bool) {
let is_init = value.is_some();
let maybe_init = if is_init {
MaybeUninit::new(value.take().unwrap())
} else {
MaybeUninit::uninit()
};
std::mem::forget(value); // work around imprecise drop check in const fns, see https://github.com/rust-lang/rust/issues/73255.
(maybe_init, is_init)
}
to support
impl<T> StackVec<{$n}, T> {
pub const fn cond_push(self, value: Option<T>) -> StackVec<{$n + 1}, T> {
let (item, init) = maybe_uninit_from_option(value);
let StackVec {
data: [$($v),*],
len
} = self;
StackVec {
data: [$($v,)* item],
len: len + if init { 1 } else { 0 },
}
}
}
The problem is that the drop glue for T can run arbitrary code, which doesn't need to be const-safe. You can avoid the error if you guarantee that T doesn't have drop glue. I.e. this compiles:
const fn f<T: Copy>(slot: &mut MaybeUninit<T>, value: Option<T>) {
if let Some(value) = value {
// use value somehow
slot.write(value);
}
}
Is it too restrictive for your use case? Since Drop cannot be called in const contexts, I struggle to think of an example in stable Rust where this would be too limiting.
On nightly, you can use the unstable const Destruct bound to achieve the more general effect:
#![feature(const_destruct, const_trait_impl)]
use std::marker::Destruct;
use std::mem::MaybeUninit;
const fn f<T: const Destruct>(slot: &mut MaybeUninit<T>, value: Option<T>) {
match value {
Some(v) => { slot.write(v); }
None => { },
}
}
Of course, ideally, your code should just work, since no value of T is dropped in the None branch. That requires more precise compiler analysis of drops, which is tracked by #![feature(const_precise_live_drops)].