Aria 3.2.6
C++23 MVVM framework — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
function_ref.hpp
Go to the documentation of this file.
1#pragma once
2
3// aria::function_ref<R(Args...)> — non-owning, zero-allocation, signature-erased
4// callable handle.
5//
6// Inspired by `std::function_ref` (C++26 / P0792). A `function_ref` is a thin
7// view over a callable: it stores **two pointers** (target + invoker) and
8// nothing else. It does **not** own its target; the caller is responsible for
9// keeping the underlying object alive for as long as the `function_ref` is in
10// use.
11//
12// When to reach for it
13// --------------------
14// * Hot-path callbacks that are invoked synchronously and never escape the
15// call site (e.g. predicates passed to STL-like algorithms, visitors,
16// "do-this-once and return" callbacks).
17// * Public APIs that want to accept any callable without forcing a
18// `std::function` allocation on every call. Compare:
19//
20// // before — every caller pays for a std::function copy + possible heap.
21// void for_each(const std::function<void(int)>& fn);
22//
23// // after — function_ref is two pointers; no allocation, no virtuals.
24// void for_each(aria::function_ref<void(int)> fn);
25//
26// When NOT to use it
27// ------------------
28// * If the callback is going to be **stored** past the call (e.g. registered
29// as an observer, captured by a coroutine, queued onto a dispatcher), use
30// an owning type instead — `aria::inplace_function` for small lambdas,
31// `std::function` if the size cap is unacceptable.
32// * `function_ref` does NOT participate in copy/move of the underlying
33// callable. Mutating captures inside the wrapped lambda will mutate the
34// original captured-by-value object — correct behaviour, but easy to
35// misread when comparing with `std::function`.
36//
37// Design notes
38// ------------
39// * The invoker is a free function pointer (not a virtual call). Modern
40// compilers reliably inline through it when both the construction site and
41// the call site are visible.
42// * Construction from a function pointer (e.g. `int(*)(int)`) is supported
43// directly, including via implicit decay from a function reference.
44// * Member pointers are rejected; a null free-function pointer produces a
45// disengaged view. Function pointers are stored by value.
46// * `function_ref` is trivially copyable, so copying it is free.
47// * Disengaged ("default constructed") `function_ref` invokes UB if called;
48// the type contract treats default construction as a placeholder for later
49// assignment, mirroring `string_view`.
50
51#include <cstddef>
52#include <functional>
53#include <memory>
54#include <type_traits>
55#include <utility>
56
57namespace aria {
58
59namespace detail {
60#ifdef __has_builtin
61# if __has_builtin(__reference_converts_from_temporary)
62# define ARIA_DETAIL_HAS_REFERENCE_TEMPORARY_BUILTIN
63# endif
64#endif
65
66// is_invocable_r alone accepts a reference bound to a conversion temporary.
67// Reject those targets before instantiating the erased invocation thunk.
68template<class R, class Fn, class... Args>
69inline constexpr bool safely_invocable_r_v = [] {
70 if constexpr (!std::is_invocable_r_v<R, Fn, Args...>) {
71 return false;
72 } else if constexpr (std::is_reference_v<R>) {
73 using Result = std::invoke_result_t<Fn, Args...>;
74#if defined(__cpp_lib_reference_from_temporary) && __cpp_lib_reference_from_temporary >= 202202L
75 return !std::reference_converts_from_temporary_v<R, Result>;
76#elifdef ARIA_DETAIL_HAS_REFERENCE_TEMPORARY_BUILTIN
77 // Android NDK libc++ can lack the standard trait even when its
78 // Clang frontend implements the underlying C++23 lifetime check.
79 return !__reference_converts_from_temporary(R, Result);
80#else
81 // A conservative fallback for incomplete C++23 libraries: accept
82 // direct reference qualification/base conversions only. Conversions
83 // through a value or proxy must not silently return a dangling ref.
84 return std::is_reference_v<Result> &&
85 std::is_convertible_v<std::add_pointer_t<std::remove_reference_t<Result>>,
86 std::add_pointer_t<std::remove_reference_t<R>>>;
87#endif
88 } else {
89 return true;
90 }
91}();
92#undef ARIA_DETAIL_HAS_REFERENCE_TEMPORARY_BUILTIN
93} // namespace detail
94
95template<class Sig>
96class function_ref; // primary template intentionally undefined
97
98template<class R, class... Args>
99class function_ref<R(Args...)> {
100public:
101 using result_type = R;
102
103 // Default-constructed function_ref is disengaged. Calling it is UB.
104 // Provided for the "construct now, assign later" idiom.
105 constexpr function_ref() noexcept = default;
106
107 constexpr function_ref(std::nullptr_t) noexcept {}
108
109 // The exact-signature overload also resolves overloaded function names.
110 function_ref(R (*fp)(Args...)) noexcept
111 : target_{.function = reinterpret_cast<ErasedFunction>(fp)},
112 invoke_(fp ? &invoke_function_pointer_<decltype(fp)> : nullptr) {}
113
114 // Store function pointers by value, including implicit function decay
115 // and noexcept functions. The generic object path must never retain a
116 // pointer to the temporary pointer object at a construction/assignment.
117 template<class Fp,
118 std::enable_if_t<std::is_pointer_v<Fp> &&
119 std::is_function_v<std::remove_pointer_t<Fp>> &&
120 detail::safely_invocable_r_v<R, Fp, Args...>, int> = 0>
121 function_ref(Fp fp) noexcept
122 : target_{.function = reinterpret_cast<ErasedFunction>(fp)},
123 invoke_(fp ? &invoke_function_pointer_<Fp> : nullptr) {}
124
125 // Construct from any non-`function_ref` callable invocable as
126 // `R(Args...)`. The callable is referenced — not copied — so the caller
127 // must keep it alive.
128 template<class Fn,
129 class = std::enable_if_t<
130 !std::is_same_v<std::remove_cvref_t<Fn>, function_ref> &&
131 !std::is_pointer_v<std::remove_cvref_t<Fn>> &&
132 !std::is_function_v<std::remove_reference_t<Fn>> &&
133 !std::is_member_pointer_v<std::remove_cvref_t<Fn>> &&
134 detail::safely_invocable_r_v<R, Fn&, Args...>>>
135 function_ref(Fn&& fn) noexcept
136 : target_{.object = static_cast<const void*>(std::addressof(fn))},
137 invoke_(&invoke_callable_<std::remove_reference_t<Fn>>) {}
138
139 // Trivially copyable — implicit copy/move is correct.
140 constexpr function_ref(const function_ref&) noexcept = default;
141 constexpr function_ref& operator=(const function_ref&) noexcept = default;
142
143 constexpr function_ref& operator=(std::nullptr_t) noexcept {
144 target_.object = nullptr;
145 invoke_ = nullptr;
146 return *this;
147 }
148
149 function_ref& operator=(R (*fp)(Args...)) noexcept {
150 target_.function = reinterpret_cast<ErasedFunction>(fp);
151 invoke_ = fp ? &invoke_function_pointer_<decltype(fp)> : nullptr;
152 return *this;
153 }
154
155 template<class Fp,
156 std::enable_if_t<std::is_pointer_v<Fp> &&
157 std::is_function_v<std::remove_pointer_t<Fp>> &&
158 detail::safely_invocable_r_v<R, Fp, Args...>, int> = 0>
159 function_ref& operator=(Fp fp) noexcept {
160 target_.function = reinterpret_cast<ErasedFunction>(fp);
161 invoke_ = fp ? &invoke_function_pointer_<Fp> : nullptr;
162 return *this;
163 }
164
165 template<class Fn,
166 class = std::enable_if_t<
167 !std::is_same_v<std::remove_cvref_t<Fn>, function_ref> &&
168 !std::is_pointer_v<std::remove_cvref_t<Fn>> &&
169 !std::is_function_v<std::remove_reference_t<Fn>> &&
170 !std::is_member_pointer_v<std::remove_cvref_t<Fn>> &&
171 detail::safely_invocable_r_v<R, Fn&, Args...>>>
172 function_ref& operator=(Fn&& fn) noexcept {
173 target_.object = static_cast<const void*>(std::addressof(fn));
174 invoke_ = &invoke_callable_<std::remove_reference_t<Fn>>;
175 return *this;
176 }
177
178 // True iff the function_ref points to something invocable.
179 explicit constexpr operator bool() const noexcept { return invoke_ != nullptr; }
180
181 R operator()(Args... args) const {
182 // UB to call when disengaged. Asserting here would impose a cost on every
183 // call; callers are expected to guard with `if (fr)` when relevant.
184 return invoke_(target_, std::forward<Args>(args)...);
185 }
186
187 friend constexpr bool operator==(const function_ref& a, std::nullptr_t) noexcept {
188 return a.invoke_ == nullptr;
189 }
190 friend constexpr bool operator==(std::nullptr_t, const function_ref& a) noexcept {
191 return a.invoke_ == nullptr;
192 }
193 friend constexpr bool operator!=(const function_ref& a, std::nullptr_t) noexcept {
194 return a.invoke_ != nullptr;
195 }
196 friend constexpr bool operator!=(std::nullptr_t, const function_ref& a) noexcept {
197 return a.invoke_ != nullptr;
198 }
199
200private:
201 using ErasedFunction = void (*)();
202 union Target {
203 const void* object = nullptr;
204 ErasedFunction function;
205 };
206 using Invoker = R (*)(Target, Args...);
207
208 template<class Fn>
209 static R invoke_callable_(Target target, Args... args) {
210 // Preserve const targets while allowing mutable non-const callables.
211 auto* p = const_cast<Fn*>(static_cast<const Fn*>(target.object));
212 if constexpr (std::is_void_v<R>) {
213 std::invoke(*p, std::forward<Args>(args)...);
214 } else {
215 return std::invoke(*p, std::forward<Args>(args)...);
216 }
217 }
218
219 template<class Fp>
220 static R invoke_function_pointer_(Target target, Args... args) {
221 // Function-pointer conversions are standard reversible conversions;
222 // no assumption that an object pointer can represent a function.
223 auto fp = reinterpret_cast<Fp>(target.function);
224 if constexpr (std::is_void_v<R>) {
225 std::invoke(fp, std::forward<Args>(args)...);
226 } else {
227 return std::invoke(fp, std::forward<Args>(args)...);
228 }
229 }
230
231 Target target_{};
232 Invoker invoke_ = nullptr;
233};
234
235// Deduction guide: enables `aria::function_ref f = some_lambda;` for the
236// common case of a lambda whose call signature is a unique `R(Args...)`.
237template<class R, class... Args>
238function_ref(R (*)(Args...)) -> function_ref<R(Args...)>;
239
240} // namespace aria
function_ref & operator=(R(*fp)(Args...)) noexcept
Definition function_ref.hpp:149
function_ref(R(*fp)(Args...)) noexcept
Definition function_ref.hpp:110
constexpr function_ref(const function_ref &) noexcept=default
friend constexpr bool operator==(const function_ref &a, std::nullptr_t) noexcept
Definition function_ref.hpp:187
R result_type
Definition function_ref.hpp:101
friend constexpr bool operator!=(std::nullptr_t, const function_ref &a) noexcept
Definition function_ref.hpp:196
function_ref(Fp fp) noexcept
Definition function_ref.hpp:121
constexpr function_ref() noexcept=default
friend constexpr bool operator!=(const function_ref &a, std::nullptr_t) noexcept
Definition function_ref.hpp:193
constexpr function_ref & operator=(std::nullptr_t) noexcept
Definition function_ref.hpp:143
constexpr function_ref & operator=(const function_ref &) noexcept=default
function_ref(Fn &&fn) noexcept
Definition function_ref.hpp:135
friend constexpr bool operator==(std::nullptr_t, const function_ref &a) noexcept
Definition function_ref.hpp:190
function_ref & operator=(Fp fp) noexcept
Definition function_ref.hpp:159
function_ref & operator=(Fn &&fn) noexcept
Definition function_ref.hpp:172
R operator()(Args... args) const
Definition function_ref.hpp:181
Definition function_ref.hpp:96
Definition signal.hpp:12
function_ref(R(*)(Args...)) -> function_ref< R(Args...)>
Definition validation_key.hpp:110