Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
inplace_function.hpp
Go to the documentation of this file.
1#pragma once
2
3// aria::inplace_function<R(Args...), Capacity, Align> — small-object owning
4// type-erased callable.
5//
6// Inspired by `std::inplace_function` (P0228) and the various battle-tested
7// implementations shipped by Boost / EA / fixed_callable. Holds the callable
8// in an internal aligned buffer of `Capacity` bytes; if the callable cannot
9// fit (size or alignment), the program is rejected at compile time. There is
10// **no** heap fallback — by design.
11//
12// Design contract
13// ---------------
14// 1. **Owns its callable.** Move/copy-constructs / destroys the underlying
15// object. Targets must be copy constructible, matching the wrapper's
16// own copyable interface. Target copy/move exceptions propagate.
17// Failed assignment leaves the destination empty (basic guarantee).
18// 2. **Zero heap allocation.** A storage overflow is a static_assert, not a
19// runtime malloc.
20// 3. **Two pointers + buffer.** Layout is `(invoker_ptr, manager_ptr,
21// aligned_buffer)`. `invoker_ptr` calls the wrapped callable; the
22// `manager_ptr` is a single function pointer that handles destroy /
23// move via a tag dispatch (one indirection rather than three).
24// 4. **Typed lifetime operations.** The manager forwards copy, move and
25// destruction to the target type; trivial operations can be optimised
26// by the compiler without changing the lifetime contract.
27// 5. **Empty-state safe.** `operator bool()` reports engagement; calling an
28// empty `inplace_function` throws `aria::bad_inplace_function_call`,
29// which derives from `std::bad_function_call`.
30//
31// Why not just std::function
32// --------------------------
33// `std::function` permits, but does not require, small-buffer optimisation.
34// Many standard libraries kick into heap allocation when the captured state
35// exceeds an undocumented threshold (libc++: 24 bytes on 64-bit). For hot
36// derived-list predicates, comparators and mappers we want a hard guarantee:
37// no allocation, period. `inplace_function` is that guarantee, expressed in
38// the type system.
39//
40// Capacity guidance
41// -----------------
42// * 32 bytes is the project default — fits a `[a, b, c, d](){...}` capture
43// plus four 8-byte references on 64-bit, with room for a small struct.
44// * If a binding site needs more, bump the capacity for that site. The
45// `static_assert` will scream loudly when the constraint is violated.
46
47#include "aria/function_ref.hpp"
48
49#include <cstddef>
50#include <cstring>
51#include <functional>
52#include <memory>
53#include <new>
54#include <stdexcept>
55#include <type_traits>
56#include <utility>
57
58namespace aria {
59
60class bad_inplace_function_call : public std::bad_function_call {
61public:
62 [[nodiscard]] const char* what() const noexcept override {
63 return "aria::inplace_function: invoking an empty wrapper";
64 }
65};
66
67namespace detail::inplace {
68
69// Manager opcodes — a single function-pointer per concrete callable handles
70// move, copy, and destruction. Using a single manager pointer (rather than
71// one for each operation) keeps the `inplace_function` footprint tight.
72//
73// Construction constrains every erased target to be copy constructible,
74// so every engaged wrapper supports the copy opcode.
75enum class Op : unsigned char {
76 Destroy,
77 MoveConstruct,
78 CopyConstruct,
79};
80
81template<class Fn>
82void manager_for(Op op, void* self, void* other) {
83 auto* dst = static_cast<Fn*>(self);
84 switch (op) {
85 case Op::Destroy:
86 dst->~Fn();
87 return;
88 case Op::MoveConstruct: {
89 auto* src = static_cast<Fn*>(other);
90 ::new (dst) Fn(std::move(*src));
91 return;
92 }
93 case Op::CopyConstruct: {
94 const auto* src = static_cast<const Fn*>(other);
95 ::new (dst) Fn(*src);
96 return;
97 }
98 }
99}
100
101} // namespace detail::inplace
102
103template<class Sig,
104 std::size_t Capacity = 32,
105 std::size_t Alignment = alignof(std::max_align_t)>
107
108template<class R, class... Args, std::size_t Capacity, std::size_t Alignment>
109class inplace_function<R(Args...), Capacity, Alignment> {
110public:
111 using result_type = R;
112
113 constexpr inplace_function() noexcept = default;
114
115 inplace_function(std::nullptr_t) noexcept {}
116
117 template<class Fn,
118 class Decayed = std::decay_t<Fn>,
119 class = std::enable_if_t<
120 !std::is_same_v<Decayed, inplace_function> &&
121 std::is_invocable_r_v<R, Decayed&, Args...> &&
122 std::is_copy_constructible_v<Decayed> &&
123 std::is_move_constructible_v<Decayed>>>
125 emplace_<Decayed>(std::forward<Fn>(fn));
126 }
127
129 copy_from_(other);
130 }
131
133 if (this != &other) {
134 reset();
135 copy_from_(other);
136 }
137 return *this;
138 }
139
141 move_from_(other);
142 }
143
145 if (this != &other) {
146 reset();
147 move_from_(other);
148 }
149 return *this;
150 }
151
152 inplace_function& operator=(std::nullptr_t) noexcept {
153 reset();
154 return *this;
155 }
156
157 template<class Fn,
158 class Decayed = std::decay_t<Fn>,
159 class = std::enable_if_t<
160 !std::is_same_v<Decayed, inplace_function> &&
161 std::is_invocable_r_v<R, Decayed&, Args...> &&
162 std::is_copy_constructible_v<Decayed> &&
163 std::is_move_constructible_v<Decayed>>>
165 reset();
166 emplace_<Decayed>(std::forward<Fn>(fn));
167 return *this;
168 }
169
171
172 void reset() noexcept {
173 if (manager_ != nullptr) {
174 auto manager = std::exchange(manager_, nullptr);
175 invoker_ = nullptr;
176 // A capture destructor may reset us again or install a new
177 // callback. Do not destroy twice or overwrite that new state.
178 manager(detail::inplace::Op::Destroy, storage_(), nullptr);
179 }
180 }
181
182 explicit operator bool() const noexcept { return invoker_ != nullptr; }
183
184 R operator()(Args... args) const {
185 if (invoker_ == nullptr) {
187 }
188 return invoker_(storage_(), std::forward<Args>(args)...);
189 }
190
191 // Implicit conversion to a non-owning view — `function_ref` always
192 // remains valid for the lifetime of the `inplace_function`. The view
193 // captures `*this`, not the raw invoker pointer; that lets it route
194 // through `operator()` (which already knows how to dispatch the held
195 // callable through the type-erased `invoker_`), avoiding the
196 // signature mismatch between the internal
197 // `R(*)(const void*, Args...)` invoker and the public
198 // `R(*)(Args...)` view contract.
199 operator function_ref<R(Args...)>() const noexcept {
200 if (invoker_ == nullptr) return {};
201 return function_ref<R(Args...)>{*this};
202 }
203
204 // Convenience: take a non-owning view explicitly. Equivalent to the
205 // implicit conversion above; provided so callers can spell the
206 // intent without resorting to a `static_cast<function_ref<...>>`.
207 [[nodiscard]] function_ref<R(Args...)> ref() const noexcept {
208 return static_cast<function_ref<R(Args...)>>(*this);
209 }
210
211 friend bool operator==(const inplace_function& a, std::nullptr_t) noexcept {
212 return a.invoker_ == nullptr;
213 }
214 friend bool operator==(std::nullptr_t, const inplace_function& a) noexcept {
215 return a.invoker_ == nullptr;
216 }
217 friend bool operator!=(const inplace_function& a, std::nullptr_t) noexcept {
218 return a.invoker_ != nullptr;
219 }
220 friend bool operator!=(std::nullptr_t, const inplace_function& a) noexcept {
221 return a.invoker_ != nullptr;
222 }
223
224private:
225 using Invoker = R (*)(const void*, Args...);
226 using ManagerFn =
227 void (*)(detail::inplace::Op, void* /*self*/, void* /*other*/);
228
229 template<class Stored, class U>
230 void emplace_(U&& fn) {
231 static_assert(sizeof(Stored) <= Capacity,
232 "aria::inplace_function: callable does not fit. "
233 "Either reduce capture size or increase Capacity.");
234 static_assert(alignof(Stored) <= Alignment,
235 "aria::inplace_function: callable alignment exceeds Alignment. "
236 "Increase the Alignment template parameter.");
237
238 // A function reference decays to a stored pointer, but cannot be null.
239 using Argument = std::remove_reference_t<U>;
240 if constexpr (std::is_pointer_v<Argument> || std::is_member_pointer_v<Argument>) {
241 if (fn == nullptr) return;
242 }
243 ::new (storage_()) Stored(std::forward<U>(fn));
244 invoker_ = &invoke_<Stored>;
245 manager_ = &detail::inplace::manager_for<Stored>;
246 }
247
248 template<class Fn>
249 static R invoke_(const void* obj, Args... args) {
250 // const_cast: the underlying callable may have a non-const operator()
251 // (mutable lambdas). The storage is morally non-const; we only mark
252 // it const for the function_ref interop.
253 auto* p = const_cast<Fn*>(static_cast<const Fn*>(obj));
254 if constexpr (std::is_void_v<R>) {
255 std::invoke(*p, std::forward<Args>(args)...);
256 } else {
257 return std::invoke(*p, std::forward<Args>(args)...);
258 }
259 }
260
261 void move_from_(inplace_function& other) {
262 if (other.manager_ != nullptr) {
263 other.manager_(detail::inplace::Op::MoveConstruct,
264 storage_(), other.storage_());
265 invoker_ = other.invoker_;
266 manager_ = other.manager_;
267 other.reset();
268 }
269 }
270
271 void copy_from_(const inplace_function& other) {
272 if (other.manager_ != nullptr) {
273 // Publish engagement only after successful construction. A
274 // throwing target copy leaves
275 // this wrapper empty and the source unchanged.
276 other.manager_(detail::inplace::Op::CopyConstruct,
277 storage_(),
278 const_cast<void*>(other.storage_()));
279 invoker_ = other.invoker_;
280 manager_ = other.manager_;
281 }
282 }
283
284 void* storage_() noexcept {
285 return static_cast<void*>(&buffer_);
286 }
287 const void* storage_() const noexcept {
288 return static_cast<const void*>(&buffer_);
289 }
290
291 alignas(Alignment) std::byte buffer_[Capacity]{};
292 Invoker invoker_ = nullptr;
293 ManagerFn manager_ = nullptr;
294};
295
296} // namespace aria
Definition inplace_function.hpp:60
const char * what() const noexcept override
Definition inplace_function.hpp:62
Definition function_ref.hpp:60
friend bool operator==(const inplace_function &a, std::nullptr_t) noexcept
Definition inplace_function.hpp:211
void reset() noexcept
Definition inplace_function.hpp:172
function_ref< R(Args...)> ref() const noexcept
Definition inplace_function.hpp:207
inplace_function & operator=(inplace_function &&other)
Definition inplace_function.hpp:144
inplace_function(inplace_function &&other)
Definition inplace_function.hpp:140
friend bool operator!=(const inplace_function &a, std::nullptr_t) noexcept
Definition inplace_function.hpp:217
~inplace_function()
Definition inplace_function.hpp:170
friend bool operator==(std::nullptr_t, const inplace_function &a) noexcept
Definition inplace_function.hpp:214
inplace_function & operator=(std::nullptr_t) noexcept
Definition inplace_function.hpp:152
inplace_function(const inplace_function &other)
Definition inplace_function.hpp:128
R operator()(Args... args) const
Definition inplace_function.hpp:184
inplace_function & operator=(const inplace_function &other)
Definition inplace_function.hpp:132
friend bool operator!=(std::nullptr_t, const inplace_function &a) noexcept
Definition inplace_function.hpp:220
inplace_function(Fn &&fn)
Definition inplace_function.hpp:124
inplace_function & operator=(Fn &&fn)
Definition inplace_function.hpp:164
R result_type
Definition inplace_function.hpp:111
Definition inplace_function.hpp:106
Definition signal.hpp:12
Definition validation_key.hpp:110