Última atualização:2026-10-12
Escrevendo C++ FnOnce à mão¶
《C++那些事》Versões em C++ de Fn, FnOnce e FnMut escritas à mão¶
Quem escreve C++ com certeza já usou std::function, mas você já se perguntou: std::function pode ser copiado e chamado repetidamente — e se eu quiser um callback que «só pode ser chamado uma vez»? Para eventos únicos ou uma fila de tarefas move-only, std::function não serve: ele exige que seja copiável, e coisas como unique_ptr não cabem nele.
O design do Rust aqui é lindo: os três traits Fn/FnMut/FnOnce separam com toda a clareza a mutabilidade da closure do número de chamadas. Então a pergunta: dá para escrever à mão uma versão em C++? Daí nasce este post.
1. Objetivo¶
Queremos implementar três templates de classe:
FnOnce<Ret(Args...)>: só pode ser chamado uma vez; após a chamada o estado é consumido (move) e há suporte a capturas move-only;FnMut<Ret(Args...)>: pode ser chamado várias vezes, com captura mutável;Fn<Ret(Args...)>: pode ser chamado várias vezes, com captura imutável, e é copiável.
A relação entre os três também copia o Rust: Fn é um subconjunto de FnMut, e FnMut é um subconjunto de FnOnce. Ou seja, aquilo que Fn consegue fazer, as interfaces de FnMut e FnOnce aceitam.
2. Design¶
O núcleo ainda é o mesmo type erasure: a classe base guarda um invoke virtual e a classe derivada embrulha o objeto chamável de verdade. A diferença está em três detalhes:
(1) o parâmetro de FnOnce::invoke é &&; ao terminar a chamada, a closure embrulhada é movida embora, e chamar de novo depois da primeira vez dispara direto um assert;
(2) FnMut::invoke não é const, permitindo modificar as capturas;
(3) Fn::invoke é const, e além disso a classe derivada deve fornecer clone para suportar a cópia do Fn externo.
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_;
};
Há um ponto que entrevistadores adoram pegar: operator() é restrito a ser chamado apenas em rvalues (o qualificador &&), o que junto com std::move(*impl_) solda a semântica — quer chamar? Primeiro mova-se. É o «callback consumidor».
FnMut e Fn afrouxam as restrições sobre o mesmo 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. Relação de conversão¶
Copiamos as regras do Rust: Fn pode converter-se em FnMut, e FnMut em FnOnce; o contrário não. A implementação dá a FnMut/Fn um construtor a partir do tipo mais estrito, transferindo o impl interno. Assim, uma interface que recebe FnOnce (por exemplo, submissão de tarefa única) aceita qualquer closure, e uma interface que recebe Fn (por exemplo, callback disparado repetidamente) só aceita as imutáveis.
4. Testes¶
#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;
}
Depois de rodar, vale comparar com std::function: colocar uma captura de unique_ptr num std::function nem compila (exige CopyConstructible) — e é exatamente por isso que esta implementação existe.
O código completo já está na comunidade paga (星球); pegue lá se tiver interesse. Para fechar, duas questões para pensar:
(1) por que FnOnce::operator() precisa do qualificador && e o que aconteceria sem ele?
(2) por que o construtor de cópia de Fn precisa passar pelo clone em vez de copiar o unique_ptr diretamente?
Tópicos relacionados¶
- Pequeno projeto de gRPC em C++ | C++那些事
- Comunicação entre processos em C++ na prática | C++那些事
- Demo de serviço de URL curta em C++ | C++那些事
- C++17 e SQLite para medir o tamanho de diretórios | C++那些事
- Testes de performance com Google Benchmark | C++那些事
- Servidor HTTP com corrotinas do C++20 | C++那些事
- Serialização ZeroCopy com Protobuf | C++那些事
- Acelerando Python com pybind11 | C++那些事
- Projeto de calculadora em pilha em C++ | C++那些事