Last updated:2026-10-12
Hand-Writing C++ FnOnce¶
《C++那些事》Hand-Written C++ Versions of Fn, FnOnce, and FnMut¶
If you write C++, you have surely used std::function, but have you ever thought about this: std::function can be copied and called over and over — so what if I want a callback that "can only be called once"? For one-shot events or a move-only task queue, std::function is a poor fit: it requires copyability, so things like unique_ptr simply cannot be stored in it.
Rust's design here is beautiful: the three traits Fn/FnMut/FnOnce separate closure mutability from call count crystal clear. So the question is: can we hand-write a C++ version? Hence this post.
1. Goal¶
We want to implement three class templates:
FnOnce<Ret(Args...)>: can be called only once; after the call the state is consumed (moved), and move-only captures are supported;FnMut<Ret(Args...)>: can be called multiple times, with mutable captures;Fn<Ret(Args...)>: can be called multiple times, with immutable captures, and is copyable.
The relationship among the three also copies Rust: Fn is a subset of FnMut, and FnMut is a subset of FnOnce. In other words, whatever Fn can do, the interfaces of FnMut and FnOnce can accept.
2. Design¶
The core is still the same old type erasure: the base class holds a virtual invoke, and the derived class wraps the real callable object. The differences lie in three details:
(1) the parameter of FnOnce::invoke is &&; once the call finishes, the wrapped closure is moved away, and calling again after the first call directly trips an assertion;
(2) FnMut::invoke is not const, which allows modifying the captures;
(3) Fn::invoke is const, and at the same time the derived class must provide clone to support copying the outer Fn.
template <class>
class FnOnce;
template <class Ret, class... Args>
class FnOnce<Ret(Args...)> {
public:
FnOnce() = default;
template <class F>
FnOnce(F functor) : impl_(std::make_unique<Model<F>>(std::move(functor))) {}
Ret operator()(Args... args) && {
return std::move(*impl_).invoke(std::forward<Args>(args)...);
}
explicit operator bool() const { return impl_ != nullptr; }
private:
struct Concept {
virtual ~Concept() = default;
virtual Ret invoke(Args&&... args) && = 0;
};
template <class F>
struct Model final : public Concept {
Model(F functor) : functor_(std::move(functor)) {}
Ret invoke(Args&&... args) && override {
return std::move(functor_)(std::forward<Args>(args)...);
}
F functor_;
};
std::unique_ptr<Concept> impl_;
};
There is a point interviewers love to catch here: operator() is restricted to being callable only on rvalues (the && qualifier), which together with std::move(*impl_) welds the semantics shut — want to call it? Move yourself out first. This is a "consuming callback".
FnMut and Fn loosen the restrictions on the same skeleton:
template <class Ret, class... Args>
class FnMut<Ret(Args...)> {
public:
Ret operator()(Args... args) {
return impl_->invoke(std::forward<Args>(args)...); // 非 const,可改捕获
}
// Concept::invoke 非 const,其余同 FnOnce
};
template <class Ret, class... Args>
class Fn<Ret(Args...)> {
public:
Fn(const Fn& other) : impl_(other.impl_->clone()) {} // 拷贝走 clone
Ret operator()(Args... args) const {
return impl_->invoke(std::forward<Args>(args)...); // const
}
// Concept::invoke 为 const,并提供 virtual clone
};
3. Conversion Relations¶
We copy Rust's rules verbatim: Fn can convert to FnMut, and FnMut can convert to FnOnce; the reverse direction is not allowed. The implementation simply gives FnMut/Fn a way to construct from a stricter type, handing the internal impl over. That way, an interface taking FnOnce (say, one-shot task submission) accepts any closure, while an interface taking Fn (say, a repeatedly fired callback) only accepts immutable ones.
4. Tests¶
#include <cassert>
#include <memory>
int main() {
// move-only 捕获:std::function 塞不进去,FnOnce 没问题
auto ptr = std::make_unique<int>(42);
FnOnce<int()> take = [p = std::move(ptr)] { return *p; };
assert(std::move(take)() == 42);
// 只能调用一次:再次调用断言失败
// std::move(take)(); // crash: impl_ == nullptr
// FnMut:计数器累加
int count = 0;
FnMut<int()> mut = [&count] { return ++count; };
mut();
assert(mut() == 2);
// Fn:可拷贝、可重复调用
Fn<int()> fn = [] { return 1; };
auto copy = fn;
assert(fn() == copy());
return 0;
}
After it runs, you can compare it with std::function: putting a unique_ptr capture into a std::function fails to compile outright (it requires CopyConstructible), and that is exactly the reason this implementation exists.
The complete code has been placed in the Planet community; grab it there if you are interested. Finally, two questions to think about:
(1) Why does FnOnce::operator() need the && qualifier, and what would happen without it?
(2) Why must Fn's copy constructor go through clone instead of directly copying the unique_ptr?
Related Topics¶
- A Small C++ gRPC Project | C++那些事
- C++ Inter-Process Communication in Practice | C++那些事
- C++ Short URL Service Demo | C++那些事
- C++17 and SQLite Folder Size Statistics | C++那些事
- Google Benchmark Performance Testing | C++那些事
- C++20 Coroutine HTTP Server | C++那些事
- Protobuf ZeroCopy Serialization | C++那些事
- Accelerating Python with pybind11 | C++那些事
- C++ Stack Calculator Design | C++那些事