Consider a trait which requires a certain cleanup method to be implemented and also ensure that this cleanup is actually performed when an instance is dropped. That we can write something like this:
trait MyTrait {
fn cleanup(&self);
}
struct MyStruct;
impl MyTrait for MyStruct {
fn cleanup(&self) {
println!("Cleaning up");
}
}
impl Drop for MyStruct {
fn drop(&mut self) {
<Self as MyTrait>::cleanup(self);
}
}
fn main() {
{
let x = MyStruct;
}
println!("done");
}
and it will work, but it does not force the implementation of Drop
on MyStruct
and it might be easily omitted.
Is there a way to define MyTrait
in a way that will ensure or provide a default implementation of drop()
calling cleanup()
?
11
Here’s a possible solution using a macro:
mod library {
pub trait MyTraitImpls {
fn cleanup(&self);
}
/// Users are forbidden from implementing this.
#[doc(hidden)]
pub trait __Private {}
/// Implement via `MyTraitImpls` and `impl_my_trait!(...)`
pub trait MyTrait: MyTraitImpls + __Private {}
impl<T: MyTraitImpls + __Private> MyTrait for T {}
#[macro_export]
macro_rules! impl_my_trait {
($name: ident) => {
impl library::__Private for $name {}
impl Drop for $name {
fn drop(&mut self) {
<Self as library::MyTraitImpls>::cleanup(self);
}
}
}
}
}
use crate::library::*;
struct MyStruct;
impl MyTraitImpls for MyStruct {
fn cleanup(&self) {
println!("Cleaning up");
}
}
impl_my_trait!(MyStruct);
The user could bypass this by impl library::__Private for MyStruct {}
but at least you can’t do that accidentally.
You could write this as a procedural macro (#[derive(MyTrait)]
) to make it more idiomatic.
I have converged to an implementation which is arguably over-cautious, but seems to provide the required functionality. It requires a wrapper type though. Here is an example of generic “mapping” API, where the implementer is required to provide the code for performing the mapping and the code for unmapping (cleanup in the original question).
mod mapping {
use std::marker::PhantomData;
pub struct Token {
_priv: PhantomData<()>,
}
pub trait MappingTrait
{
type Params;
fn map(params: Self::Params, _t: Token) -> Self;
fn unmap(&self, _t: Token);
}
pub struct Mapping<M: MappingTrait>
{
mapping_impl: M,
}
impl <M: MappingTrait> Mapping<M> {
pub fn new(params: M::Params) -> Self {
Self {
mapping_impl: M::map(params, Token {_priv: PhantomData} ),
}
}
}
impl<M: MappingTrait> Drop for Mapping<M> {
fn drop(&mut self) {
self.mapping_impl.unmap(Token {_priv: PhantomData});
}
}
}
The required implementation of MappingTrait
requires the map
and unmap
methods to use a parameter of Token
type, which is not constructable outside of the module, so the implemented type cannot be used “directly” even in the module it is implemented in.
Then there is a wrapper type Mapping<M: MappingTrait>
, which can internally construct Token
and use the provided API. It is also implementing Drop
, where unmap
of the wrapped type is called.
Here is the example usage:
use mapping::*;
struct MyMappingImpl
{
some_data: usize,
}
impl MappingTrait for MyMappingImpl {
type Params = usize;
fn map(params: usize, _t: Token) -> Self {
println!("Mapping {}...", params);
MyMappingImpl {some_data: params}
}
fn unmap(&self, _t: Token) {
println!("Unmapping {}...", self.some_data);
}
}
fn main() {
{
let _m: Mapping<MyMappingImpl> = Mapping::new(42);
}
println!("Done");
}
This code will print
Mapping 42...
Unmapping 42...
Done
Link to Playground