C++23 std::move_only_function — A Callable Wrapper That Doesn't Force Copy-ability
Why C++23 finally fixes move-only callbacks, task queues, and coroutine continuations that std::function couldn’t handle
C++23 std::move_only_function — A Callable Wrapper That Doesn't Force Copy-ability
Why C++23 finally fixes move-only callbacks, task queues, and coroutine continuations that std::function couldn’t handle

**std::function has been a source of friction for me for years. Not because it's bad — it's genuinely useful — but because it has an awkward constraint: it requires that whatever you store in it is copyable**. That requirement has influenced and constrained callback API designs since C++11.
C++23 gives us **std::move_only_function**, and it's one of those features where you go "why didn't we have this from the start?"
The Problem: std::function Forces Copyability
Say you’ve got a unique resource — a database connection, a file handle, something wrapped in a **std::unique_ptr. You want to create a callback that **owns this resource:
#include <functional>
#include <memory>
#include <iostream>
void register_callback(std::function<void()> cb) {
cb();
}
int main() {
auto resource = std::make_unique<int>(42);
auto callback = [res = std::move(resource)]() {
std::cout << "Resource value: " << *res << "\n";
};
// This will NOT compile.
register_callback(std::move(callback));
}
In file included from /usr/include/c++/13/functional:59,
from call.cpp:1:
/usr/include/c++/13/bits/std_function.h: In instantiation of ‘std::function<_Res(_ArgTypes ...)>::function(_Functor&&) [with _Functor = main()::<lambda()>; _Constraints = void; _Res = void; _ArgTypes = {}]’:
call.cpp:17:22: required from here
/usr/include/c++/13/bits/std_function.h:439:69:
error: static assertion failed:
std::function target must be copy-constructible
439 | static_assert(is_copy_constructible<__decay_t<_Functor>>::value,
| ^~~~~
/usr/include/c++/13/bits/std_function.h:439:69: note: ‘std::integral_constant<bool, false>::value’ evaluates to false
Compiler error. The lambda captured a **unique_ptr by move, making it move-only. But `std::function** demands its stored callable is copy-constructible — even if you never actually copy thestd::function` itself.

The workarounds over the years — wrapping in **shared_ptr, writing custom move-only wrappers, reverting to C-style `void*`** callbacks — all worked, none were good. Every large codebase I've worked on had its own bespoke solution.
The Solution: std::move_only_function
C++23 introduces **std::move_only_function in `<functional>`. It's a type-erased callable wrapper that only requires the stored callable to be **movable, not copyable.
#include <functional>
#include <memory>
#include <iostream>
void register_callback(std::move_only_function<void()> cb) {
cb();
}
int main() {
auto resource = std::make_unique<int>(42);
auto callback = [res = std::move(resource)]() {
std::cout << "Resource value: " << *res << "\n";
};
// Compiles and works.
register_callback(std::move(callback));
}
Drop-in replacement for the move-only case.
Ok the above example was a simple one.
If you’ve ever built a thread pool or task queue, you’ve hit this problem. Tasks are submitted, moved into a queue, popped, executed, destroyed. Nobody copies them. Yet **std::function** demands copyability anyway:
#include <functional>
#include <memory>
#include <queue>
#include <iostream>
class TaskQueue {
std::queue<std::function<void()>> tasks_; // ← the problem
public:
void submit(std::function<void()> task) {
tasks_.push(std::move(task));
}
void run_next() {
if (tasks_.empty()) return;
auto task = std::move(tasks_.front());
tasks_.pop();
task();
}
};
int main() {
TaskQueue q;
auto db_conn = std::make_unique<int>(9001);
// Won't compile — lambda is move-only, std::function refuses it
q.submit([conn = std::move(db_conn)]() {
std::cout << "Querying with connection " << *conn << "\n";
});
}
The queue never copies anything. Every task moves in, moves out, runs once, gets destroyed. But **std::function doesn't care about what you actually do — it checks what you **could do, and rejects the move-only lambda at construction time.
Swap in move_only_function and the same code just works:
#include <functional>
#include <memory>
#include <queue>
#include <iostream>
class TaskQueue {
std::queue<std::move_only_function<void()>> tasks_; // ← fixed
public:
void submit(std::move_only_function<void()> task) {
tasks_.push(std::move(task));
}
void run_next() {
if (tasks_.empty()) return;
auto task = std::move(tasks_.front());
tasks_.pop();
task();
}
};
int main() {
TaskQueue q;
auto db_conn = std::make_unique<int>(9001);
// Compiles — move_only_function accepts move-only callables
q.submit([conn = std::move(db_conn)]() {
std::cout << "Querying with connection " << *conn << "\n";
});
auto file = std::make_unique<std::string>("/var/log/app.log");
q.submit([f = std::move(file)]() {
std::cout << "Writing to " << *f << "\n";
});
q.run_next(); // "Querying with connection 9001"
q.run_next(); // "Writing to /var/log/app.log"
}
The API is identical. The only change is the type in the queue and the function signatures. No hacks, no **shared_ptr** indirection, no custom wrapper class maintained by that one person who left the team in 2019.

A Subtle Bug I Traced Back to Forced Copyability
Before I get into coroutines and const-correctness, let me tell you about a real failure pattern I’ve seen.
Side note: If you’re short on time, skip to the next section — otherwise, here’s a real production bug this pattern can cause.
We had an event system. Handlers were stored as **std::function<void(Event const&)> in a std::vector. One handler owned a logging session — a `unique_ptr<LogSession>** — that wrote structured events to disk. Becausestd::function` refused the move-only lambda, someone wrapped the session in a **shared_ptr** to make it work:
// The "fix" that caused the bug
auto session = std::make_shared<LogSession>("/var/log/events.log");
dispatcher.on("user.login", [session](Event const& e) {
session->write(e);
});
Looked fine. Worked fine — for months. Then someone added a feature that cloned the handler list to create a “snapshot” of the dispatch table for replay purposes. Now two vectors held **std::function objects that shared the same `LogSession** viashared_ptr`. Both wrote to the same file. Interleaved. Corrupted output. No crash, no obvious error — just silently garbled logs that nobody noticed until an audit three weeks later.
The root cause wasn’t the **shared_ptr. It was that `std::function** *forced* shared ownership where exclusive ownership was the correct model. If the handler had been amove_only_function`, the snapshot code wouldn't have compiled — because you can't copy a **move_only_function**. The developer would have been forced to confront the ownership question at compile time instead of discovering it in production.
// With move_only_function — the snapshot code fails to compile
std::vector<std::move_only_function<void(Event const&)>> handlers_;
// Later, someone tries:
auto snapshot = handlers_; // Compile error — can't copy move_only_function
// They're forced to think: do I need to move these? Deep-copy the state?
// Share explicitly? The type system made them ask the right question.
This is the kind of bug that **move_only_function** doesn't just fix — it prevents the conditions that lead to it.
Const-Correctness: Fixing a Semantic Mismatch
**std::function::operator() is always `const**, but it will happily call a **non-const**operator()` on the stored callable:
#include <functional>
#include <iostream>
int main() {
int counter = 0;
const std::function<void()> f = [counter]() mutable {
std::cout << counter++ << "\n";
};
f(); // prints 0
f(); // prints 1 — state changed through a const reference
}
This is a deliberate design choice, not a bug — **const on `std::function** means the ***wrapper* is const**, **not the *stored callable's state***. But it creates a gap between whatconst` appears to mean at the call site and what actually happens. Whether this bothers you depends on your codebase's const-correctness standards.
**std::move_only_function** makes the qualifier explicit in the type:
// Non-const: operator() can mutate the stored callable's state
std::move_only_function<void()> mutable_fn;
// Const: operator() propagates const to the stored callable
std::move_only_function<void() const> const_fn;

The Coroutine Connection
If you’re using C++20 coroutines, move_only_function is a strong default for continuation storage — though not the only option (custom intrusive continuations, function_ref for non-owning cases, and allocation-free trampolines all have their place depending on your constraints).
Let’s separate two things clearly, because they get conflated a lot:
The coroutine handle — std::coroutine_handle is trivially copyable. It's just a pointer to the coroutine frame. Nothing move-only about it.
The continuation wrapper — this is the lambda (or callable) you build around the handle, often capturing owned state alongside it. This is the thing you type-erase and store in your scheduler. Since resuming a coroutine twice is undefined behavior, this wrapper benefits from single-owner semantics.
**move_only_function* models that second thing well. It doesn't prevent all misuse — you could still stash the raw handle separately and resume it twice — but it makes accidental double-ownership of the wrapper* a compile error instead of a runtime surprise.
Here’s a complete example — a minimal coroutine task system with move_only_function as the continuation backbone:
#include <coroutine>
#include <functional>
#include <iostream>
#include <memory>
#include <queue>
#include <utility>
struct Task {
struct promise_type {
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
};
class Scheduler {
std::queue<std::move_only_function<void()>> ready_queue_;
public:
static Scheduler& instance() {
static Scheduler s;
return s;
}
void schedule(std::move_only_function<void()> work) {
ready_queue_.push(std::move(work));
}
void run() {
while (!ready_queue_.empty()) {
auto task = std::move(ready_queue_.front());
ready_queue_.pop();
task();
}
}
};
// Awaitable that defers to the scheduler
struct Defer {
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
Scheduler::instance().schedule([h]() { h.resume(); });
}
void await_resume() const noexcept {}
};
// Awaitable that owns a move-only resource
struct AsyncQuery {
std::unique_ptr<std::string> connection;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
auto conn = std::move(connection);
Scheduler::instance().schedule(
[h, c = std::move(conn)]() mutable {
std::cout << " [db] queried via " << *c << "\n";
h.resume();
}
);
}
void await_resume() const noexcept {}
};
Task do_work() {
std::cout << "1. starting work\n";
co_await Defer{};
std::cout << "2. resumed after defer\n";
auto conn = std::make_unique<std::string>("postgres://localhost/mydb");
co_await AsyncQuery{std::move(conn)};
std::cout << "3. query complete\n";
co_await Defer{};
std::cout << "4. all done\n";
}
int main() {
do_work();
Scheduler::instance().run();
}
Output:
1. starting work
2. resumed after defer
[db] queried via postgres://localhost/mydb
3. query complete
4. all done
The flow through the system looks like this:

