morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
scope.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <cstddef>
5#include <memory>
6#include <type_traits>
7#include <utility>
8#include <vector>
9
10#include "../attributes.hpp"
11#include "runtime.hpp"
12#include "signal.hpp"
13
14/// @file
15/// @brief `morph::reactive::Scope`: owns reactive nodes and objects, and destroys them in reverse.
16///
17/// Specified in `docs/spec/reactive/signals.md`.
18
19namespace morph::reactive {
20
21/// @brief Owns effects, computeds and adopted objects; destroys them in reverse creation order.
22///
23/// A mounted view, a Switch case and a ForEach row each own one, so tearing one down stops every
24/// binding it made before anything those bindings point at goes away.
25///
26/// Scopes nest: a scope made by `child()` is one level deeper, and the Effects
27/// a scope makes carry its depth. Queued Effects run shallowest first, so an owner's Effects run before those of
28/// the scopes it owns, whatever order the writes came in and whichever Effect was made first: a row binding never
29/// evaluates against a parent that is about to remove the row.
30///
31/// An Effect the scope owns may clear or destroy the scope from its own body (a remount): the graph
32/// keeps the running body alive, and the body must not touch the scope or anything it owned afterwards.
33/// That holds for an Effect's first run too, which happens inside `effect()` before the scope stores it:
34/// - If the first run clears the scope, the new Effect is stored after the clear and stays.
35/// - If it destroys the scope, the new Effect is destroyed at once, since a dead scope owns nothing, and
36/// the reference `effect()` returns dangles (as does every reference to an object the scope owned).
37/// That destructor runs after the scope is gone, so it must not use the scope.
38///
39/// Only the nodes check the owner (`detail::site::kOffOwner`); the scope's own list is unguarded. Using a
40/// scope off its Runtime's owner is a misuse that is not reported.
41///
42/// An owned object's destructor may call back into the scope. It leaves the list before it is destroyed.
43/// A destructor that keeps calling `make()` while the scope is clearing never lets `clear()` finish.
44class Scope {
45public:
46 /// @brief Constructs a scope.
47 /// @param runtime The runtime the nodes made here belong to. Borrowed: it must outlive the scope.
48 /// @param depth How deeply the scope is nested: zero for a root, one more than its owner's for a scope another
49 /// owns (`child()` passes that).
50 explicit Scope(Runtime& runtime MORPH_LIFETIMEBOUND, std::size_t depth = 0) noexcept
51 : _rt{&runtime}, _depth{depth} {}
52
53 /// @brief Destroys everything owned, newest first, and tells every `make()` still constructing that
54 /// the scope is gone.
56 for (Construction* frame = _constructing; frame != nullptr; frame = frame->previous) {
57 frame->scopeDied = true;
58 }
59 clear();
60 }
61 Scope(Scope const&) = delete;
62 Scope& operator=(Scope const&) = delete;
63 Scope(Scope&&) = delete;
64 Scope& operator=(Scope&&) = delete;
65
66 /// @brief Constructs a `T` from @p args and owns it.
67 /// @tparam T The object type.
68 /// @tparam Args Constructor argument types.
69 /// @param args Forwarded to `T`'s constructor.
70 /// @return The new object, alive until the scope is cleared or destroyed.
71 template <typename T, typename... Args>
72 T& make(Args&&... args) {
73 // The constructor may run user code (an Effect's first run) that destroys this scope. The frame
74 // is how the destructor tells this call, and every make enclosing it, so none touches `this`
75 // afterwards.
76 Construction frame{.previous = _constructing};
77 _constructing = &frame;
78 std::unique_ptr<T> made;
79 try {
80 made = std::make_unique<T>(std::forward<Args>(args)...);
81 } catch (...) {
82 leave(frame);
83 throw;
84 }
85 if (frame.scopeDied) {
86 // A dead scope owns nothing: the object goes now, and the reference is already dangling.
87 T& dead = *made;
88 made = nullptr;
89 return dead; // NOLINT(clang-analyzer-cplusplus.NewDelete): documented dangling return
90 }
91 leave(frame);
92 return adopt(std::move(made));
93 }
94
95 /// @brief Takes ownership of @p owned.
96 /// @tparam T The object type.
97 /// @param owned The object; must not be null.
98 /// @return The object, alive until the scope is cleared or destroyed.
99 template <typename T>
100 T& adopt(std::unique_ptr<T> owned) {
101 T& ref = *owned;
102 // The slot is made empty first, and only then given the pointer: the emplace may throw while
103 // @p owned still holds the object, and `reset` cannot throw.
104 _owned.emplace_back(nullptr, [](void* ptr) noexcept { std::default_delete<T>{}(static_cast<T*>(ptr)); });
105 _owned.back().reset(owned.release());
106 return ref;
107 }
108
109 /// @brief Makes an Effect owned by this scope, at this scope's depth.
110 /// @tparam F A callable taking no arguments.
111 /// @param fn The effect body.
112 /// @return The Effect.
113 template <typename F>
114 Effect& effect(F&& fn) {
115 return make<Effect>(*_rt, std::forward<F>(fn), _depth);
116 }
117
118 /// @brief Makes a scope one level deeper, owned by this one.
119 /// @return The child scope, alive until this scope is cleared or destroyed.
120 Scope& child() { return make<Scope>(*_rt, _depth + 1); }
121
122 /// @brief Makes a Computed owned by this scope; its type is what @p fn returns.
123 /// @tparam F A callable taking no arguments.
124 /// @param fn The computation.
125 /// @return The Computed.
126 template <typename F>
127 auto& computed(F&& fn) {
128 using Value = std::remove_cvref_t<std::invoke_result_t<F&>>;
129 return make<Computed<Value>>(*_rt, std::forward<F>(fn));
130 }
131
132 /// @brief Destroys everything owned, newest first.
133 ///
134 /// Each object leaves the list before it is destroyed, so a destructor or a running Effect body
135 /// that calls back into the scope (`clear()`, `make()`) sees a consistent list.
136 void clear() noexcept {
137 while (!_owned.empty()) {
138 auto const last = std::move(_owned.back());
139 _owned.pop_back();
140 }
141 }
142
143 /// @brief How many objects the scope owns.
144 /// @return The count.
145 [[nodiscard]] std::size_t size() const noexcept { return _owned.size(); }
146
147 /// @brief The runtime.
148 /// @return The runtime passed at construction.
149 [[nodiscard]] Runtime& runtime() const noexcept { return *_rt; }
150
151 /// @brief How deeply this scope is nested.
152 /// @return Zero for a root scope; one more than the parent's for a child.
153 [[nodiscard]] std::size_t depth() const noexcept { return _depth; }
154
155private:
156 /// @brief One `make()` in progress; frames nest in call order.
157 struct Construction {
158 Construction* previous = nullptr; ///< The enclosing `make()`, or null.
159 bool scopeDied = false; ///< Set by the destructor while this `make()` is still constructing.
160 };
161
162 /// @brief Ends @p frame, unless the scope died meanwhile and `this` is no longer valid.
163 /// @param frame The innermost frame.
164 void leave(Construction const& frame) noexcept {
165 if (!frame.scopeDied) {
166 _constructing = frame.previous;
167 }
168 }
169
170 Runtime* _rt;
171 std::size_t _depth = 0;
172 Construction* _constructing = nullptr;
173 std::vector<std::unique_ptr<void, void (*)(void*)>> _owned;
174};
175
176} // namespace morph::reactive
A side effect: runs once on construction, then in a flush after any source it read changed.
Definition signal.hpp:350
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition runtime.hpp:70
Owns effects, computeds and adopted objects; destroys them in reverse creation order.
Definition scope.hpp:44
~Scope()
Destroys everything owned, newest first, and tells every make() still constructing that the scope is ...
Definition scope.hpp:55
Scope(Runtime &runtime, std::size_t depth=0) noexcept
Constructs a scope.
Definition scope.hpp:50
T & adopt(std::unique_ptr< T > owned)
Takes ownership of owned.
Definition scope.hpp:100
void clear() noexcept
Destroys everything owned, newest first.
Definition scope.hpp:136
std::size_t size() const noexcept
How many objects the scope owns.
Definition scope.hpp:145
auto & computed(F &&fn)
Makes a Computed owned by this scope; its type is what fn returns.
Definition scope.hpp:127
Scope & child()
Makes a scope one level deeper, owned by this one.
Definition scope.hpp:120
Runtime & runtime() const noexcept
The runtime.
Definition scope.hpp:149
Effect & effect(F &&fn)
Makes an Effect owned by this scope, at this scope's depth.
Definition scope.hpp:114
T & make(Args &&... args)
Constructs a T from args and owns it.
Definition scope.hpp:72
std::size_t depth() const noexcept
How deeply this scope is nested.
Definition scope.hpp:153
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.