Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — 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
59template<class Sig>
60class function_ref; // primary template intentionally undefined
61
62template<class R, class... Args>
63class function_ref<R(Args...)> {
64public:
65 using result_type = R;
66
67 // Default-constructed function_ref is disengaged. Calling it is UB.
68 // Provided for the "construct now, assign later" idiom.
69 constexpr function_ref() noexcept = default;
70
71 constexpr function_ref(std::nullptr_t) noexcept {}
72
73 // The exact-signature overload also resolves overloaded function names.
74 function_ref(R (*fp)(Args...)) noexcept
75 : target_{.function = reinterpret_cast<ErasedFunction>(fp)},
76 invoke_(fp ? &invoke_function_pointer_<decltype(fp)> : nullptr) {}
77
78 // Store function pointers by value, including implicit function decay
79 // and noexcept functions. The generic object path must never retain a
80 // pointer to the temporary pointer object at a construction/assignment.
81 template<class Fp,
82 std::enable_if_t<std::is_pointer_v<Fp> &&
83 std::is_function_v<std::remove_pointer_t<Fp>> &&
84 std::is_invocable_r_v<R, Fp, Args...>, int> = 0>
85 function_ref(Fp fp) noexcept
86 : target_{.function = reinterpret_cast<ErasedFunction>(fp)},
87 invoke_(fp ? &invoke_function_pointer_<Fp> : nullptr) {}
88
89 // Construct from any non-`function_ref` callable invocable as
90 // `R(Args...)`. The callable is referenced — not copied — so the caller
91 // must keep it alive.
92 template<class Fn,
93 class = std::enable_if_t<
94 !std::is_same_v<std::remove_cvref_t<Fn>, function_ref> &&
95 !std::is_pointer_v<std::remove_cvref_t<Fn>> &&
96 !std::is_function_v<std::remove_reference_t<Fn>> &&
97 !std::is_member_pointer_v<std::remove_cvref_t<Fn>> &&
98 std::is_invocable_r_v<R, Fn&, Args...>>>
99 function_ref(Fn&& fn) noexcept
100 : target_{.object = static_cast<const void*>(std::addressof(fn))},
101 invoke_(&invoke_callable_<std::remove_reference_t<Fn>>) {}
102
103 // Trivially copyable — implicit copy/move is correct.
104 constexpr function_ref(const function_ref&) noexcept = default;
105 constexpr function_ref& operator=(const function_ref&) noexcept = default;
106
107 constexpr function_ref& operator=(std::nullptr_t) noexcept {
108 target_.object = nullptr;
109 invoke_ = nullptr;
110 return *this;
111 }
112
113 function_ref& operator=(R (*fp)(Args...)) noexcept {
114 target_.function = reinterpret_cast<ErasedFunction>(fp);
115 invoke_ = fp ? &invoke_function_pointer_<decltype(fp)> : nullptr;
116 return *this;
117 }
118
119 template<class Fp,
120 std::enable_if_t<std::is_pointer_v<Fp> &&
121 std::is_function_v<std::remove_pointer_t<Fp>> &&
122 std::is_invocable_r_v<R, Fp, Args...>, int> = 0>
123 function_ref& operator=(Fp fp) noexcept {
124 target_.function = reinterpret_cast<ErasedFunction>(fp);
125 invoke_ = fp ? &invoke_function_pointer_<Fp> : nullptr;
126 return *this;
127 }
128
129 template<class Fn,
130 class = std::enable_if_t<
131 !std::is_same_v<std::remove_cvref_t<Fn>, function_ref> &&
132 !std::is_pointer_v<std::remove_cvref_t<Fn>> &&
133 !std::is_function_v<std::remove_reference_t<Fn>> &&
134 !std::is_member_pointer_v<std::remove_cvref_t<Fn>> &&
135 std::is_invocable_r_v<R, Fn&, Args...>>>
136 function_ref& operator=(Fn&& fn) noexcept {
137 target_.object = static_cast<const void*>(std::addressof(fn));
138 invoke_ = &invoke_callable_<std::remove_reference_t<Fn>>;
139 return *this;
140 }
141
142 // True iff the function_ref points to something invocable.
143 explicit constexpr operator bool() const noexcept { return invoke_ != nullptr; }
144
145 R operator()(Args... args) const {
146 // UB to call when disengaged. Asserting here would impose a cost on every
147 // call; callers are expected to guard with `if (fr)` when relevant.
148 return invoke_(target_, std::forward<Args>(args)...);
149 }
150
151 friend constexpr bool operator==(const function_ref& a, std::nullptr_t) noexcept {
152 return a.invoke_ == nullptr;
153 }
154 friend constexpr bool operator==(std::nullptr_t, const function_ref& a) noexcept {
155 return a.invoke_ == nullptr;
156 }
157 friend constexpr bool operator!=(const function_ref& a, std::nullptr_t) noexcept {
158 return a.invoke_ != nullptr;
159 }
160 friend constexpr bool operator!=(std::nullptr_t, const function_ref& a) noexcept {
161 return a.invoke_ != nullptr;
162 }
163
164private:
165 using ErasedFunction = void (*)();
166 union Target {
167 const void* object = nullptr;
168 ErasedFunction function;
169 };
170 using Invoker = R (*)(Target, Args...);
171
172 template<class Fn>
173 static R invoke_callable_(Target target, Args... args) {
174 // Preserve const targets while allowing mutable non-const callables.
175 auto* p = const_cast<Fn*>(static_cast<const Fn*>(target.object));
176 if constexpr (std::is_void_v<R>) {
177 std::invoke(*p, std::forward<Args>(args)...);
178 } else {
179 return std::invoke(*p, std::forward<Args>(args)...);
180 }
181 }
182
183 template<class Fp>
184 static R invoke_function_pointer_(Target target, Args... args) {
185 // Function-pointer conversions are standard reversible conversions;
186 // no assumption that an object pointer can represent a function.
187 auto fp = reinterpret_cast<Fp>(target.function);
188 if constexpr (std::is_void_v<R>) {
189 std::invoke(fp, std::forward<Args>(args)...);
190 } else {
191 return std::invoke(fp, std::forward<Args>(args)...);
192 }
193 }
194
195 Target target_{};
196 Invoker invoke_ = nullptr;
197};
198
199// Deduction guide: enables `aria::function_ref f = some_lambda;` for the
200// common case of a lambda whose call signature is a unique `R(Args...)`.
201template<class R, class... Args>
202function_ref(R (*)(Args...)) -> function_ref<R(Args...)>;
203
204} // namespace aria
function_ref & operator=(R(*fp)(Args...)) noexcept
Definition function_ref.hpp:113
function_ref(R(*fp)(Args...)) noexcept
Definition function_ref.hpp:74
constexpr function_ref(const function_ref &) noexcept=default
friend constexpr bool operator==(const function_ref &a, std::nullptr_t) noexcept
Definition function_ref.hpp:151
R result_type
Definition function_ref.hpp:65
friend constexpr bool operator!=(std::nullptr_t, const function_ref &a) noexcept
Definition function_ref.hpp:160
function_ref(Fp fp) noexcept
Definition function_ref.hpp:85
constexpr function_ref() noexcept=default
friend constexpr bool operator!=(const function_ref &a, std::nullptr_t) noexcept
Definition function_ref.hpp:157
constexpr function_ref & operator=(std::nullptr_t) noexcept
Definition function_ref.hpp:107
constexpr function_ref & operator=(const function_ref &) noexcept=default
function_ref(Fn &&fn) noexcept
Definition function_ref.hpp:99
friend constexpr bool operator==(std::nullptr_t, const function_ref &a) noexcept
Definition function_ref.hpp:154
function_ref & operator=(Fp fp) noexcept
Definition function_ref.hpp:123
function_ref & operator=(Fn &&fn) noexcept
Definition function_ref.hpp:136
R operator()(Args... args) const
Definition function_ref.hpp:145
Definition function_ref.hpp:60
Definition signal.hpp:12
function_ref(R(*)(Args...)) -> function_ref< R(Args...)>
Definition validation_key.hpp:110