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 when that first happens.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 to34
/// yield; the holder sets `acked` when it observes that (so the owner knows it has paused) and calls35
/// `wake` so the owner's condition variable re-checks. `wake` (owner-provided) synchronizes on the36
/// owner's mutex before notifying — the ack is one-shot, so an unsynchronized notify racing a waiter37
/// mid-predicate would be lost and the waiter would sleep forever.38
struct Gate {39
std::atomic<bool> valid{true};40
std::atomic<bool> acked{false};41
std::function<void()> wake; ///< set by the owner to notify its cv_ when the holder first acks.42
};43
} // namespace detail45
/**46
* @brief The only handle to an owned object — a move-only RAII lease granted by an `Owner`.47
*48
* `M` is `read` (shared) or `write`/`write_renewable` (exclusive). Reads go through `read(...)`; a49
* write lease sets through `write(...)`. The lease holds a direct pointer to the object plus the50
* release callback it fires on destruction. @tparam T the owned type (`Ownable`). @tparam M the mode.51
*/52
template <Ownable T, Mode M>53
class Lease {54
public:55
/**56
* The owner's grant path (called only by `Owner`). @complexity O(1). @alloc none.57
* @param obj pointer to the owned object.58
* @param gate the generation gate that carries the yield signal.59
* @param release callback fired once on destruction to tell the owner this lease is done.60
* @concurrency called by the owner with its coordinator mutex held — never construct one yourself.61
* @test Memory.EveryAccessorReturnsARequestNotABareLease62
*/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
Lease(Lease&& o) noexcept { steal(o); }69
/// Move-assign: release ours, then take over @p o's grant. @param o source. @return `*this`.70
/// @complexity O(1). @alloc none. @test Memory.EveryAccessorReturnsARequestNotABareLease71
Lease& operator=(Lease&& o) noexcept { if (this != &o) { drop(); steal(o); } return *this; }72
Lease(const Lease&) = delete;73
Lease& operator=(const Lease&) = delete;74
/// Release the lease (fires the owner's release callback if still held).75
/// @complexity O(1). @alloc none.76
/// @concurrency the release callback takes the owner's coordinator mutex and wakes waiting77
/// requests (a draining writer proceeds once the last reader releases here).78
/// @test Memory.WriteWaitsForReadersToDrain79
~Lease() { drop(); }81
/// Read the whole object. Available on every lease. @return a `const T&` — never a copy or `T*`.82
/// @complexity O(1). @alloc none.83
/// @concurrency race-free while the lease is held: a writer cannot proceed until this lease84
/// releases (yielding on `!valid()` is cooperative, not forced). No lock is taken here.85
/// @test Memory.ReadReturnsAReferenceNotACopyOrPointer86
const T& read() const { return *obj_; }88
/// Read element @p index of an Indexed sequence: `r.read(i)`. Mirrors `w.write(i, v)`.89
/// @param index the position to read. @return a const reference to the element.90
/// @complexity `T::operator[]`. @alloc none.91
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects92
const auto& read(std::size_t index) const93
requires Indexed<T>94
{ return (*obj_)[index]; }96
/// Read the value at @p key of a Mapping: `r.read(k)`. Mirrors `w.write(k, v)`. Throws if absent97
/// (reading a missing key never inserts). @tparam K a type convertible to the key type.98
/// @param key the key to look up. @return a const reference to the mapped value.99
/// @complexity `T::at`. @alloc none.100
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects101
template <class K>102
requires (Mapping<T> && std::convertible_to<K, typename T::key_type>)103
const auto& read(K&& key) const104
{ return (*obj_).at(std::forward<K>(key)); }106
/// Read the first element (containers with `front()`: vector / deque / list / string …).107
/// @return a const reference to the first element. @complexity O(1). @alloc none.108
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects109
const auto& read_front() const110
requires HasFront<T>111
{ return (*obj_).front(); }113
/// Read the last element (containers with `back()`).114
/// @return a const reference to the last element. @complexity O(1). @alloc none.115
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects116
const auto& read_back() const117
requires HasBack<T>118
{ return (*obj_).back(); }120
/// Replace the whole object: `w.write(value)`. The primary write form. Write / write_renewable only.121
/// @param value the new value (moved in). @complexity O(1) plus assigning @p value.122
/// @alloc whatever `T`'s assignment allocates.123
/// @concurrency exclusive: no reader or other writer coexists while this lease is valid. A124
/// writer that observed `!valid()` (an immediate-write preempted it) must wait for `valid()` to125
/// flip back before writing again — writing while suspended races with the immediate-write.126
/// @test Memory.WriteWaitsForReadersToDrain127
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects128
/// @systest MemoryCheatah.ScalarWriteReadModifyWrite129
void write(T value)130
requires is_write_mode<M>131
{ *obj_ = std::move(value); }133
/// Set element @p index of an Indexed sequence: `w.write(i, v)`. Deduced. Write modes only.134
/// @tparam V the element value type. @param index the position to set. @param value the new element.135
/// @complexity `T::operator[]`. @alloc whatever the element assignment allocates.136
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects137
/// @systest MemoryCheatah.OwnerOfNdArrayElements138
template <class V>139
requires (is_write_mode<M> && Indexed<T>)140
void write(std::size_t index, V&& value)141
{ (*obj_)[index] = std::forward<V>(value); }143
/// Set @p key of a Mapping: `w.write(k, v)`. Deduced. Write modes only. @tparam K key type.144
/// @tparam V value type. @param key the key to set (may insert). @param value the mapped value.145
/// @complexity `T::operator[]` (may insert). @alloc whatever the insert/assignment allocates.146
/// @test Memory.LeasesModifyTheCorrectItemsOfComplexObjects147
template <class K, class V>148
requires (is_write_mode<M> && Mapping<T> &&149
std::convertible_to<K, typename T::key_type>)150
void write(K&& key, V&& value)151
{ (*obj_)[std::forward<K>(key)] = std::forward<V>(value); }153
/// Still ours? `true` until the owner asks us to yield (a writer waiting; an immediate-write). The154
/// holder observing `!valid()` is how the owner learns it has paused. @return whether the lease is155
/// still valid. @complexity O(1). @alloc none.156
/// @concurrency an atomic acquire load; the first observation of a stop acks and wakes the owner157
/// (that one call briefly takes the owner's mutex) — polling this from the holding thread is what158
/// lets a drain/preempt make progress. The lease handle itself is not internally synchronized:159
/// poll from the thread that holds the lease.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, in the holder's thread,179
/// the first time `valid()` observes the stop. Replaces any previous handler. @complexity O(1).180
/// @alloc one callback holder (the @p handler `std::function`, moved in — nothing beyond its own state).181
/// @param handler the callback to run when the owner asks this lease to yield.182
/// @concurrency the handler never fires asynchronously — only from inside a `valid()` call, on183
/// the thread that polls it.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