RPNX::DataStructures
Header-only C++ data structures and supporting utilities.
Loading...
Searching...
No Matches
functional.hpp
1// Copyright (c) 2026 Ryan P. Nicholl <rnicholl@protonmail.com>
2
3#ifndef RPNXDATASTRUCTURES_FUNCTIONAL_HPP
4#define RPNXDATASTRUCTURES_FUNCTIONAL_HPP
5
6#include "rpnx/memory.hpp"
7#include <array>
8#include <cstddef>
9#include <cstring>
10#include <functional>
11#include <memory>
12#include <type_traits>
13#include <utility>
14
15namespace rpnx
16{
21 template < typename F >
22 class function;
23
24 template < typename F >
26
37 template < typename R, typename... Args >
38 class function< R(Args...) >
39 {
40 using callable_f = R (*)(function< R(Args...) >*, Args...);
41 using copy_ctor_f = void (*)(function< R(Args...) >*, function< R(Args...) > const*);
42 using copy_assign_f = void (*)(function< R(Args...) >*, function< R(Args...) > const*);
43 using move_ctor_f = void (*)(function< R(Args...) >*, function< R(Args...) >*) noexcept;
44 using move_assign_f = void (*)(function< R(Args...) >*, function< R(Args...) >*) noexcept;
45 using reset_f = void (*)(function< R(Args...) >*) noexcept;
46 using destroy_f = void (*)(function< R(Args...) >*) noexcept;
47
48 struct impl_tbl
49 {
50 copy_ctor_f const m_copy_ctor;
51 copy_assign_f const m_copy_assign;
52 move_ctor_f const m_move_ctor;
53 move_assign_f const m_move_assign;
54 reset_f const m_reset;
55 destroy_f const m_destroy;
56 callable_f const m_call;
57 };
58
59 static std::size_t constexpr sbo_size = 16;
60 static std::size_t constexpr sbo_align = 16;
61
62 callable_f m_callable;
63 impl_tbl const* m_impl_tbl;
64 alignas(sbo_align) std::array< std::byte, sbo_size > m_storage;
65
66 template < typename Functor >
67 static constexpr bool use_sbo()
68 {
69 return sizeof(Functor) <= sbo_size && alignof(Functor) <= sbo_align && std::is_nothrow_copy_constructible_v< Functor > && std::is_nothrow_copy_assignable_v< Functor >;
70 }
71
79 template < typename Functor >
80 static Functor* get_storage_address(function< R(Args...) >* f)
81 {
82 if constexpr (use_sbo< Functor >())
83 {
84 return std::launder< Functor >(reinterpret_cast< Functor* >(f->m_storage.data()));
85 }
86 else
87 {
88 static_assert(alignof(Functor*) <= sbo_align, "Functor pointer must fit in storage for non-SBO case");
89 static_assert(sizeof(Functor*) <= sbo_size, "Functor pointer must fit in storage for non-SBO case");
90 return std::launder< Functor >(*reinterpret_cast< Functor** >(f->m_storage.data()));
91 }
92 }
93
94 template < typename Functor >
95 static Functor const* get_storage_address(function< R(Args...) > const* f)
96 {
97 if constexpr (use_sbo< Functor >())
98 {
99 return std::launder< Functor const >(reinterpret_cast< Functor const* >(f->m_storage.data()));
100 }
101 else
102 {
103 static_assert(alignof(Functor*) <= sbo_align, "Functor pointer must fit in storage for non-SBO case");
104 static_assert(sizeof(Functor*) <= sbo_size, "Functor pointer must fit in storage for non-SBO case");
105 return std::launder< Functor const >(*reinterpret_cast< Functor* const* >(f->m_storage.data()));
106 }
107 }
108
109 template < auto& function_ref >
110 struct dispatch_for
111 {
112 static constexpr auto& id = function_ref;
113 };
114
115 template < auto& function_ref >
116 auto make_dispatch_for(decltype(function_ref)&) -> dispatch_for< function_ref >
117 {
118 return {};
119 }
120
121 template < auto& Function >
122 static R free_call_impl(function< R(Args...) >* f, Args... args)
123 {
124 return Function(args...);
125 }
126
127 template < auto& Function >
128 static void free_copy_ctor_impl(function< R(Args...) >* self, function< R(Args...) > const* other)
129 {
130 self->m_impl_tbl = &free_vtbl< Function >;
131 self->m_callable = &free_call_impl< Function >;
132 }
133
134 template < auto& Function >
135 static void free_copy_assign_impl(function< R(Args...) >* self, function< R(Args...) > const* other)
136 {
137 self->m_impl_tbl->m_destroy(self);
138 self->m_impl_tbl = &free_vtbl< Function >;
139 self->m_callable = &free_call_impl< Function >;
140 }
141
142 template < auto& Function >
143 static void free_move_ctor_impl(function< R(Args...) >* self, function< R(Args...) >* other) noexcept
144 {
145 self->m_impl_tbl = &free_vtbl< Function >;
146 self->m_callable = &free_call_impl< Function >;
147 }
148
149 template < auto& Function >
150 static void free_move_assign_impl(function< R(Args...) >* self, function< R(Args...) >* other) noexcept
151 {
152 self->m_impl_tbl->m_destroy(self);
153 self->m_impl_tbl = &free_vtbl< Function >;
154 self->m_callable = &free_call_impl< Function >;
155 }
156
157 template < typename Functor >
158 static void copy_ctor_impl(function< R(Args...) >* self, function< R(Args...) > const* other)
159 {
160 if constexpr (use_sbo< Functor >())
161 {
162 Functor* self_ptr = std::launder< Functor >(get_storage_address< Functor >(self));
163 const Functor* other_ptr = std::launder< Functor const >(get_storage_address< Functor >(other));
164 new (self_ptr) Functor(*other_ptr);
165 self->m_impl_tbl = &vtbl< Functor >;
166 self->m_callable = &call_impl< Functor >;
167 }
168 else
169 {
170 // For non-SBO, we need to allocate a new Functor on the heap and copy-construct it.
171 const Functor* other_ptr = get_storage_address< Functor >(other);
172 Functor* new_functor = new Functor(*other_ptr);
173 new ((void*)self->m_storage.data()) Functor*(new_functor);
174 self->m_impl_tbl = &vtbl< Functor >;
175 self->m_callable = &call_impl< Functor >;
176 }
177 }
178
179 template < typename Functor >
180 static void move_ctor_impl(function< R(Args...) >* self, function< R(Args...) >* other) noexcept
181 {
182 if constexpr (use_sbo< Functor >())
183 {
184 Functor* self_ptr = std::launder< Functor >(get_storage_address< Functor >(self));
185 const Functor* other_ptr = std::launder< Functor const >(get_storage_address< Functor >(other));
186 new (self_ptr) Functor(std::move(*other_ptr));
187 self->m_impl_tbl = &vtbl< Functor >;
188 self->m_callable = &call_impl< Functor >;
189 }
190 else
191 {
192 // For move-ctor, just steal the contents
193 Functor* other_ptr = std::launder< Functor >(get_storage_address< Functor >(other));
194 self->m_impl_tbl = &vtbl< Functor >;
195 self->m_callable = &call_impl< Functor >;
196 new ((void*)self->m_storage.data()) Functor*(other_ptr);
197 *std::launder(reinterpret_cast< Functor** >(other->m_storage.data())) = nullptr;
198 other->m_impl_tbl = &void_vtbl;
199 other->m_callable = &null_call_impl;
200 }
201 }
202
203 template < typename Functor >
204 static void copy_assign_impl(function< R(Args...) >* self, function< R(Args...) > const* other)
205 {
206 if (self == other)
207 {
208 return;
209 }
210
211 if constexpr (use_sbo< Functor >())
212 {
213 self->m_impl_tbl->m_destroy(self);
214 poison_region(self, sizeof(self));
215 copy_ctor_impl< Functor >(self, other);
216 if constexpr (sizeof(Functor) < sizeof(m_storage))
217 {
218 // If the functor is smaller than the storage, we can poison the remaining bytes to prevent accidental misuse.
219 poison_region(reinterpret_cast< std::byte* >(self) + sizeof(Functor), sizeof(m_storage) - sizeof(Functor));
220 }
221 }
222 else
223 {
224 // For non-SBO, we need to allocate a new Functor on the heap and copy-construct it.
225 const Functor* other_ptr = get_storage_address< Functor >(other);
226 Functor* new_functor = new Functor(*other_ptr);
227 self->m_impl_tbl->m_destroy(self);
228 new ((void*)self->m_storage.data()) Functor*(new_functor);
229 self->m_impl_tbl = &vtbl< Functor >;
230 self->m_callable = vtbl< Functor >.m_call;
231 }
232 }
233
234 template < typename Functor >
235 static void move_assign_impl(function< R(Args...) >* self, function< R(Args...) >* other) noexcept
236 {
237 if (self == other)
238 {
239 return;
240 }
241 self->m_impl_tbl->m_destroy(self);
242 move_ctor_impl< Functor >(self, other);
243 }
244
245 template < typename Functor >
246 static void destroy_impl(function< R(Args...) >* f) noexcept
247 {
248 Functor* ptr = get_storage_address< Functor >(f);
249 if constexpr (use_sbo< Functor >())
250 {
251 ptr->~Functor();
252 }
253 else
254 {
255 delete ptr;
256 }
257 }
258
259 static void null_copy_ctor_impl(function< R(Args...) >* self, function< R(Args...) > const* other) noexcept
260 {
261 self->m_impl_tbl = &void_vtbl;
262 self->m_callable = &null_call_impl;
263 }
264
265 static void null_move_ctor_impl(function< R(Args...) >* self, function< R(Args...) >* other) noexcept
266 {
267 self->m_impl_tbl = &void_vtbl;
268 self->m_callable = &null_call_impl;
269 }
270
271 static void null_copy_assign_impl(function< R(Args...) >* self, function< R(Args...) > const* other) noexcept
272 {
273 self->m_impl_tbl->m_destroy(self);
274 self->m_impl_tbl = &void_vtbl;
275 self->m_callable = &null_call_impl;
276 }
277
278 static void null_move_assign_impl(function< R(Args...) >* self, function< R(Args...) >* other) noexcept
279 {
280 self->m_impl_tbl->m_destroy(self);
281 self->m_impl_tbl = &void_vtbl;
282 self->m_callable = &null_call_impl;
283 }
284
285 static void null_destroy_impl(function< R(Args...) >* f) noexcept
286 {
287 // No-op since there is no functor to destroy
288 }
289
290 static void null_reset_impl(function< R(Args...) >* f) noexcept
291 {
292 // No-op since the null state is the reset state...
293 poison_region(reinterpret_cast< void* >(&f->m_storage), sizeof(f->m_storage));
294 }
295
296 template < typename Functor >
297 static void reset_impl(function< R(Args...) >* f) noexcept
298 {
299 destroy_impl< Functor >(f);
300 f->m_impl_tbl = &void_vtbl;
301 f->m_callable = &null_call_impl;
302 }
303
304 template < typename Functor >
305 static R call_impl(function< R(Args...) >* f, Args... args)
306 {
307 Functor* ptr = get_storage_address< Functor >(f);
308 return (*ptr)(std::forward< Args >(args)...);
309 }
310
311 static R null_call_impl(function< R(Args...) >* f, Args... args)
312 {
313 throw std::bad_function_call();
314 }
315
316 template < typename Functor >
317 static constexpr impl_tbl make_impl_tbl()
318 {
319 return {.m_copy_ctor = &copy_ctor_impl< Functor >, .m_copy_assign = &copy_assign_impl< Functor >, .m_move_ctor = &move_ctor_impl< Functor >, .m_move_assign = &move_assign_impl< Functor >, .m_reset = &reset_impl< Functor >, .m_destroy = &destroy_impl< Functor >, .m_call = &call_impl< Functor >};
320 }
321
322 static constexpr impl_tbl make_null_impl_tbl()
323 {
324 return {
325 .m_copy_ctor = &null_copy_ctor_impl,
326 .m_copy_assign = &null_copy_assign_impl,
327 .m_move_ctor = &null_move_ctor_impl,
328 .m_move_assign = &null_move_assign_impl,
329 .m_reset = &null_reset_impl,
330 .m_destroy = &null_destroy_impl,
331 .m_call = &null_call_impl,
332 };
333 }
334
335 template < typename Functor >
336 static constexpr impl_tbl const vtbl = make_impl_tbl< Functor >();
337 template < auto& Function >
338 static constexpr impl_tbl const free_vtbl = {.m_copy_ctor = &free_copy_ctor_impl< Function >, .m_copy_assign = &free_copy_assign_impl< Function >, .m_move_ctor = &free_move_ctor_impl< Function >, .m_move_assign = &free_move_assign_impl< Function >, .m_reset = &null_reset_impl, .m_destroy = &null_destroy_impl, .m_call = &free_call_impl< Function >};
339
340 static constexpr impl_tbl const void_vtbl = make_null_impl_tbl();
341
342 public:
343 function() noexcept : m_callable(&null_call_impl), m_impl_tbl(&void_vtbl)
344 {
345 poison_region(reinterpret_cast< void* >(&m_storage), sizeof(m_storage));
346 }
347
348 ~function() noexcept
349 {
350 m_impl_tbl->m_destroy(this);
351 }
352
357 template < auto& fn >
359 {
361 static constexpr auto& id = fn;
362 };
363
369 template < auto& fn >
370 constexpr function_tag< fn > tag_of(decltype(fn)&)
371 {
372 return {};
373 }
374
375 // Optional convenience: accept a function lvalue reference and forward to tag_of
376
382 template < typename Functor, typename = std::enable_if_t< !std::is_same_v< std::decay_t< Functor >, function > > >
383 function(Functor&& f) noexcept(use_sbo< std::decay_t< Functor > >())
384 {
385 using Decayed = std::decay_t< Functor >;
386 static_assert(std::is_invocable_r_v< R, Decayed, Args... >, "Functor must be invokable with the correct signature");
387
388 if constexpr (use_sbo< Decayed >())
389 {
390 new (get_storage_address< Decayed >(this)) Decayed(std::forward< Functor >(f));
391 m_impl_tbl = &vtbl< Decayed >;
392 m_callable = &call_impl< Decayed >;
393 }
394 else
395 {
396 Decayed* storage = new Decayed(std::forward< Functor >(f));
397 new ((void*)m_storage.data()) Decayed*(storage);
398 m_impl_tbl = &vtbl< Decayed >;
399 m_callable = &call_impl< Decayed >;
400 }
401 }
402
404 function(function const& other)
405 {
406 other.m_impl_tbl->m_copy_ctor(this, &other);
407 }
408
410 function(function&& other) noexcept
411 {
412 other.m_impl_tbl->m_move_ctor(this, &other);
413 }
414
416 function& operator=(function const& other)
417 {
418 if (this == &other)
419 {
420 return *this;
421 }
422 other.m_impl_tbl->m_copy_assign(this, &other);
423 return *this;
424 }
425
427 function& operator=(function&& other) noexcept
428 {
429 if (this == &other)
430 {
431 return *this;
432 }
433 other.m_impl_tbl->m_move_assign(this, &other);
434 return *this;
435 }
436
438 operator bool() const noexcept
439 {
440 return m_callable != &null_call_impl;
441 }
442
449 R operator()(Args... args)
450 {
451 return m_callable(this, std::forward< Args >(args)...);
452 }
453
455 bool operator==(std::nullptr_t) const noexcept
456 {
457 return m_callable == &null_call_impl;
458 }
459 };
460
461 static_assert(sizeof(rpnx::function< int(int) >) == 32);
462} // namespace rpnx
463
464#endif // RPNXDATASTRUCTURES_FUNCTIONAL_HPP
Definition functional.hpp:25
bool operator==(std::nullptr_t) const noexcept
Tests whether the function is empty.
Definition functional.hpp:455
function(function const &other)
Copy-constructs a function and its stored callable.
Definition functional.hpp:404
function(function &&other) noexcept
Move-constructs a function, leaving other empty when it owns heap storage.
Definition functional.hpp:410
function(Functor &&f) noexcept(use_sbo< std::decay_t< Functor > >())
Constructs a function from a compatible callable.
Definition functional.hpp:383
function & operator=(function &&other) noexcept
Move-assigns a function.
Definition functional.hpp:427
R operator()(Args... args)
Invokes the stored callable.
Definition functional.hpp:449
constexpr function_tag< fn > tag_of(decltype(fn)&)
Creates a compile-time identity tag for a free function.
Definition functional.hpp:370
function & operator=(function const &other)
Copy-assigns a function and its stored callable.
Definition functional.hpp:416
Definition functional.hpp:22
Containers, iterator adapters, callable wrappers, and value utilities.
Definition annex.hpp:14
void poison_region(void *ptr, std::size_t size)
Marks a memory region as poisoned when instrumentation is enabled.
Definition memory.hpp:20
Compile-time identity tag for a free function.
Definition functional.hpp:359