Última actualización:2026-10-12
Implementando C++ FnOnce a mano¶
《C++那些事》Versiones en C++ de Fn, FnOnce y FnMut escritas a mano¶
Quien escribe C++ seguro ha usado std::function, pero ¿te has preguntado algo: std::function se puede copiar y llamar repetidamente, ¿y si quiero un callback que «solo pueda llamarse una vez»? Para eventos de una sola vez o una cola de tareas move-only, std::function no encaja: exige que sea copiable, y cosas como unique_ptr no caben en él.
El diseño de Rust aquí es precioso: los tres traits Fn/FnMut/FnOnce distinguen con toda claridad la mutabilidad de la clausura del número de invocaciones. Entonces la pregunta es: ¿podemos escribir a mano una versión en C++? De ahí nace este artículo.
1. Objetivo¶
Queremos implementar tres plantillas de clase:
FnOnce<Ret(Args...)>: solo puede invocarse una vez; tras la llamada el estado se consume (move) y admite capturas move-only;FnMut<Ret(Args...)>: puede invocarse varias veces, con captura mutable;Fn<Ret(Args...)>: puede invocarse varias veces, con captura inmutable, y es copiable.
La relación entre los tres también copia a Rust: Fn es un subconjunto de FnMut, y FnMut es un subconjunto de FnOnce. Es decir, aquello que Fn puede hacer, las interfaces de FnMut y FnOnce pueden aceptarlo.
2. Diseño¶
El núcleo sigue siendo el mismo type erasure: la clase base guarda un invoke virtual y la clase derivada envuelve el objeto invocable real. La diferencia está en tres detalles:
(1) el parámetro de FnOnce::invoke es &&; al terminar la llamada se mueve la clausura envuelta, y volver a llamar después de la primera vez dispara directamente un assert;
(2) FnMut::invoke no es const, lo que permite modificar las capturas;
(3) Fn::invoke es const, y además la clase derivada debe ofrecer clone para soportar la copia del Fn exterior.
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_;
};
Hay un detalle que a los entrevistadores les encanta atrapar: operator() está restringido a invocarse solo sobre rvalues (el calificador &&), lo que junto con std::move(*impl_) suelda la semántica: ¿quieres llamarlo? Primero muévete a ti mismo. Esto es un «callback consumible».
FnMut y Fn relajan las restricciones sobre el mismo esqueleto:
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. Relación de conversión¶
Copiamos las reglas de Rust: Fn puede convertirse en FnMut, y FnMut en FnOnce; al revés no. La implementación consiste en darle a FnMut/Fn un constructor a partir del tipo más estricto, transfiriéndole el impl interno. Así, una interfaz que recibe FnOnce (por ejemplo, el envío de una tarea de una sola vez) acepta cualquier clausura, y una interfaz que recibe Fn (por ejemplo, un callback que se dispara repetidamente) solo acepta las inmutables.
4. Pruebas¶
#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;
}
Después de ejecutarlo, conviene compararlo con std::function: meter una captura de unique_ptr en un std::function ni siquiera compila (exige CopyConstructible), y justo por eso existe esta implementación.
El código completo ya está en la comunidad de pago (星球); consíguelo allí si te interesa. Para terminar, dos preguntas para reflexionar:
(1) ¿Por qué FnOnce::operator() necesita el calificador && y qué pasaría sin él?
(2) ¿Por qué el constructor de copia de Fn debe pasar por clone en lugar de copiar directamente el unique_ptr?
Temas relacionados¶
- Pequeño proyecto gRPC en C++ | C++那些事
- Comunicación entre procesos en C++ en la práctica | C++那些事
- Demo de servicio de URL cortas en C++ | C++那些事
- C++17 y SQLite para medir el tamaño de directorios | C++那些事
- Pruebas de rendimiento con Google Benchmark | C++那些事
- Servidor HTTP con corrutinas de C++20 | C++那些事
- Serialización ZeroCopy con Protobuf | C++那些事
- Acelerar Python con pybind11 | C++那些事
- Diseño de una calculadora de pila en C++ | C++那些事