Rust dev here, I think this proposal is off to a pretty good start, and will probably have significant safety benefits. However, there are still some things I think need to be addressed at some point.
First off, there is the 'static
lifetime. In Rust, the 'static
lifetime is a special lifetime that outlives all other lifetimes. Roughly speaking, any 'static
object can be assumed to be valid for the entire length of the program. For example, values, references to global variables, and data leaked on the heap can all typically be considered to have a 'static
lifetime.
Another thing to consider is what happens when the returned lifetime of a pointer is not bounded to the argument lifetimes. In the most simple case, something like
int* $a leak(){
//create an int and leak it on the heap
}
In rust, this is handled by forcing the returned lifetime to be either 'static
or unbounded. Unbounded lifetimes are chosen by the caller of the function and are equivalent to 'static
in terms of inference most of the time.
Another consideration is what to do with function pointers. Getting these right is surprisingly difficult. Naively one might think the following code is valid:
void use_callback( (*callback)(int* $a) ){
int foo = 42;
callback(&foo);
}
However, it isn’t- the callback requires a lifetime of at least as long as the call site of use_callback
, while the pointer passed is defined in the local scope and will be invalidated upon return. Rust solves this problem with Higher-Ranked Trait Bounds (HRTBs), which confusingly have far more to do with lifetimes than traits. Rust desugars the function pointer fn(&i32)
into for<'a> fn(&'a i32)
instead of fn(&'a i32)
. In words, using HRTBs means the function is valid for all lifetimes 'a
rather than some specific lifetime 'a
. In practice, this means the lifetime of the function pointer and its arguments are dissociated, so
fn use_callback(callback: for<'a> fn(&'a i32)){
let foo = 42;
callback(&foo);
}
is valid, but
fn use_callback<'a> (callback: fn(&'a i32)){
let foo = 42;
callback(&foo); //error[E0597]: `foo` does not live long enough
}
is not.
Those were just the first few things that popped into my head, there are plenty of other edge cases to consider as well:
-
I want to use lifetime checks in most of my code, but some of the stuff I’m doing can’t be modeled with lifetimes. How can I opt-out of checking just those parts?*
-
I casted my class with a lifetime to a base class without one. Now what?**
-
For my code to be modeled correctly, I need to lengthen a lifetime. How can I do that?***
-
When working with a template parameter T
can I assert that T
must have a certain lifetime?****
-
My object Foo
shouldn’t be used after the object Bar
is destroyed, but Foo
doesn’t have a reference to Bar
. Is there a way to add a phantom lifetime to Foo
?*****
There are most definitely other small issues to worry about beyond those, but that’s what I can think of at the moment. My biggest concern with all of this is that the lifetime model may struggle to integrate properly with the language, especially since there is no borrow checker, and might not be as useful as it seems because of it.
Anyways, while I don’t work with C++ very often, I think this could be quite useful, and I am interested to see where this goes.
*Rust handles this with a divide between references and safe code; and raw pointers and unsafe
code. This will obviously not work for C++.
**Rust has a similar problem with trait objects and handles this by associating a lifetime with trait objects that are 'static
by default.
***In Rust, the safest way to do this is mem::transmute
, although casting a reference to a pointer and back can also do this.
****This is an important part of rust’s lifetime system because references aren’t allowed to outlive the data they point to.
*****Rust uses PhantomData
for this purpose. However, it is mainly used to add lifetime information to a pointer that otherwise wouldn’t have any. Because C++ doesn’t have the same distinction between a pointer and reference that rust does, this would be a much more niche use case, because most of the time just adding a pointer to the parent object would be fine.