Interface and Design Differences

**move_only_function** doesn't need copy semantics in its interface or usage model, which gives implementations freedom to omit copy-related machinery internally. Whether that translates to measurable differences depends on your implementation and workload — the primary win is correctness and expressiveness.
**move_only_function also supports `noexcept** in the signature, whichstd::function` never could:
std::move_only_function<void() noexcept> safe_callback;
std::move_only_function<void() const noexcept> ultra_safe;
When to Use Which?

Rule of thumb: if you’re not sure you need to copy the callable, start with **move_only_function. It's the more restrictive default. You can always relax to `std::function` later if you genuinely need copy semantics**.
Closing Thoughts
std::move_only_function fills a gap that's been frustrating C++ developers for over a decade:
- Stores move-only callables — unique_ptr captures, non-copyable state, no workarounds needed.
- Natural fit for coroutine continuations — models single-owner semantics for the wrapper around
coroutine_handle, making accidental misuse harder. - Fixes the const-correctness mismatch — the qualifier is part of the type, explicit in the contract.
- Supports
noexceptin the signature — expressible where it wasn't before. - Doesn’t require copy semantics in its interface — cleaner contract overall.
If you’re building async infrastructure with C++20 coroutines and C++23 is available, **move_only_function is the natural choice for continuation storage. And if you're not using coroutines yet — it still solves the decade-old "unique_ptr in a callback**" problem.
Compiler support: GCC 12+, Clang 16+, MSVC 19.32+. Requires -std=c++23 or /std:c++latest.
If you’re serious about passing technical interviews, try PracHub. It helped me structure my preparation with advanced mock sessions. [**Check it out here**] and start practicing. (Disclosure: This is an affiliate link).
Found this article helpful ? Clap 👏 and follow me for more C++ and system programming content.
메타데이터
- post_id
- 369e79e5baa0
- slug
- std-move-only-function-cpp23-callable-wrapper-no-copy-369e79e5baa0
- url
- https://towardsdev.com/std-move-only-function-cpp23-callable-wrapper-no-copy-369e79e5baa0
- canonical_url
- https://towardsdev.com/std-move-only-function-cpp23-callable-wrapper-no-copy-369e79e5baa0
- author_url
- https://medium.com/@sagarmadala
- status
- ok
- fetched_at
- 2026-06-23 03:48:11