Последнее обновление:2026-10-12
Пишем C++ FnOnce вручную¶
《C++那些事》Самописные C++-версии Fn, FnOnce и FnMut¶
Любой, кто пишет на C++, пользовался std::function, но задумывались ли вы вот о чём: std::function можно копировать и вызывать сколько угодно раз — а что если мне нужен колбэк, который «можно вызвать только один раз»? Для разовых событий или очереди move-only задач std::function подходит плохо: он требует копируемости, и такие вещи, как unique_ptr, в него просто не положить.
Дизайн Rust здесь великолепен: три трейта Fn/FnMut/FnOnce чётко разводят изменяемость замыкания и количество вызовов. Возникает вопрос: можно ли написать такой же набор на C++ своими руками? Так и появилась эта статья.
1. Цель¶
Мы реализуем три шаблона классов:
FnOnce<Ret(Args...)>: можно вызвать только один раз; после вызова состояние потребляется (move), поддерживается move-only захват;FnMut<Ret(Args...)>: можно вызывать многократно, захват изменяемый;Fn<Ret(Args...)>: можно вызывать многократно, захват неизменяемый, копируемый.
Их отношения тоже списаны с Rust: Fn — подмножество FnMut, а FnMut — подмножество FnOnce. То есть всё, что умеет Fn, примут интерфейсы FnMut и FnOnce.
2. Дизайн¶
Ядро — всё то же стирание типов: базовый класс хранит виртуальный invoke, производный оборачивает настоящий вызываемый объект. Отличия в трёх деталях:
(1) параметр FnOnce::invoke — &&; после вызова обёрнутое замыкание перемещается прочь, а повторный вызов после первого сразу падает на assert;
(2) FnMut::invoke не const — разрешает изменять захват;
(3) Fn::invoke — const, и вдобавок производный класс должен предоставить clone, чтобы поддерживать копирование внешнего 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_;
};
Здесь есть момент, который интервьюеры любят подлавливать: operator() ограничен вызовом только для rvalue (квалификатор &&), и вместе со std::move(*impl_) это намертво фиксирует семантику — хочешь вызвать? Сначала перемести себя. Это и есть «потребляющий колбэк».
FnMut и Fn ослабляют ограничения на том же каркасе:
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. Отношения преобразования¶
Правила списаны с Rust: Fn можно преобразовать в FnMut, а FnMut — в FnOnce; в обратную сторону нельзя. Реализация — дать FnMut/Fn конструктор из более строгого типа и передать ему внутренний impl. Так интерфейс, принимающий FnOnce (скажем, постановка разовой задачи), примет любое замыкание, а интерфейс, принимающий Fn (скажем, многократно срабатывающий колбэк), — только неизменяемые.
4. Тесты¶
#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;
}
После прогона полезно сравнить со std::function: поместить захват unique_ptr в std::function просто не скомпилируется (требуется CopyConstructible) — в этом и состоит смысл существования данной реализации.
Полный код уже выложен в платном сообществе (星球); при желании получите его там. Напоследок два вопроса на подумать:
(1) зачем FnOnce::operator() нужен квалификатор && и что будет без него?
(2) почему конструктор копии Fn обязан идти через clone, а не копировать unique_ptr напрямую?
Похожие темы¶
- Небольшой проект на C++ gRPC | C++那些事
- Межпроцессное взаимодействие в C++ на практике | C++那些事
- Демо сервиса коротких ссылок на C++ | C++那些事
- C++17 и SQLite: подсчёт размера каталога | C++那些事
- Тестирование производительности Google Benchmark | C++那些事
- HTTP-сервер на корутинах C++20 | C++那些事
- Сериализация Protobuf ZeroCopy | C++那些事
- Ускорение Python с помощью pybind11 | C++那些事
- Проектирование стекового калькулятора на C++ | C++那些事