morph
0.1.0
Asynchronous bridge between GUI thread and business models
Toggle main menu visibility
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
19
namespace
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.
44
class
Scope
{
45
public
:
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.
55
~Scope
() {
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
155
private
:
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
morph::reactive::Effect
A side effect: runs once on construction, then in a flush after any source it read changed.
Definition
signal.hpp:350
morph::reactive::Runtime
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition
runtime.hpp:70
morph::reactive::Scope
Owns effects, computeds and adopted objects; destroys them in reverse creation order.
Definition
scope.hpp:44
morph::reactive::Scope::~Scope
~Scope()
Destroys everything owned, newest first, and tells every make() still constructing that the scope is ...
Definition
scope.hpp:55
morph::reactive::Scope::Scope
Scope(Runtime &runtime, std::size_t depth=0) noexcept
Constructs a scope.
Definition
scope.hpp:50
morph::reactive::Scope::adopt
T & adopt(std::unique_ptr< T > owned)
Takes ownership of owned.
Definition
scope.hpp:100
morph::reactive::Scope::clear
void clear() noexcept
Destroys everything owned, newest first.
Definition
scope.hpp:136
morph::reactive::Scope::size
std::size_t size() const noexcept
How many objects the scope owns.
Definition
scope.hpp:145
morph::reactive::Scope::computed
auto & computed(F &&fn)
Makes a Computed owned by this scope; its type is what fn returns.
Definition
scope.hpp:127
morph::reactive::Scope::child
Scope & child()
Makes a scope one level deeper, owned by this one.
Definition
scope.hpp:120
morph::reactive::Scope::runtime
Runtime & runtime() const noexcept
The runtime.
Definition
scope.hpp:149
morph::reactive::Scope::effect
Effect & effect(F &&fn)
Makes an Effect owned by this scope, at this scope's depth.
Definition
scope.hpp:114
morph::reactive::Scope::make
T & make(Args &&... args)
Constructs a T from args and owns it.
Definition
scope.hpp:72
morph::reactive::Scope::depth
std::size_t depth() const noexcept
How deeply this scope is nested.
Definition
scope.hpp:153
runtime.hpp
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
signal.hpp
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.
morph
reactive
scope.hpp
Generated by
1.17.0