Summary
Introduce a new defer {...}
statement syntax with keyword defer
for registering code block to be executed on current scope is exited.
Motivation
It is a common pattern to use drop guards to run some cleanup code reliably even on the panic unwinding or async task cancelation. Many stdlib functions define its own internal type which implements Drop trait for this purpose. More generally scopeguard crate is used by many crates for it. But these approaches have a fundamental limitation. scopeguard mentions:
If the scope guard closure needs to access an outer value that is also mutated outside of the scope guard, then you may want to use the scope guard with a value. The guard works like a smart pointer, so the inner value can be accessed by reference or by mutable reference.
This limitation is due to the fact that the guard is a value which borrows other values it needs to touch on its drop logic. It makes it not a trivial task to use those values while the guard is alive. Usually there are some workarounds like:
let (foo, bar) = &mut *scopeguard::guard((foo, bar), |(foo, bar)| { ... });
Which is not a very pleasant code. Not only it's too verbose, but also it undermines one of the purposes of the closure which doesn't force to specify all its captures. To solve it the language not a library needs to provide this functionality.
Guide-level explanation
A keyword defer
can be used to declare a defer statement(defer block). Its syntax is to prepend a defer
keyword before a block expression.
defer {
println!("Hello, defer!");
}
A defer block is a statement(not an expression) which executes its inner block when the scope it declared is exited. Since it's a statement which doesn't produce any value, it doesn't borrow any values it captures until it got actually executed.
let mut numbers = vec![1, 2]
defer {
println!("numbers: {numbers:?}");
}
numbers.push(3);
// prints `numbers: [1, 2, 3]`
It still be executed on panic unwinding and async task cancelation.
std::panic::catch_unwind(|| {
defer { println!("declared before panic"); }
println!("now go panicking");
panic!();
defer { println!("declared after panic"); }
});
// prints:
// now go panicking
// declared before panic
let handle = task::spawn(async {
defer { println!("declared before sleep"); }
async_sleep(an_hour).await;
defer { println!("declared after sleep"); }
});
sleep(a_second);
handle.abort();
// prints: `declared before sleep`
Like the drop glue, it is executed in the reverse order of its declaration. This allows it safe to use any variables it can see within the defer statement.
let a = "a".to_owned();
defer { println!("first, {a}"); }
let b = "b".to_owned();
defer { println!("second, {a} {b}"); }
let c = "c".to_owned();
defer { println!("third, {a} {b} {c}"); }
// prints:
// third, a b c
// second, a b
// first, a
Like closures and async blocks, you can return
from the defer blocks. It escapes from the current defer statement, but it doesn't affect executions of other defer statements.
defer {
println!("1");
}
defer {
println!("2");
return;
println!("3");
}
println!("4");
defer {
println!("5");
}
// prints:
// 4
// 5
// 2
// 1
Like the if expression without any else
part, inner block expression of the defer statement can only returns ()
type.
defer {
return 42; // compile error
};
defer { 7 } // compile error
defer { 7; } // ok
You can't move out values if it's used/captured by some defer statement.
let s = "some text".to_owned();
defer { println!("{s}"); }
drop(s); // compile error
Since it should be executed on drop, you can't .await
directly within defer statement even its declared scope allows it.
async fn foo() {
defer {
async_sleep(an_hour).await; // compile error
}
}
Reference-level explanation
Since the keyword defer
is not reserved, it is used as a raw keyword(k#defer
).
From the language implementation's perspective, a defer statement is like a let statement with ZST variable but it has custom code block instead of the drop glue. Due to the semantics of the return
within the defer statement, it's also possible to implement it as a closure constructed at the position of the drop glue and immediately call it.
From the type checker's perspective, it can be handle as a normal block expression statement. For the type system it doesn't matter whether the block is executed on its declaration or on its scope exits. Since the desugaring should happen as a part of the construction of the MIR, any processes that happens later including the borrow checker doesn't need to know about this feature.
Drawbacks
Compilers need to run more than single function call on the drop glue position. it may not be trivial to implement.
It allows code which doesn't run at the position it is written. It may confuse readers who're not used to the concept of the drop guard.
Its semantics doesn't match exactly with the Go's defer
statement. It may confuse people who're used to that language.
Rationale and alternatives
The enforced block({}
) gives visual isolation between the code that will be executed immediately and the code that will be executed later. It has its own scope so it's natural that variables declared within the defer statement can't be used from the outside. Also it prevents to put semicolons at the end of the block expression.
Technically the scopeguard
crate and/or the drop guard newtype pattern can cover every use cases specified above.
Prior art
The scopeguard
crate is a popular crate to provide library level solution to the problem this RFC tries to solve. The defer block statement is designed to be a superset of the defer!
macro in this crate.
Unresolved questions
Bikeshedding. Is defer
the best name/keyword for this feature?
Future possibilities
- Conditional execution based on what triggered the scope exit - normal execution/panic unwinding/async task cancelation.