tiny_delegate.hpp is a compact C++17/C++20 callback library with three related delegate types:
tiny::delegate_ref<Sig>: non-owning, cheap to copy and rebindtiny::delegate_sbo<Sig, InlineBytes, InlineAlign>: owning, move-only, SBO-firsttiny::delegate<Sig, InlineBytes, InlineAlign>: hybrid, move-only, owns by default but can switch into explicit ref mode
It is aimed at projects that want a small, explicit, predictable callback abstraction without immediately defaulting to std::function.
- C++17/C++20 compatible
- No exceptions required by design
- No built-in assert policy forced on the user
- Move-only owning delegates
- Non-owning ref delegate
- Small-buffer optimization
- Optional heap fallback
- Explicit
borrowandbindAPIs - Compile-time fit checks for size and alignment
- Works with move-only callables
tiny_delegate is strong not because it tries to beat every callback library on every axis, but because it hits a very practical balance:
- explicit lifetime semantics
- predictable ownership
- low-level control over size and alignment
- simple API surface
- good fit for embedded and systems code
- It gives you three clear tools instead of one overloaded abstraction:
delegate_reffor non-owning callback viewsdelegate_sbofor owning-only callbacksdelegatefor hybrid "automatic chooser" behavior
tiny::delegateis unusually practical because it can switch between:- function pointer path
- owning functor/lambda path
- explicit
borrow(...)ref path - explicit
bind<&T::method>(obj)ref path
borrowandbindare explicit, which makes lifetime intent visible in code instead of hidden in implicit conversions.- Size and alignment are first-class configuration knobs:
InlineBytesInlineAlignfits_inlinerequired_inline_bytesrequired_inline_alignstatic_assert_fits_inline
- Heap fallback can be disabled completely, which is a very strong property for embedded code.
- Oversized or over-aligned owning callables can fail at compile time instead of surprising you at runtime.
- Introspection is built in:
owning()non_owning()uses_inline()uses_heap()
For ordinary application code, many callback wrappers are "good enough".
For low-level code, firmware, hot paths, or callback-heavy infrastructure, the usual questions are:
- who owns the callable?
- is this a borrowed reference or an owned object?
- can this allocate?
- will this fit inline?
- what happens if alignment is larger than expected?
tiny_delegate answers those questions directly in the API.
llvm::function_ref is a non-owning callable reference and is mainly intended for short-lived parameter use.
tiny_delegate gives you that style through delegate_ref, but also adds:
- owning delegates
- SBO
- optional heap fallback
- explicit member binding
- one hybrid type that can do both owning and ref modes
If you only want a tiny non-owning parameter wrapper, llvm::function_ref is simpler.
If you want one small library that covers both stored and non-stored callbacks, tiny_delegate is broader.
absl::AnyInvocable is a move-only owning callable wrapper.
tiny_delegate is stronger when you want:
- an explicit non-owning companion type
- explicit
borrow(...) - explicit method binding
- size and alignment surfaced as part of the public API
- embedded-friendly "no heap fallback" configuration
If you only need a production-grade move-only owning wrapper inside the Abseil ecosystem, AnyInvocable is a strong choice.
If you want a unified owning + non-owning delegate toolbox, tiny_delegate gives you more structure.
function2 is a very feature-rich library. It supports copyable wrappers, move-only wrappers, non-owning views, richer qualifier handling and multi-signature overload support.
tiny_delegate is attractive when you want something smaller and easier to reason about:
- simpler mental model
- explicit ownership split
- direct embedded-oriented size/alignment controls
- direct
borrow/bindAPIs
If you need maximum wrapper expressiveness, function2 may be stronger.
If you want a smaller, more focused delegate library with very explicit semantics, tiny_delegate is often the nicer fit.
ETL-style delegates are often very good for embedded work, especially when you want non-owning callback references.
tiny_delegate adds a useful extra layer:
- a dedicated non-owning type
- a dedicated owning type
- a hybrid type that can choose automatically
- compile-time size/alignment fit tooling for owned callables
That makes it attractive when your project mixes classic embedded callback references with modern lambdas and move-only functors.
tiny_delegate is not trying to be the best at everything.
Other libraries may be better if you need:
- copyable owning delegates as the main API
- allocator-aware wrappers
- multiple overload signatures in one wrapper type
- full cv/ref/noexcept-qualified wrapper signatures
- a very large existing ecosystem around the callback type
Its strength is not "maximum feature count". Its strength is clarity, predictability, and a very good ownership model for real systems code.
#include "tiny_delegate.hpp"Non-owning callback view.
- Stores a free-function pointer, or
- stores a pointer to an external callable/object
Properties:
- copyable
- does not manage lifetime
- very cheap to copy and reassign
Use it when:
- the target object definitely outlives the callback
- you want maximum cheapness
- you do not want ownership at all
Owning callback.
Properties:
- move-only
- owns the callable
- stores inline when it fits
- can optionally fall back to heap if enabled
- has no borrow/ref mode
Use it when:
- you want ownership only
- you want the type itself to communicate "this callback owns its target"
- you want SBO behavior without the hybrid ref path
Hybrid callback.
Properties:
- move-only
- function pointer path for free functions and captureless non-generic lambdas
- owning path for normal lambdas/functors
- explicit ref path via
tiny::borrow(x) - explicit ref path via
tiny::bind<&T::method>(obj)
Use it when:
- you want one type for most callback use cases
- sometimes you want ownership, sometimes borrow/bind
| Need | Recommended type |
|---|---|
| Pure non-owning callback view | tiny::delegate_ref<Sig> |
| Always own the callable | tiny::delegate_sbo<Sig, ...> |
| One general-purpose callback type | tiny::delegate<Sig, ...> |
| Embedded code with strict lifetime discipline | tiny::delegate_ref<Sig> or tiny::delegate<Sig, ...> with heap fallback off |
| Source / behavior | delegate_ref |
delegate_sbo |
delegate |
|---|---|---|---|
| Free function pointer | yes | yes | yes |
| Captureless non-generic lambda | yes, through function-pointer conversion | yes | yes |
| Stateful lambda | yes, only via tiny::borrow(lvalue) |
yes, as owned callable | yes, as owned callable |
| Move-only lambda | no | yes | yes |
| Functor object | yes, only via tiny::borrow(lvalue) |
yes | yes |
Direct tiny::borrow(...) API |
yes | no | yes |
| Direct method-bind API | yes | no | yes |
| Copyable type | yes | no | no |
| Can use heap fallback | no | yes | yes |
| Can be in explicit ref mode | yes | no | yes |
Accepted forms:
- free-function pointer
- captureless non-generic lambda, via conversion to function pointer
tiny::borrow(functor_lvalue)tiny::borrow(generic_lambda_lvalue)tiny::delegate_ref<Sig>::bind<&T::method>(obj)
Examples:
int plus_one(int x) { return x + 1; }
struct Functor {
int operator()(int x) { return x + 10; }
};
struct Device {
int read(int x) { return x + 100; }
};
Functor fn;
Device dev;
tiny::delegate_ref<int(int)> a = &plus_one;
tiny::delegate_ref<int(int)> b = tiny::borrow(fn);
tiny::delegate_ref<int(int)> c =
tiny::delegate_ref<int(int)>::bind<&Device::read>(dev);Not supported:
- owning a temporary lambda/functor
- move-only callable ownership
- heap fallback
Accepted forms:
- free-function pointer
- captureless non-generic lambda
- generic lambda, as owned callable
- stateful lambda
- move-only lambda
- functor object
Examples:
tiny::delegate_sbo<int(int), 64> a = &plus_one;
tiny::delegate_sbo<int(int), 64> b = [](int x) { return x + 2; };
tiny::delegate_sbo<int(int), 64> c = [sum = 0](int x) mutable { return sum += x; };
tiny::delegate_sbo<int(int), 64> d = Functor{};Not supported:
tiny::borrow(...)tiny::bind<&T::method>(obj)- ref mode of any kind
If you really need to call an external object through delegate_sbo, you can still wrap it manually:
struct Device {
int read(int x) { return x + 1; }
};
Device dev;
tiny::delegate_sbo<int(int), 64> cb = [&dev](int x) {
return dev.read(x);
};But note what this means:
delegate_sboowns the lambda wrapper- it does not own
dev devmust still outlive callback use- this is not the same as library-level
borrow/bind
Accepted forms:
- free-function pointer
- captureless non-generic lambda
- generic lambda, as owned callable
- stateful lambda
- move-only lambda
- functor object
tiny::borrow(functor_lvalue)tiny::bind<&T::method>(obj)
Examples:
tiny::delegate<int(int)> a = &plus_one;
tiny::delegate<int(int)> b = [](int x) { return x + 2; };
tiny::delegate<int(int)> c = [sum = 0](int x) mutable { return sum += x; };
tiny::delegate<int(int)> d = Functor{};
tiny::delegate<int(int)> e = tiny::borrow(fn);
tiny::delegate<int(int)> f = tiny::bind<&Device::read>(dev);This is the "automatic chooser" type:
- if the input behaves like a function pointer, it uses the function-pointer path
- if the input is a normal callable object, it owns it
- if the input is
borrow(...), it becomes non-owning - if the input is
bind(...), it becomes non-owning method binding
Define these before including the header.
#define TINY_DELEGATE_DEFAULT_BYTES 64
#define TINY_DELEGATE_DEFAULT_ALIGN alignof(std::max_align_t)
#define TINY_DELEGATE_ENABLE_HEAP_FALLBACK 0
#define TINY_DELEGATE_ASSERT(expr, msg) ((void)0)
#include "tiny_delegate.hpp"Default inline byte capacity used by:
tiny::delegate<Sig>- free
tiny::bind<&T::method>(obj)helper
Default:
64Default inline alignment used by tiny::delegate<Sig>.
Default:
alignof(std::max_align_t)Controls behavior when an owning callable does not fit inline.
0: compile-time failure1: allocate on heap
Default:
0User hook for runtime assertions.
Default:
#define TINY_DELEGATE_ASSERT(expr, msg) ((void)0)Example:
#include <cassert>
#define TINY_DELEGATE_ASSERT(expr, msg) assert((expr) && (msg))
#include "tiny_delegate.hpp"#include "tiny_delegate.hpp"
int plus_one(int x) { return x + 1; }
struct Worker {
int base = 10;
int add(int x) { return base + x; }
};
int main() {
tiny::delegate<int(int)> a = &plus_one;
Worker w{20};
tiny::delegate<int(int)> b = tiny::bind<&Worker::add>(w);
auto counter = [sum = 0](int x) mutable {
sum += x;
return sum;
};
tiny::delegate<int(int)> c = counter;
return a(1) + b(2) + c(3);
}Yes, you can use auto for local variables when the initializer already creates a delegate object.
Examples:
auto counter = [sum = 0](int x) mutable { return sum += x; };
Worker w{20};
auto a = tiny::delegate<int(int)>{&plus_one};
auto b = tiny::delegate_sbo<int(int), 64>{[](int x) { return x + 2; }};
auto c = tiny::delegate_ref<int(int)>{tiny::borrow(counter)};
auto d = tiny::bind<&Worker::add>(w);Important:
auto cb = &plus_one;This is only a raw function pointer:
int (*)(int)It is not a tiny::delegate.
So these are different:
auto a = &plus_one; // raw function pointer
auto b = tiny::delegate<int(int)>{&plus_one}; // delegate objectPractical rule:
- for API surface, class members, typedefs and examples that teach the library, prefer the explicit delegate type
- for local variables,
autois fine if the initializer already makes the delegate type obvious
tiny::delegate<void()> cb;
if (!cb) {
// empty
}
cb = nullptr;The same idea works for:
tiny::delegate_ref<Sig>tiny::delegate_sbo<Sig, ...>tiny::delegate<Sig, ...>
Calling an empty delegate is only guarded by TINY_DELEGATE_ASSERT, so if your project wants hard failure on misuse, override that macro.
tiny::delegate_ref<Sig>is copyabletiny::delegate_sbo<Sig, ...>is move-onlytiny::delegate<Sig, ...>is move-only
Owning delegates are move-only so they can hold move-only callables.
int on_value(int x) { return x + 5; }
tiny::delegate<int(int)> cb = &on_value;
int y = cb(7); // 12This uses the function-pointer path.
tiny::delegate<int(int)> cb = [](int x) { return x * 2; };
int y = cb(9); // 18Because the lambda is captureless and non-generic, it converts to a function pointer.
auto stateful = [sum = 0](int x) mutable {
sum += x;
return sum;
};
tiny::delegate<int(int)> cb = stateful;
int a = cb(1); // 1
int b = cb(2); // 3
int c = stateful(5); // 5, separate stateImportant:
statefulkeeps its own statecbstores its own copied state
This is expected behavior.
#include <memory>
tiny::delegate<int(int)> cb = [ptr = std::make_unique<int>(40)](int x) {
return *ptr + x;
};
int y = cb(2); // 42Also works with delegate_sbo:
tiny::delegate_sbo<int(int), 64> cb = [ptr = std::make_unique<int>(7)](int x) {
return *ptr + x;
};struct Accumulator {
int total = 0;
int operator()(int x) {
total += x;
return total;
}
};
Accumulator acc;
tiny::delegate_ref<int(int)> ref = tiny::borrow(acc);
int a = ref(3); // 3
int b = ref(4); // 7ref does not own acc.
Accumulator acc;
tiny::delegate<int(int)> cb = tiny::borrow(acc);
int a = cb(3); // 3
int b = cb(4); // 7
bool is_ref = cb.non_owning(); // true
bool own = cb.owning(); // false
bool inl = cb.uses_inline(); // false
bool heap = cb.uses_heap(); // falseThis is the hybrid delegate in explicit ref mode.
struct Adder {
int bias = 10;
int operator()(int x) const { return bias + x; }
};
const Adder add{};
tiny::delegate<int(int)> cb = tiny::borrow(add);
int y = cb(5); // 15This works because the callable is invocable as const.
struct Device {
int base = 100;
int read(int x) { return base + x; }
};
Device dev{100};
tiny::delegate<int(int)> cb = tiny::bind<&Device::read>(dev);
int y = cb(23); // 123struct Sensor {
int base = 50;
int sample(int x) const { return base + x; }
};
const Sensor s{50};
tiny::delegate<int(int)> cb = tiny::bind<&Sensor::sample>(s);
int y = cb(2); // 52If you want a pure ref delegate for a method:
struct Driver {
void tick() {}
};
Driver d;
tiny::delegate_ref<void()> ref =
tiny::delegate_ref<void()>::bind<&Driver::tick>(d);using Task = tiny::delegate_sbo<void(), 32>;
Task t = [] {
// owned callable
};This type never enters borrow/bind ref mode.
using SmallCb = tiny::delegate<int(int), 16>;
SmallCb cb = [](int x) { return x + 4; };
int y = cb(3); // 7If the callable does not fit and heap fallback is off, compilation fails.
Sometimes a callable fits by size but not by alignment.
struct alignas(32) BigAlign {
int operator()(int x) const { return x + 1; }
};On many platforms the default inline alignment is 16, so this does not fit inline by default.
Use a larger alignment if you want inline storage:
using AlignedCb = tiny::delegate<int(int), 64, 32>;
AlignedCb cb = BigAlign{};
int y = cb(5); // 6The same idea applies to tiny::delegate_sbo.
struct MyFunctor {
int operator()(int) const { return 0; }
};
using D = tiny::delegate<int(int), 64>;
static_assert(D::fits_inline<MyFunctor>());
static_assert(D::required_inline_bytes<MyFunctor>() == sizeof(MyFunctor));
static_assert(D::required_inline_align<MyFunctor>() == alignof(MyFunctor));If you want a clearer compile-time failure:
using D = tiny::delegate<int(int), 64, 16>;
D::static_assert_fits_inline<MyFunctor>();Also available on tiny::delegate_sbo.
#define TINY_DELEGATE_ENABLE_HEAP_FALLBACK 1
#include "tiny_delegate.hpp"
struct Large {
char payload[256]{};
int operator()(int x) const { return x + 1; }
};
tiny::delegate<int(int), 64> cb = Large{};
bool heap = cb.uses_heap(); // true
bool inl = cb.uses_inline(); // falseThe same pattern works for tiny::delegate_sbo.
With the default policy:
#define TINY_DELEGATE_ENABLE_HEAP_FALLBACK 0
#include "tiny_delegate.hpp"an oversized or over-aligned owning callable becomes a compile-time error.
This is often exactly what embedded code wants.
tiny::delegate<void()> cb = [] {};
cb = nullptr;
if (!cb) {
// empty again
}The same works for:
tiny::delegate_ref<Sig>tiny::delegate_sbo<Sig, ...>
int plus_one(int x) { return x + 1; }
int plus_two(int x) { return x + 2; }
tiny::delegate<int(int)> cb = &plus_one;
cb = &plus_two;
cb = nullptr;
cb = &plus_one;Rebinding an owning delegate destroys the previous owned target and installs the new one.
int plus_one(int x) { return x + 1; }
struct Worker {
int run(int x) const { return x; }
};
using Sig1 = tiny::sig_of_t<decltype(&plus_one)>; // int(int)
using Sig2 = tiny::sig_of_t<decltype(&Worker::run)>; // int(int)The free tiny::bind<&T::method>(obj) helper uses this same idea internally.
Free helper:
auto cb = tiny::bind<&Device::read>(dev);Return type:
tiny::delegate<tiny::sig_of_t<decltype(&Device::read)>>That means the free helper uses the global defaults:
TINY_DELEGATE_DEFAULT_BYTESTINY_DELEGATE_DEFAULT_ALIGN
If you need custom inline capacity or alignment, use the typed form:
using FastCb = tiny::delegate<int(int), 32, 32>;
FastCb cb = FastCb::bind<&Device::read>(dev);Generic lambdas are not function pointers, but they still work as callable objects if the requested signature is valid.
Owned case:
auto generic = [](auto x) { return x + 1; };
tiny::delegate<int(int)> a = generic;
tiny::delegate_sbo<int(int), 64> b = generic;Borrowed case:
tiny::delegate_ref<int(int)> ref = tiny::borrow(generic);
tiny::delegate<int(int)> borrowed = tiny::borrow(generic);The important distinction is:
- captureless non-generic lambda can use the function-pointer path
- generic lambda is treated like a normal functor object
This section is intentionally repetitive. It is here so the README can be used as a quick reference when you are trying to remember "can I do X with this delegate type?".
tiny::delegate<void()> cb = [] {
// do something
};
if (cb) {
cb();
}tiny::delegate<int(int, int)> add = [](int a, int b) {
return a + b;
};
int sum = add(2, 3); // 5class JobQueue {
public:
using DoneCb = tiny::delegate<void(int), 32>;
void setDoneCallback(DoneCb cb) {
done_ = std::move(cb);
}
void finish(int code) {
if (done_) done_(code);
}
private:
DoneCb done_;
};using Callback = tiny::delegate<void(int), 32>;
void subscribe(Callback cb) {
if (cb) cb(42);
}
subscribe([](int value) {
// value == 42
});tiny::delegate<int(int)> make_scaler(int base) {
return [base](int x) {
return base * x;
};
}
auto cb = make_scaler(3);
int y = cb(4); // 12struct Device {
int base = 7;
int read(int x) const { return base + x; }
};
tiny::delegate<int(int)> make_reader(const Device& dev) {
return tiny::bind<&Device::read>(dev);
}The object must still outlive the returned delegate.
struct Counter {
int total = 0;
int operator()(int x) {
total += x;
return total;
}
};
Counter counter;
tiny::delegate_ref<int(int)> a = tiny::borrow(counter);
tiny::delegate_ref<int(int)> b = a;
int x = a(2); // 2
int y = b(3); // 5Both refs target the same external object.
struct Device {
int base = 100;
int operator()(int x) { return base + x; }
int read(int x) { return base + x; }
};
Device dev;
tiny::delegate<int(int)> cb = [](int x) { return x + 1; }; // owning
cb = tiny::borrow(dev); // ref mode through operator()
cb = tiny::bind<&Device::read>(dev); // ref mode by method binding
cb = [value = 5](int x) { return value + x; }; // back to owningtiny::delegate is the only one of the three types that can switch like this.
int external = 10;
tiny::delegate<int()> cb = [&external] {
return external;
};
external = 20;
int y = cb(); // 20The delegate owns the lambda object, but the lambda object still refers to external.
tiny::delegate<int(int)> cb = [count = 0](int x) mutable {
count += x;
return count;
};
int a = cb(1); // 1
int b = cb(1); // 2
int c = cb(5); // 7struct Task {
void run() {
// do work
}
};
class Scheduler {
public:
using Callback = tiny::delegate<void(), 32>;
void set(Callback cb) {
cb_ = std::move(cb);
}
void tick() {
if (cb_) cb_();
}
private:
Callback cb_;
};
Task task;
Scheduler sch;
sch.set(tiny::bind<&Task::run>(task));struct Task {
void run() {}
};
class Scheduler {
public:
using Callback = tiny::delegate_ref<void()>;
void set(Callback cb) { cb_ = cb; }
void tick() { if (cb_) cb_(); }
private:
Callback cb_;
};
Task task;
Scheduler sch;
sch.set(tiny::delegate_ref<void()>::bind<&Task::run>(task));class WorkerPool {
public:
using Job = tiny::delegate_sbo<void(), 48>;
void setJob(Job job) {
job_ = std::move(job);
}
void execute() {
if (job_) job_();
}
private:
Job job_;
};#define TINY_DELEGATE_ENABLE_HEAP_FALLBACK 1
#include "tiny_delegate.hpp"
struct Large {
char payload[256]{};
void operator()() const {}
};
tiny::delegate<void(), 64> cb = Large{};
if (cb.uses_heap()) {
// oversized callable went to heap
}Only meaningful when heap fallback is enabled.
tiny::delegate_sbo<void(), 64> cb = [] {
// small callable
};
bool inl = cb.uses_inline();
bool heap = cb.uses_heap();tiny::delegate64<void()> a = [] {};
tiny::delegate32<int(int)> b = [](int x) { return x + 1; };
tiny::delegate_sbo64<void()> c = [] {};
tiny::delegate_sbo32<int(int)> d = [](int x) { return x + 2; };These are only shorthand for common byte sizes.
struct Device {
int read(int x) const { return x + 1; }
};
using Callback = tiny::delegate<int(int), 64, 32>;
Device dev;
Callback cb = Callback::bind<&Device::read>(dev);auto ok_lambda = [count = 0](int x) mutable { return count += x; };
auto ok = tiny::borrow(ok_lambda); // ok
// auto bad = tiny::borrow([count = 0](int x) mutable { return count += x; });
// error: cannot borrow a temporarystruct Device {
int read(int x) const { return x + 1; }
};
Device dev;
auto ok = tiny::bind<&Device::read>(dev); // ok
// auto bad = tiny::bind<&Device::read>(Device{});
// error: cannot bind a temporaryauto generic = [](auto x) { return x + 10; };
tiny::delegate_ref<int(int)> ref = tiny::borrow(generic);
tiny::delegate<int(int)> cb = tiny::borrow(generic);This stays non-owning.
auto generic = [](auto x) { return x * 2; };
tiny::delegate<int(int)> cb = generic;
tiny::delegate_sbo<int(int), 64> box = generic;This stores the generic lambda as a normal callable object.
struct Device {
int read(int x) { return x + 1; }
};
class FastApi {
public:
using Callback = tiny::delegate_ref<int(int)>;
void set(Callback cb) { cb_ = cb; }
private:
Callback cb_;
};
class FlexibleApi {
public:
using Callback = tiny::delegate<int(int), 32>;
void set(Callback cb) { cb_ = std::move(cb); }
private:
Callback cb_;
};Same overall pattern, different ownership model.
int plus_one(int x) { return x + 1; }
auto make_callback() {
return tiny::delegate<int(int)>{&plus_one};
}This is a good use of auto, because the returned object is already a delegate.
struct Device {
int read(int x) const { return x + 5; }
};
Device dev;
auto cb = tiny::bind<&Device::read>(dev);Here auto is fine because tiny::bind already returns a delegate object.
int plus_one(int x) { return x + 1; }
auto raw = &plus_one; // raw function pointer, not tiny::delegateThis is the main auto pitfall to remember.
tiny::delegate<int(int)> cb;
bool empty = !cb;
bool ref = cb.non_owning();
bool own = cb.owning();
bool inl = cb.uses_inline();
bool heap = cb.uses_heap();Meaning:
non_owning(): callback is in borrow/bind ref modeowning(): callback owns somethinguses_inline(): callback owns inline storageuses_heap(): callback owns heap storage
Important:
If cb is in ref mode, then:
cb.non_owning(); // true
cb.owning(); // false
cb.uses_inline();// false
cb.uses_heap(); // falseuses_inline() is intentionally about owned inline storage, not "anything that is not heap".
tiny::delegate_sbo<int(int), 64> cb = [](int x) { return x + 1; };
bool inl = cb.uses_inline();
bool heap = cb.uses_heap();class Button {
public:
using Callback = tiny::delegate<void(), 32>;
void setOnClick(Callback cb) {
onClick_ = std::move(cb);
}
void click() {
if (onClick_) onClick_();
}
private:
Callback onClick_;
};Usage:
Button b;
b.setOnClick([] {
// ...
});class Dispatcher {
public:
using RxCb = tiny::delegate_ref<void(const std::uint8_t*, std::size_t)>;
void setHandler(RxCb cb) { handler_ = cb; }
void receive(const std::uint8_t* data, std::size_t size) {
if (handler_) handler_(data, size);
}
private:
RxCb handler_;
};This is a good fit when:
- lifetime is guaranteed externally
- rebinding should be cheap
- you want no ownership overhead
#define TINY_DELEGATE_ENABLE_HEAP_FALLBACK 0
#define TINY_DELEGATE_DEFAULT_BYTES 64
#define TINY_DELEGATE_DEFAULT_ALIGN alignof(std::max_align_t)
#include "tiny_delegate.hpp"Effect:
- small callables fit inline
- oversized callables fail at compile time
- no hidden heap allocation
For extremely hot paths with stable lifetime, tiny::delegate_ref is often the cleanest choice.
This section is the most important one.
Correct:
auto counter = [sum = 0](int x) mutable { return sum += x; };
tiny::delegate<int(int)> cb = tiny::borrow(counter);Wrong:
tiny::delegate<int(int)> cb =
tiny::borrow([sum = 0](int x) mutable { return sum += x; }); // errorBorrowing a temporary is forbidden.
Correct:
Device dev;
auto cb = tiny::bind<&Device::read>(dev);Wrong:
auto cb = tiny::bind<&Device::read>(Device{}); // errorBinding a temporary is forbidden.
int external = 10;
tiny::delegate<int()> cb = [&external] {
return external;
};The delegate owns the lambda object.
But the lambda object still refers to external, so external must outlive callback use.
Inline fit depends on both:
sizeof(callable) <= InlineBytesalignof(callable) <= InlineAlign
That means:
- enough bytes is not enough
- over-aligned callables may require custom
InlineAlign
This is expected behavior, not a bug.
template <class Sig> using delegate64 = delegate<Sig, 64>;
template <class Sig> using delegate32 = delegate<Sig, 32>;
template <class Sig> using delegate_sbo64 = delegate_sbo<Sig, 64>;
template <class Sig> using delegate_sbo32 = delegate_sbo<Sig, 32>;These are only convenience aliases.
- It does not manage lifetime of borrowed/bound targets
- It does not emulate
std::bindfull argument binding - It does not copy owning delegates
- It does not silently heap-allocate unless you enabled heap fallback
Because:
auto lambda = [count = 0](int x) mutable { return count += x; };
tiny::delegate<int(int)> cb = lambda;stores a copy of lambda.
Because alignment is checked independently from size.
Use a larger alignment:
using D = tiny::delegate<int(int), 64, 32>;Because free tiny::bind<&T::method>(obj) always returns tiny::delegate<sig> with the global defaults.
If you need a custom configuration, use:
using D = tiny::delegate<int(int), 32, 32>;
D cb = D::bind<&T::method>(obj);Use:
tiny::delegate_ref<Sig>for non-owning callback viewstiny::delegate_sbo<Sig, ...>for explicit owning SBO delegatestiny::delegate<Sig, ...>for the general case
Use:
tiny::borrow(x)when lifetime is guaranteed externallytiny::bind<&T::method>(obj)when you want to bind only the objectfits_inline,required_inline_bytes,required_inline_align, andstatic_assert_fits_inlinewhen you want compile-time confidence
For embedded systems, keeping heap fallback off is usually the most honest and predictable policy.
