Source
stdlib/memory/lease.hpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#pragma once5
/**6
* @file lease.hpp7
* @brief `memory::Lease<T, Mode>` — the only handle to an owned object.8
*9
* Move-only RAII. A lease does NOT hold a `std::lock`; the `Owner` coordinator grants logical10
* exclusion (readers share, a writer is alone) and the lease just carries a **release callback** it11
* fires on destruction to tell the owner "I'm done". Access:12
* - `r.read()` → `const T&` (a REFERENCE to the owned object — never a copy or raw pointer).13
* - `w.write(value)` → SETTER: replace the whole object. `write` NEVER returns an object.14
* - `w.write(index, v)` → set element `index` (Indexed sequences: vector/array/string/ndarray).15
* - `w.write(key, v)` → set `key` (Mapping containers: map/unordered_map/dict).16
* `valid()`/`expired()` track a shared `Gate` the owner flips to ask the holder to yield (drain, or an17
* immediate-write preempt); `on_interrupt(fn)` fires `fn` once per stop, when the holder observes it.18
*/20
#include <atomic>21
#include <concepts>22
#include <cstddef>23
#include <functional>24
#include <memory>25
#include <utility>27
#include "mode.hpp"28
#include "ownable.hpp"30
namespace cheatah::memory {32
namespace detail {33
/// Shared signal between the owner and a lease. The owner flips `valid` false to ask the holder to yield;34
/// the holder sets `acked` when it observes that and calls `wake` so the owner's cv re-checks. `wake`35
/// (owner-provided) synchronizes on the owner's mutex before notifying — the ack is one-shot, so an36
/// unsynchronized notify racing a waiter mid-predicate would be lost and the waiter would sleep forever.37
struct Gate {38
std::atomic<bool> valid{true};39
std::atomic<bool> acked{false};40
std::function<void()> wake; ///< set by the owner to notify its cv_ when the holder first acks.41
};42
} // namespace detail44
/**45
* @brief The only handle to an owned object — a move-only RAII lease granted by an `Owner`.46
*47
* `M` is `read` (shared) or `write`/`write_renewable` (exclusive). Reads go through `read(...)`; a48
* write lease sets through `write(...)`. The lease holds a direct pointer to the object plus the49
* release callback it fires on destruction. @tparam T the owned type (`Ownable`). @tparam M the mode.50
*/51
template <Ownable T, Mode M>52
class Lease {53
public:54
/**55
* The owner's grant path (called only by `Owner`). @complexity O(1). @alloc none.56
* @param obj pointer to the owned object.57
* @param gate the generation gate that carries the yield signal.58
* @param release callback fired once on destruction to tell the owner this lease is done.59
* @concurrency called by the owner with its coordinator mutex held — never construct one yourself.60
* @test Memory.EveryAccessorReturnsARequestNotABareLease61
* @test Memory.ReadLeasesCoexist62
*/63
Lease(T* obj, std::shared_ptr<detail::Gate> gate, std::function<void()> release) noexcept64
: obj_(obj), gate_(std::move(gate)), release_(std::move(release)) {}66
/// Move-construct, taking over @p o's grant (it is left released). @param o the lease to move from.67
/// @complexity O(1). @alloc none. @test Memory.EveryAccessorReturnsARequestNotABareLease68
/// @test Memory.ReadLeasesCoexist69
Lease(Lease&& o) noexcept { steal(o); }70
/// Move-assign: release ours, then take over @p o's grant. @param o source. @return `*this`.71
/// @complexity O(1). @alloc none. @test Memory.EveryAccessorReturnsARequestNotABareLease72
Lease& operator=(Lease&& o) noexcept { if (this != &o) { drop(); steal(o); } return *this; }73
Lease(const Lease&) = delete;74
Lease& operator=(const Lease&) = delete;75
/// Release the lease (fires the owner's release callback if still held).76
/// @complexity O(1). @alloc none.77
/// @concurrency the release callback takes the owner's coordinator mutex and wakes waiting78
/// requests (a draining writer proceeds once the last reader releases here).79
/// @test Memory.WriteWaitsForReadersToDrain80
~Lease() { drop(); }82
/// Read the whole object. Available on every lease. @return a `const T&` — never a copy or `T*`.83
/// @complexity O(1). @alloc none.84
/// @concurrency race-free while the lease is held: a writer cannot proceed until this lease85
/// releases (yielding on `!valid()` is cooperative, not forced). No lock is taken here.86
/// @test Memory.ReadReturnsAReferenceNotACopyOrPointer87
const T& read() const { return *obj_; }89
/// Read element @p index of an Indexed sequence: `r.read(i)`. Mirrors `w.write(i, v)`.90
/// @param index the position to read. @return a const reference to the element.91
/// @complexity `T::operator[]`. @alloc none.92
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects93
const auto& read(std::size_t index) const94
requires Indexed<T>95
{ return (*obj_)[index]; }97
/// Read the value at @p key of a Mapping: `r.read(k)`. Mirrors `w.write(k, v)`. Throws if absent98
/// (reading a missing key never inserts). @tparam K a type convertible to the key type.99
/// @param key the key to look up. @return a const reference to the mapped value.100
/// @complexity `T::at`. @alloc none when @p key already has the key type; a key that needs101
/// converting (`const char*` to a `std::string` key) builds a temporary.102
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects103
template <class K>104
requires (Mapping<T> && std::convertible_to<K, typename T::key_type>)105
const auto& read(K&& key) const106
{ return (*obj_).at(std::forward<K>(key)); }108
/// Read the first element (containers with `front()`: vector / deque / list / string …).109
/// @return a const reference to the first element. @complexity O(1). @alloc none.110
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects111
const auto& read_front() const112
requires HasFront<T>113
{ return (*obj_).front(); }115
/// Read the last element (containers with `back()`).116
/// @return a const reference to the last element. @complexity O(1). @alloc none.117
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects118
const auto& read_back() const119
requires HasBack<T>120
{ return (*obj_).back(); }122
/// Replace the whole object: `w.write(value)`. The primary write form. Write / write_renewable only.123
/// @param value the new value (moved in). @complexity O(1) plus assigning @p value.124
/// @alloc whatever `T`'s assignment allocates.125
/// @concurrency exclusive: no reader or other writer coexists while this lease is valid. A writer that126
/// observed `!valid()` must wait for it to flip back — writing while suspended races with the preempter.127
/// @test Memory.WriteWaitsForReadersToDrain128
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects129
/// @systest MemoryCheatah.ScalarWriteReadModifyWrite130
void write(T value)131
requires is_write_mode<M>132
{ *obj_ = std::move(value); }134
/// Set element @p index of an Indexed sequence: `w.write(i, v)`. Deduced. Write modes only.135
/// @tparam V the element value type. @param index the position to set. @param value the new element.136
/// @complexity `T::operator[]`. @alloc whatever the element assignment allocates.137
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects138
/// @systest MemoryCheatah.OwnerOfNdArrayElements139
template <class V>140
requires (is_write_mode<M> && Indexed<T>)141
void write(std::size_t index, V&& value)142
{ (*obj_)[index] = std::forward<V>(value); }144
/// Set @p key of a Mapping: `w.write(k, v)`. Deduced. Write modes only. @tparam K key type.145
/// @tparam V value type. @param key the key to set (may insert). @param value the mapped value.146
/// @complexity `T::operator[]` (may insert). @alloc whatever the insert/assignment allocates.147
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects148
template <class K, class V>149
requires (is_write_mode<M> && Mapping<T> &&150
std::convertible_to<K, typename T::key_type>)151
void write(K&& key, V&& value)152
{ (*obj_)[std::forward<K>(key)] = std::forward<V>(value); }154
/// Still ours? `true` until the owner asks us to yield (a writer waiting; an immediate-write). The155
/// holder observing `!valid()` is how the owner learns it has paused. @return whether the lease is156
/// still valid. @complexity O(1) plus the interrupt handler on first observing a stop. @alloc none.157
/// @concurrency an atomic acquire load; the first observation of a stop acks and wakes the owner158
/// (that one call briefly takes the owner's mutex) — polling from the holding thread is what lets a159
/// drain/preempt make progress. The handle is not internally synchronized: poll from that thread.160
/// @test Memory.ReadLeaseValidUntilAWriterNeedsIn161
bool valid() const noexcept {162
const bool v = gate_->valid.load(std::memory_order_acquire);163
if (!v) {164
if (!gate_->acked.exchange(true, std::memory_order_acq_rel) && gate_->wake)165
gate_->wake(); // first ack: wake the owner's cv_ so it re-checks (we ack off its mutex)166
if (on_interrupt_ && !fired_) { fired_ = true; on_interrupt_(); }167
} else {168
fired_ = false; // reset so a later preempt (write resume then re-preempt) can fire again169
}170
return v;171
}172
/// Asked to yield? The negation of valid(). @return `true` once the owner needs the lease back.173
/// @complexity O(1). @alloc none.174
/// @test Memory.ReadLeaseValidUntilAWriterNeedsIn175
/// @systest MemoryCheatah.ReadLeaseValidState176
bool expired() const noexcept { return !valid(); }178
/// Register the "what to do if the owner interrupts me" handler; fires once per stop, in the holder's179
/// thread, the first time `valid()` observes it — and again after a resume and a second preempt.180
/// Replaces any previous handler. @complexity O(1).181
/// @alloc one callback holder (the @p handler `std::function`, moved in — nothing beyond its own state).182
/// @param handler the callback to run when the owner asks this lease to yield.183
/// @concurrency never fires asynchronously — only from inside a `valid()` call, on the polling thread.184
/// @test Memory.InterruptCallbackFiresWhenTheOwnerNeedsTheLeaseBack185
void on_interrupt(std::function<void()> handler) { on_interrupt_ = std::move(handler); }187
private:188
void drop() noexcept { if (release_) { auto r = std::move(release_); release_ = nullptr; r(); } }189
void steal(Lease& o) noexcept {190
obj_ = o.obj_; gate_ = std::move(o.gate_); release_ = std::move(o.release_);191
on_interrupt_ = std::move(o.on_interrupt_); fired_ = o.fired_;192
o.obj_ = nullptr; o.release_ = nullptr;193
}195
T* obj_{}; ///< direct pointer to the object (one deref).196
std::shared_ptr<detail::Gate> gate_; ///< shared yield signal (per generation / active write).197
std::function<void()> release_; ///< tells the owner "I'm done" (fired once, in dtor).198
std::function<void()> on_interrupt_; ///< optional push handler when asked to yield.199
mutable bool fired_ = false;///< has on_interrupt_ fired for the current stop?200
};202
} // namespace cheatah::memory