अंतिम अद्यतन:2026-10-12
खुद C++ FnOnce लिखना¶
《C++那些事》C++ संस्करणों Fn, FnOnce, FnMut को खुद लिखना¶
C++ लिखने वालों ने std::function ज़रूर इस्तेमाल किया होगा, लेकिन क्या आपने कभी यह सवाल सोचा है: std::function को कॉपी किया जा सकता है, बार-बार कॉल किया जा सकता है, तो अगर मुझे एक \"केवल एक बार कॉल होने वाला\" callback चाहिए तो? जैसे one-shot events या move-only task queue के लिए std::function ठीक नहीं है—— यह copyable होना माँगता है, unique_ptr जैसी चीज़ें इसमें जगह नहीं पातीं।
Rust का डिज़ाइन इस जगह बहुत सुंदर है: Fn/FnMut/FnOnce तीन trait closure की mutability और call की संख्या को एकदम साफ अलग करते हैं। तो सवाल यह है: क्या C++ में ऐसा सेट खुद लिखा जा सकता है? इसी से यह लेख बना।
1. लक्ष्य¶
हम तीन class template लागू करेंगे:
FnOnce<Ret(Args...)>: केवल एक बार कॉल हो सकता है, कॉल के बाद state consume (move) हो जाता है, move-only capture का समर्थन;FnMut<Ret(Args...)>: कई बार कॉल हो सकता है, capture mutable;Fn<Ret(Args...)>: कई बार कॉल हो सकता है, capture immutable, कॉपी किया जा सकता है।
तीनों का रिश्ता भी Rust से हूबहू लिया गया है: Fn, FnMut का subset है और FnMut, FnOnce का subset। यानी Fn जो काम कर सकता है, उसे FnMut/FnOnce के interface भी स्वीकार कर लेंगे।
2. डिज़ाइन¶
कोर वही type erasure है: base class में एक virtual invoke रहता है, derived class असली callable object को wrap करता है। अंतर तीन बातों में है:
(1) FnOnce::invoke का parameter && है; कॉल खत्म होते ही wrapped closure move हो जाता है, एक बार कॉल के बाद दोबारा कॉल पर सीधे assert fail;
(2) FnMut::invoke const नहीं है, capture में बदलाव की अनुमति;
(3) Fn::invoke const है, और साथ ही derived class को 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_;
};
यहाँ एक बिंदु है जो interviewer आसानी से पकड़ लेते हैं: operator() को केवल rvalue पर कॉल तक सीमित किया गया है (&& qualifier), और std::move(*impl_) के साथ मिलकर semantics पक्के कर दिए गए हैं—— कॉल करना है? पहले खुद को move करो। यही \"consuming callback\" है।
FnMut और Fn उसी 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. रूपांतरण संबंध¶
Rust का नियम हूबहू उठाया गया है: Fn को FnMut में बदला जा सकता है, FnMut को FnOnce में; उल्टी दिशा में नहीं। Implementation में बस FnMut/Fn को strict type से construct होने का interface देते हैं और भीतर का impl सौंप देते हैं। इससे FnOnce लेने वाला interface (जैसे one-shot task submit) हर closure ले लेगा, और Fn लेने वाला interface (जैसे बार-बार trigger होने वाला callback) केवल immutable लेगा।
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 capture को std::function में डालना compile ही नहीं होता (CopyConstructible चाहिए)—— और यही इस implementation के अस्तित्व का कारण है।
पूरा कोड तारांगन (星球, पेड समुदाय) में डाल दिया गया है, रुचि हो तो वहाँ से ले लें। अंत में दो प्रश्न:
(1) FnOnce::operator() पर && qualifier क्यों ज़रूरी है, न हो तो क्या होगा?
(2) Fn का copy constructor unique_ptr सीधे कॉपी करने के बजाय clone से ही क्यों गुज़रना चाहिए?
संबंधित विषय¶
- C++ gRPC छोटा project | C++那些事
- C++ inter-process communication व्यवहार | C++那些事
- C++ short URL service Demo | C++那些事
- C++17 और SQLite से directory का size निकालना | C++那些事
- Google Benchmark performance testing | C++那些事
- C++20 coroutine HTTP server | C++那些事
- Protobuf ZeroCopy serialization | C++那些事
- pybind11 से Python तेज़ करना | C++那些事
- C++ stack calculator design | C++那些事