For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).
Requiring explicit destruction
Normally, Mojo automatically destroys a value after its last use. Explicit destruction lets a type opt out of that behavior and require its values to be deliberately destroyed. A type that does this is an explicitly destroyed type, sometimes called a linear type.
To require explicit destruction, opt the type out of Deinitable by adding
not Deinitable to its conformance
list:
@fieldwise_init
struct Example(not Deinitable else "call 'cleanup()'"):
def cleanup(deinit self):
# perform cleanup operations
pass
def main():
var value = Example()
# use value
value^.cleanup()
Provide named deinitializer methods that use the deinit self argument
convention.
Values that require explicit destruction must be consumed by a named deinitializer or transferred out of scope. Otherwise, the compiler reports an error.
When to use explicit destruction
Choose explicit destruction when cleanup must be controlled in code. Common cases include:
- Cleanup can fail and requires error handling.
- Multiple cleanup paths are possible.
- The order of cleanup operations matters.
- Cleanup is expensive and should be deliberate.
Examples:
# Multiple cleanup paths
@fieldwise_init
struct Transaction(not Deinitable else "call 'commit()' or 'rollback()'"):
def commit(deinit self) raises: # Offers error handling
# ...
pass
def rollback(deinit self):
# ...
pass
# Order matters
struct MutexGuard(not Deinitable else "call 'unlock()' to release the lock"):
# Must be called to release the lock before other operations
def unlock(deinit self):
# ...
pass
Raising deinitializers
__deinit__() can't raise; the compiler rejects the
raises keyword on it. A named
deinitializer can raise, which lets cleanup report failure to its caller.
Because deinit self consumes the value at the call, the binding is consumed
whether the call returns or raises. The caller can't invoke another
deinitializer on that same instance.
If the type contains explicitly destroyed fields, the deinitializer must dispose of them before it raises. Implicitly destructible fields are cleaned up during unwinding.
Custom error messages
Add a message to the opt-out with else to improve compiler diagnostics:
struct CustomFileHandle(
not Deinitable else "call 'save_and_close()' or 'discard()'"
):
def save_and_close(deinit self) raises:
pass
def discard(deinit self):
pass
Make the error message actionable and follow Mojo best practices: use lower
case for call, put method and function names in single quotes, include
the parentheses, and omit the final period.
Parameterized code and explicit destruction
Parameterized code using broad constraints like AnyType and Movable can
accept both implicitly and explicitly destroyed values. However, that code
can't destroy values that require explicit destruction except under
specific circumstances.
These are the key patterns:
# Error if T requires explicit destruction
def owning_function[T: AnyType](var value: T):
pass # Error: value abandoned here
# Constrain T to Deinitable when implicit destruction is acceptable
def deinitable_function[T: Deinitable](var value: T):
pass # value.__deinit__() called automatically
# Return the value so the caller keeps destruction responsibility
def pass_through_function[T: Movable](var value: T) -> T:
return value^
# Accept a deinitializer so the function can destroy the value
def consuming_function[
T: Movable, //, Cleanup: def(var T)
](var value: T, consume: Cleanup):
consume(value^) # deinitialize here
Example types for parameterized destruction
The following examples use two types with different destruction requirements:
Basic is destroyed implicitly, while Tally requires explicit destruction.
from std.memory import Allocation, Layout, alloc, dealloc
@fieldwise_init
struct Basic(Movable):
var string: String
def __deinit__(deinit self):
print(t"Destroying Basic: {self.string}")
struct Tally(
Movable, not Deinitable else "call 'destroy()' to free the counters"
):
var counts: Allocation[Int64]
def __init__(out self, buckets: Int):
self.counts = alloc(Layout[Int64](length=buckets))
self.counts.unsafe_span().fill(0)
def record(mut self, bucket: Int):
self.counts.unsafe_span()[bucket] += 1
def destroy(deinit self):
print(t"Destroying Tally: {self.counts.unsafe_span()}")
dealloc(self.counts^)
Parametric pass-through
A parameterized pass-through function doesn't need to know its value's destruction model. The caller retains responsibility for ending the value's lifetime:
def pass_through[T: Movable](var value: T) -> T:
# perform work with value
return value^
Since the function doesn't maintain value ownership, it doesn't need to know how to destroy it:
var tally = Tally(3)
tally.record(2)
tally = pass_through(tally^)
tally^.destroy() # Destroying Tally: [0, 0, 1]
var basic = Basic("Hello")
_ = pass_through(basic^) # Destroying Basic: Hello
Parametric consumption: Movable
Pass a deinitializer to a parameterized function so it knows how to consume
any Movable value. The caller may transfer the value or a copy, knowing
it won't be abandoned.
def consuming_method[
T: Movable, //, Cleanup: def(var T)
](var value: T, consume: Cleanup):
print(t"Consuming: {reflect[T].name()}")
consume(value^)
Each explicitly destroyed type has an unknown set of deinitializer methods. Parametric code can't anticipate what these are. Using a lambda helps you pass a type-specific deinitializer to a consuming parametric function.
For example, a Tally value:
var tally = Tally(2)
tally.record(1)
comptime tally_consumer = lambda (var t: Tally): t^.destroy()
consuming_method(tally^, tally_consumer)
# Output:
# Consuming: <module_name>.Tally <- consuming_method()
# Destroying Tally: [0, 1] <- destroy()
Parametric consumption: Deinitable
Every Deinitable type has a __deinit__(), either one you write or one
Mojo synthesizes. Because the name is always the same, one generic lambda,
implicit_consumer, can consume any of them:
# Works across all Deinitable types
comptime implicit_consumer = lambda [T: Deinitable](
var value: T
): T.__deinit__(value^)
For example, a Basic value:
var basic = Basic("World")
consuming_method(basic^, implicit_consumer[Basic])
# Output:
# Consuming: <module_name>.Basic <- consuming_method()
# Destroying Basic: World <- __deinit__()
Related
- Automatic value destruction - Complete coverage of value destruction and lifetime management
AnyType- Base trait for all typesDeinitable- Trait for automatically deinitializable types