PhantomData *const T causing issues with lifetimes

I'm having issues with storing a type on a struct using PhantomData. In the PhantomData docs (PhantomData in std::marker - Rust), it suggests using PhantomData<*const T> when the compiler doesn't have to worry about lifetimes - but it still seems to be an issue.

Is it possible to achieve this without Rust thinking I'm trying to smuggle data out of a closure?

struct Logger<M> {
    __message_type: std::marker::PhantomData<*const M>
}

impl<M: std::fmt::Debug> Logger<M> {
    fn log(&mut self, message: &M) {
        println!("{:?}", message);
    }
}

#[derive(Debug)]
struct Message<'a> {
    caller: String,
    error: &'a String
}

fn create_error<F: FnMut(&String)>(mut func: F) {
    let s = String::from("Hello");
    func(&s)
}

fn main() {
    let mut logger = Logger { __message_type: Default::default() };
    
    create_error(|error| {
    
        let message = Message {
            caller: "Me".into(),
            error
        };
        
        logger.log(&message);
    });
    
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error: borrowed data cannot be stored outside of its closure
  --> src/main.rs:29:13
   |
23 |     let mut logger = Logger { __message_type: Default::default() };
   |         ---------- ...so that variable is valid at time of its declaration
24 |     
25 |     create_error(|error| {
   |                  ------- borrowed data cannot outlive this closure
26 |     
27 |         let message = Message {
   |                       ------- cannot infer an appropriate lifetime...
28 |             caller: "Me".into(),
29 |             error
   |             ^^^^^ cannot be stored outside of its closure

error: aborting due to previous error

error: could not compile `playground`.

To learn more, run the command again with --verbose.

The issue is that you are creating a Logger<Message<'a>> outside the closure, so 'a must be some lifetime that is longer than the closure body. (You can't store a type with a short lifetime in a variable with a longer lifetime.) Then you are passing it a Message<'b> where 'b is shorter than the closure body.

This doesn't work because the log method takes &mut self which is invariant in M. This means you can't pass it a Logger<Message<'long>> where a Logger<Message<'short>> is expected.

If you can change log to take &self (playground) then it will compile, but you might need to make other changes to your code (like using interior mutability) for this to work.

Another approach might be to remove the type parameter M from the Logger struct, and make it a parameter of the log method instead.

You also shouldn't need to add the M type parameter to your logger. Based on the usage shown here you can make the Logger::log() method generic over any M: Debug.

(playground)

(this may or may not be relevant depending on how cut down the example is)

Thanks for the explanation @mbrubeck! I think I'm going to have to resort to using interior mutability. I was optimistic after reading the following in the docs that PhantomData might be special, but it seems that's not the case.

Adding a field of type PhantomData<T> indicates that your type owns data of type T . This in turn implies that when your type is dropped, it may drop one or more instances of the type T . This has bearing on the Rust compiler's drop check analysis.

If your struct does not in fact own the data of type T , it is better to use a reference type, like PhantomData<&'a T> (ideally) or PhantomData<*const T> (if no lifetime applies), so as not to indicate ownership.

std::marker::PhantomData

Thanks @Michael-F-Bryan, the example I showed was a cut-down version that just showed the issue I was running into with the lifetimes and PhantomData - that being said my actual version wasn't that much longer!

If you're interested here's that more thorough example showing what I was really trying to do:

trait Node<Context> {
    fn process(&mut self, frames: usize, context: &mut Context);
}

struct Graph<N: Node<Context>, Context> {
    nodes: Vec<N>,
    __context: std::marker::PhantomData<*const Context>,
}

impl<N: Node<Context>, Context> Graph<N, Context> {
    fn process(&mut self, frames: usize, context: &mut Context) {
        for node in self.nodes {
            node.process(frames, context);
        }
    }
}

struct TestNode;

impl Node<AudioContext<'_>> for TestNode {
    fn process(&mut self, frames: usize, context: &mut AudioContext) {}
}

fn process_audio<F: 'static + FnMut(&Vec<f32>)>(mut callback: F) {
    let mut input = vec![0.; 10];
    callback(&mut input)
}

struct AudioContext<'a> {
    audio: &'a Vec<f32>,
}

fn main() {
    let graph = Graph {
        nodes: vec![TestNode],
        __context: Default::default(),
    };

    process_audio(move |audio| {
        let mut context = AudioContext { audio };

        graph.process(0, &mut context)
    });
}

(playground)

Basically I wanted to allow users to be able to provide a type and then the process method of both the parent and the children would only be able to take that type as the "context".

Thanks again for the help!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.