Macro static_assert_generic::explicitly_drop

source ·
explicitly_drop!() { /* proc-macro */ }
Expand description

Experimental macro that forces variables of a certin type to not have their destructor run, using the same methods as static_assert!.
Its primary use case is being able to drop objects that require updating other structures. It also has some serious drawbacks, meaning that you should likely refrain from using it in any serious project.

§Example:

Consider a situation like this, where multiple allocators may be present at a time:

struct Allocator {
    ...
}

impl Allocator {
    pub fn alloc<T>(&mut self) -> Allocation<T> {
        todo!()
    }

    pub fn free<T>(&mut self, allocation: Allocation<T>) {
        // ...
    }
}

struct Allocation<T> {
    ptr_to_allocation: *const T
}

Idealy, when the Allocation runs out of scope, it would be freed by the same Allocator that allocated it. However, implementing such Drop functionality would require the allocation to also hold some kind of reference to said Allocator. This would double its size and may become an issue if Allocation appears often. Still, if the programmer forgets to free the Allocation a memory leak would take place.

Using explicitly_drop! would give a compile-time error if Allocation’s drop method appears anywhere in the code:

impl<T> Drop for Allocation<T> {
    explicitly_drop!(T => "Allocation must be freed explicitly!");
}

The free method would have to make sure that Allocation’s drop method doesn’t appear either.

pub fn free<T>(&mut self, allocation: Allocation<T>) {
    let allocation = std::mem::ManuallyDrop::new(allocation);
    // ...
}

Now if someone forgets to free an Allocation, the compiler will give an error (just like static_assert!, this isn’t caught by cargo check, and a full build is needed instead):

fn foo(allocator: Allocator) {
    let allocation: Allocation::<Whatever> = allocator.alloc();
    // ... allocation is not freed
    // allocation's drop method appears
}
 
foo(my_allocator);
 
// error[E0080]: evaluation of `<Allocation<T> as std::ops::Drop>::drop::Assert::<Whatever>::MANUAL_DROP` failed
//    |
//    |         explicitly_drop!(T => "Allocation must be freed explicitly!");
//    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'Allocation must be freed explicitly!'
//    |
//
// note: the above error was encountered while instantiating `fn <Allocation<Whatever> as std::ops::Drop>::drop`
//    |
//    | pub unsafe fn drop_in_place<T: ?Sized>(to_drop: *mut T) {
//    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

free (or prevent its drop method from appearing in any way) and the error dissapears:

fn foo(allocator: Allocator) {
    let allocation: Allocation::<Whatever> = allocator.alloc();
    // ... do something
    allocator.free(allocation);
}
 
foo(my_allocator);
 
// compiles just fine

§Drawbacks

To prevent constant evaluation from always happening, the functionality is dependant on at least one generic (be it type or const) from the type it’s implemented in, so if the type in question doesn’t have that the macro won’t work:

impl<T> Drop for Foo<T> {
    explicitly_drop!(T => "Dependant on type generic");
}
 
impl<T: ?Sized> Drop for Foo<T> {
    explicitly_drop!(T? => "If the type is unsized it needs special syntax");
}
 
impl<const C: u8> Drop for Foo<{C}> {
    explicitly_drop!(const C: u8 => "Dependant on const generic (specifying the type is needed)");
}
 
impl<const C: u8, const D: u16, T, U, V> Drop for Foo<{C}, {D}, T, U, V> {
    explicitly_drop!(const C: u8 => "Just one is needed, even if the type has more.");
}

Using a lifetime as a generic doesn’t work.

Secondly, the drop method might appear even when it doesn’t seem it should at first glance. For example, if a panic ever occours, all variables in scope, including those that need to be explicitly_dropped, have their drop method run, so even if the panic never occours at runtime, the simple appearance of drop will still cause a compile-time error:

fn bar(allocator: Allocator) {
    let allocation: Allocation::<Whatever> = allocator.alloc();
    if rand::random::<usize>() == 27 {
        panic!();
    }
    allocator.free(allocation);
}
 
foo(my_allocator);
 
// ... 'Allocation must be freed explicitly!' ...

A huge amount of functionality in rust can result in panics. Even if explicit panic!, todo!, or unwraps are avoided, these operations, and many more, can also panic:

  • Indexing into a container without bounds checking.
  • Basically every unchecked heap allocation.
  • Any mathematical operation that can over/underflow (on a debug build).
  • Possible division or modulo operation by 0.

This method also assumes that rust optimises out, and as such doesn’t attempt to evaluate constants if the method they are in isn’t use, which might not even always be the case.