Advanced lifetime control
storage<T> separates object lifetime from storage lifetime, while pin<T> gives a live value a stable address from the moment construction begins.
Types and instances
Angle brackets contain types. Ordinary constructor and assignment syntax starts a lifetime in storage; destruct(value) ends it early without freeing the backing memory.
Typed storage
storage<T> contains correctly aligned storage for T and tracks whether a live T occupies it. A default-created wrapper is Empty and does not construct T. Its Empty/Initialized state is managed through normal lifetime operations and is not exposed as a public field.
sizeof(storage<T>) is the wrapper size and is not required to equal sizeof(T). alignof(storage<T>) is sufficient for T, and the contained value address passed to a constructor is correctly aligned.
storage<Resource> slot; // Empty: no live Resource
slot = Resource(firstArgument); // Empty → Initialized
slot.Use();
destruct(slot); // Initialized → Empty
slot = Resource(secondArgument); // storage may be reused
Resource value = move slot; // slot becomes Empty
storage<Resource> slot = Resource(...) and a later slot = Resource(...) both construct directly in the wrapper's contained-value area. The constructor receives that final Resource address as this; no temporary live Resource is constructed and copied afterward. Primitive initialization such as storage<int> value = 42 is a direct store into the same area.
struct SelfReference
{
public SelfReference* Self;
public SelfReference() { Self = this; }
}
storage<SelfReference> self = SelfReference();
// self.Self == &self: the constructor observed the final address
Using empty storage, assigning into initialized storage, or invoking destruct on empty storage is invalid. Xenon reports the error at compile time when flow analysis proves the state; indirect or otherwise unknown state is checked at runtime. If initialized storage itself leaves scope, Xenon automatically invokes the contained T destructor exactly once. An early destruct or move marks the persistent state Empty and prevents a second destructor call.
{
storage<Resource> value = Resource();
} // automatic ~Resource()
storage<Resource> outer;
{
outer = Resource();
}
outer.Use(); // still live: the storage belongs to the outer scope
The state travels with the wrapper through fields, pointers, heap allocation, arrays and containing-aggregate moves. Compile-time flow state is used for diagnostics and optimization, while automatic cleanup consults the wrapper's persistent state whenever it is not statically known.
The one persistent state belongs to the complete stored T. Ordinary reads, writes and method calls on fields remain valid, but a lifetime operation cannot target only a field or deeper descendant: move slot.Child and destruct(slot.Child) are rejected. The same rule applies through storage<T>&, storage<T>* and indexed storage. Move or destroy the complete storage value so the single Initialized/Empty state remains truthful.
Calling a method through storage does not bypass this rule. A method may mutate the contained T normally, but it may not return with a child persistently moved or destructed, because the wrapper cannot represent a partially live value. A method that restores every changed child on every reachable exit has no caller-visible lifetime effect and remains valid.
References and lifetime state
A storage<T>& is an exclusive mutable reference to the storage place itself, including its Empty/Initialized lifetime state. Initialization, explicit destruction and reuse through that reference update the same state observed through the original place. A readonly storage<T>& is a shared immutable reference and cannot initialize, destroy or move from the storage.
storage<Resource> slot;
storage<Resource>& reference = slot;
reference = Resource();
destruct(reference);
reference = Resource(); // a second, distinct Resource lifetime
Use a mutable storage<T>& parameter when a callable must manage the caller's storage lifetime. Calls through such a parameter update the wrapper's persistent runtime state. Unless the callable has a more precise effect contract, caller flow analysis conservatively treats the storage as possibly initialized afterward and lets that runtime state govern cleanup and guarded access.
void Reset(storage<Resource>& value)
{
destruct(value);
value = Resource();
}
Reference-return provenance is preserved for storage itself. A function returning the one known storage<T>& argument may be used to destroy or reconstruct that same wrapper, and caller flow state is updated for the resolved storage place. Forwarding only the contained T& remains access-only and cannot bypass the wrapper state.
storage<Resource>& ForwardStorage(storage<Resource>& value)
{
return value;
}
destruct(ForwardStorage(slot)); // slot becomes Empty
Whole-storage authority never propagates across the boundary into the contained value. A return typed as T&, Field&, or a deeper descendant remains an access-only view, even when its origin can be traced exactly through nested or generic forwarding.
Resource& GetValue(storage<Resource>& value)
{
return value;
}
// destruct(GetValue(slot)); // invalid
// Resource result = move GetValue(slot); // invalid
A T& obtained from initialized storage is access to the currently live T, not a lifetime-management handle. It may read or mutate that value, but cannot be the operand of destruct or move: those operations must target the authoritative storage<T> place or a mutable storage<T>& so its state can become Empty. Reusing the same address never retargets an old reference to a new lifetime.
storage<Resource> slot = Resource();
Resource& reference = slot;
destruct(slot); // error while reference has a later use
reference.Use();
// destruct(reference); // invalid: no storage lifetime authority
// Resource moved = move reference; // invalid for the same reason
Fields and methods
struct Holder
{
public storage<Resource> Value;
public void Create() { Value = Resource(); }
public void Destroy() { destruct(Value); }
}
An uninitialized storage field is Empty, so destroying its containing object does not call ~Resource(). Once initialized—even through a method—the field is automatically destroyed with the containing object unless an earlier destruct or move has made it Empty. Generated field cleanup rechecks the state after a user-defined containing destructor, preventing double destruction.
Heap storage
storage<Resource>* ptr = new storage<Resource>();
// allocation exists, but no Resource exists yet
*ptr = Resource();
destruct(*ptr);
*ptr = Resource(); // same contained-value address, a new lifetime
delete(ptr); // destroys the live Resource, then releases the wrapper
new storage<T>() creates an Empty heap wrapper; it is distinct from new T(), which creates a live T. Likewise, storage<Resource>* is a pointer to storage for a Resource, while storage<Resource*> is inline storage for a Resource pointer value.
An ordinary mutable raw T* provides direct lifetime authority without adding ownership. Assignment directly through *pointer or pointer indexing starts a lifetime in place and never destroys a previous value automatically. Member assignment does not inherit placement semantics: pointer->Field, (*pointer).Field, indexed pointee members, and nested member paths use ordinary live-field replacement and clean up the previous field value. destruct and move may still target raw pointee places explicitly. Unlike storage<T>, raw memory has no persistent Initialized/Empty bit, so correctness remains entirely the program's responsibility.
Resource* ordinary = cast<Resource*>(malloc(sizeof(Resource), alignof(Resource)));
*ordinary = Resource(); // placement construction
Resource value = move *ordinary;
*ordinary = Resource(); // a distinct lifetime at the same address
destruct(*ordinary); // storage remains allocated
free(ordinary); // bytes only; no destructor
storage<Resource>* checked = new storage<Resource>();
*checked = Resource();
destruct(*checked); // persistent state becomes Empty
*checked = Resource();
delete(checked); // observes state, destroys, deallocates
The distinction is metadata, not lifetime authority: both mutable T* and storage<T>* permit explicit lifetime operations. The storage wrapper tracks and checks one complete contained lifetime; an ordinary pointer does not. A readonly T* or readonly storage<T>* permits access only and rejects placement assignment, move, destruction, and delete.
For a safe reference derived from a known pointer place—including a local or nested field—the compiler keeps that pointee borrow alive and rejects free(pointer) or delete(pointer) until the reference's final use. The same non-lexical rule protects destruct(*pointer) and moves from raw or checked storage. Arbitrary raw-pointer aliases remain outside the safe-reference provenance model: Xenon does not attempt whole-program raw-pointer alias analysis, and code that obscures such provenance is unsafe by construction.
Container capacity
storage<Resource>[] slots = new storage<Resource>[capacity];
slots[count] = Resource();
count++;
Allocating capacity does not construct each T. Every array element is a complete storage wrapper with its own Empty/Initialized state. Runtime-indexed construction and destruction update the selected element, and normal array cleanup destroys only initialized elements. Container code may independently track count, free lists, occupancy or generations for its algorithm.
Ownership expressions and lifetime operations
move value, lock weakValue, new Type(...), malloc(...) and calloc(...) are value-producing expressions. They may be returned, passed as arguments or composed wherever their result type is accepted. In contrast, destruct(value), free(pointer) and delete(pointer) perform standalone lifetime or memory actions and do not produce a value.
Resource destination = move source;
shared<Resource> resource = lock observer;
Resource* pointer = new Resource();
destruct(destination);
delete(pointer);
Pinned values
pin<T> guarantees address stability from the beginning of construction. The constructor executes at the final pinned address, and the value cannot then be copied, moved, passed or returned by value. Pinning does not extend lifetime; references and pointers are invalid after destruction.
pin<CallbackState> state = CallbackState();
CallbackState* address = &state;
state.Register();
// move state; // error: pinned values cannot be relocated
An aggregate containing an inline pinned field is also non-relocatable. Pinned fields are initialized directly at their final field address. pin<storage<T>> combines stable storage with restartable contained lifetimes and the same automatic scope cleanup as ordinary storage. Ownership handles are not wrapped in pin: the pointee of unique<T> or shared<T> already resides in stable heap storage.
pin<storage<Resource>> value = Resource();
destruct(value);
value = Resource(); // same stable address, new lifetime
Value lifetime dependencies
A value can carry inferred compile-time lifetime dependencies in addition to its ordinary type. A resumable result inherits dependencies from external borrows retained across any suspension point, including reference parameters, array backing storage, a borrowed receiver and borrowed captures. A borrow used only before suspension creates no result dependency. Frame-owned values use the existing pin<Frame> rules.
Result Forward(Resource& resource)
{
return ReadLater(resource); // result lifetime <= resource lifetime
}
Result Invalid()
{
Resource local = Resource();
return Forward(local); // error: returned value outlives local
}
Copy and move preserve these constraints. Ordinary returns can export dependencies on parameters or the receiver; a local owner cannot escape through such a result. Assignment checks the destination object's region, including fields and reference aliases. Generic forwarding and XELIB metadata preserve the same contracts without a new source-level lifetime syntax or runtime representation.
A pending operation can continue after its handle is destroyed. A caller borrowing a local owner must complete the operation through await before destroying or moving that owner, including on exceptional exits. Merely declaring the handle in a nested scope is insufficient. See resumable lifetimes for examples and the completion rules.