टेम्पलेट मेटाप्रोग्रामिंग पैटर्न (Template Metaprogramming Patterns)
एडवांस्ड टेम्पलेट पैटर्न में महारत हासिल करें: स्टैटिक पॉलीमॉर्फ़िज़्म के लिए CRTP, लचीली कंपोज़िशन के लिए पॉलिसी-आधारित डिज़ाइन, रनटाइम जेनेरिक्स के लिए टाइप इरेज़र, और यह मार्गदर्शन कि टेम्पलेट बनाम वर्चुअल डिस्पैच कब इस्तेमाल करें।
बेसिक टेम्पलेट से आगे
टेम्पलेट ऐसे डिज़ाइन पैटर्न संभव बनाते हैं जिनका दूसरी भाषाओं में कोई समकक्ष नहीं है। ये पैटर्न टेम्पलेट की कंपाइल-टाइम प्रकृति का फ़ायदा उठाकर स्टैटिक पॉलीमॉर्फ़िज़्म (वर्चुअल फ़ंक्शन का कोई ओवरहेड नहीं), कंपोज़ होने योग्य व्यवहार (पॉलिसी), और टाइप-सेफ़ इरेज़र (ठोस टाइप को एक समान इंटरफ़ेस के पीछे छिपाना) हासिल करते हैं।
ये शुरुआती लोगों के पैटर्न नहीं हैं — ये लाइब्रेरी लेखकों और फ़्रेमवर्क डिज़ाइनरों के औज़ार हैं। स्टैंडर्ड लाइब्रेरी के आंतरिक कोड, Boost और आधुनिक C++ फ़्रेमवर्क को पढ़ने के लिए इन्हें समझना ज़रूरी है।
CRTP: क्यूरियसली रिकरिंग टेम्पलेट पैटर्न
CRTP में, एक क्लास ऐसे टेम्पलेट से व्युत्पन्न होती है जिसे खुद आर्गुमेंट के रूप में देकर इंस्टैंशिएट किया गया हो: class Derived : public Base। बेस क्लास कंपाइल टाइम पर डिराइव्ड क्लास के मेथड कॉल कर सकती है — वर्चुअल डिस्पैच का कोई ओवरहेड नहीं। इसका उपयोग व्यवहार इंजेक्ट करने, स्टैटिक पॉलीमॉर्फ़िज़्म लागू करने और मिक्सिन (mixin) कार्यक्षमता बनाने के लिए होता है।
#include <iostream>
// CRTP base: provides a counter for each derived class
template <typename Derived>
class Counter {
static inline int count_ = 0;
protected:
Counter() { ++count_; }
~Counter() { --count_; }
Counter(const Counter&) { ++count_; }
public:
static int alive() { return count_; }
};
// Each derived class gets its OWN counter
class Dog : public Counter<Dog> {
public:
std::string name;
Dog(std::string n) : name(std::move(n)) {}
};
class Cat : public Counter<Cat> {
public:
std::string name;
Cat(std::string n) : name(std::move(n)) {}
};
// CRTP for static polymorphism: no virtual dispatch
template <typename Derived>
class Shape {
public:
double area() const {
// Call derived class method — resolved at compile time
return static_cast<const Derived*>(this)->area_impl();
}
};
class Circle : public Shape<Circle> {
double radius_;
public:
Circle(double r) : radius_(r) {}
double area_impl() const { return 3.14159265 * radius_ * radius_; }
};
class Square : public Shape<Square> {
double side_;
public:
Square(double s) : side_(s) {}
double area_impl() const { return side_ * side_; }
};
int main() {
Dog d1("Rex"), d2("Buddy");
Cat c1("Whiskers");
std::cout << "Dogs alive: " << Dog::alive() << '\n'; // 2
std::cout << "Cats alive: " << Cat::alive() << '\n'; // 1
Circle circle(5.0);
Square square(4.0);
std::cout << "Circle area: " << circle.area() << '\n'; // 78.54
std::cout << "Square area: " << square.area() << '\n'; // 16.0
}पॉलिसी-आधारित डिज़ाइन
पॉलिसी-आधारित डिज़ाइन एक क्लास को आपस में बदले जा सकने वाले व्यवहार-घटकों (पॉलिसी) में बाँटता है, जो टेम्पलेट पैरामीटर के रूप में पास किए जाते हैं। हर पॉलिसी व्यवहार का एक पहलू लागू करती है। उपयोगकर्ता अपनी ज़रूरत का सटीक संयोजन खुद बनाते हैं। इसे Andrei Alexandrescu की "Modern C++ Design" ने लोकप्रिय बनाया।
#include <iostream>
#include <mutex>
#include <stdexcept>
// Threading policies
struct SingleThreaded {
struct Lock { Lock(SingleThreaded&) {} }; // no-op
};
struct MultiThreaded {
std::mutex mtx;
struct Lock {
std::lock_guard<std::mutex> guard;
Lock(MultiThreaded& host) : guard(host.mtx) {}
};
};
// Bounds checking policies
struct NoBoundsCheck {
static void check(int /*index*/, int /*size*/) {} // no-op
};
struct StrictBoundsCheck {
static void check(int index, int size) {
if (index < 0 || index >= size)
throw std::out_of_range("Index out of bounds");
}
};
// Policy-based container: users choose their combination
template <typename T,
typename ThreadingPolicy = SingleThreaded,
typename BoundsPolicy = NoBoundsCheck>
class SmartArray : private ThreadingPolicy {
T* data_;
int size_;
public:
SmartArray(int size) : data_(new T[size]{}), size_(size) {}
~SmartArray() { delete[] data_; }
T& operator[](int index) {
typename ThreadingPolicy::Lock lock(*this);
BoundsPolicy::check(index, size_);
return data_[index];
}
int size() const { return size_; }
};
int main() {
// Fast, no overhead: no locking, no bounds checks
SmartArray<int> fast(10);
fast[0] = 42;
// Safe: bounds-checked, single-threaded
SmartArray<int, SingleThreaded, StrictBoundsCheck> safe(10);
safe[0] = 42;
try {
safe[99] = 0; // throws!
} catch (const std::out_of_range& e) {
std::cout << e.what() << '\n';
}
// Thread-safe with bounds checking
SmartArray<int, MultiThreaded, StrictBoundsCheck> robust(10);
robust[5] = 100;
}टाइप इरेज़र (Type Erasure)
टाइप इरेज़र ठोस टाइप को एक समान इंटरफ़ेस के पीछे छिपाता है, और टेम्पलेट के लचीलेपन को वर्चुअल फ़ंक्शन के रनटाइम पॉलीमॉर्फ़िज़्म से जोड़ता है। क्लासिक उदाहरण std::function है, जो कोई भी कॉलेबल रख सकता है। यह पैटर्न तीन घटक इस्तेमाल करता है: एक कॉन्सेप्ट (एब्स्ट्रैक्ट इंटरफ़ेस), एक मॉडल (टेम्पलेट रैपर जो किसी भी टाइप के लिए इंटरफ़ेस लागू करता है), और एक बाहरी हैंडल क्लास।
#include <iostream>
#include <memory>
#include <string>
// Type-erased "Printable" — can hold any type with a print() method
class Printable {
// Concept: abstract interface
struct Concept {
virtual ~Concept() = default;
virtual void print(std::ostream& os) const = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};
// Model: wraps any T that supports operator<<
template <typename T>
struct Model : Concept {
T value;
Model(T v) : value(std::move(v)) {}
void print(std::ostream& os) const override { os << value; }
std::unique_ptr<Concept> clone() const override {
return std::make_unique<Model>(value);
}
};
std::unique_ptr<Concept> ptr_;
public:
// Constructor: accepts any printable type
template <typename T>
Printable(T value) : ptr_(std::make_unique<Model<T>>(std::move(value))) {}
// Copy support
Printable(const Printable& other) : ptr_(other.ptr_->clone()) {}
Printable& operator=(const Printable& other) {
ptr_ = other.ptr_->clone();
return *this;
}
Printable(Printable&&) = default;
Printable& operator=(Printable&&) = default;
friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
p.ptr_->print(os);
return os;
}
};
int main() {
// Heterogeneous collection — no common base class needed!
std::vector<Printable> items;
items.emplace_back(42);
items.emplace_back(3.14);
items.emplace_back(std::string("hello"));
items.emplace_back('X');
for (const auto& item : items) {
std::cout << item << '\n';
}
// Output: 42, 3.14, hello, X
}एक्सप्रेशन टेम्पलेट (संक्षिप्त परिचय)
एक्सप्रेशन टेम्पलेट एक ऐसी तकनीक है जिसमें ऑब्जेक्ट पर अंकगणितीय एक्सप्रेशन, बीच के नतीजे निकालने के बजाय ऑपरेशन का एक कंपाइल-टाइम ट्री बनाते हैं। अंतिम नतीजा एक ही पास में निकलता है, जिससे अस्थायी ऑब्जेक्ट ख़त्म हो जाते हैं।
उदाहरण के लिए, एक सीधी-सादी Vector क्लास के साथ a + b + c दो अस्थायी वेक्टर बनाता है। एक्सप्रेशन टेम्पलेट के साथ, a + b + c एक हल्का एक्सप्रेशन ऑब्जेक्ट Add बनाता है जो एलिमेंट ज़रूरत पड़ने पर निकालता है — किसी अस्थायी ऑब्जेक्ट की ज़रूरत नहीं।
इस तकनीक का इस्तेमाल Eigen और Blaze जैसी हाई-परफ़ॉर्मेंस लीनियर अलजेब्रा लाइब्रेरी करती हैं। एक्सप्रेशन ट्री का मूल्यांकन लेज़ी ढंग से होता है, और कंपाइलर अक्सर नतीजे में बने सिंगल-पास लूप को वेक्टराइज़ कर सकता है।
एक्सप्रेशन टेम्पलेट को सही ढंग से लागू करना जटिल है, और यह मुख्य रूप से लाइब्रेरी लेखकों की चिंता है। अगर आपको मैट्रिक्स/वेक्टर गणित चाहिए, तो एक्सप्रेशन टेम्पलेट खुद लागू करने की बजाय Eigen जैसी लाइब्रेरी इस्तेमाल करें।
टेम्पलेट (स्टैटिक पॉलीमॉर्फ़िज़्म) और वर्चुअल फ़ंक्शन (डायनामिक पॉलीमॉर्फ़िज़्म) में से चुनते समय:
टेम्पलेट तब इस्तेमाल करें जब:
- टाइप का सेट कंपाइल टाइम पर पता हो
- आपको शून्य-ओवरहेड एब्स्ट्रैक्शन चाहिए (कोई vtable नहीं, कोई इनडायरेक्शन नहीं)
- आप चाहते हों कि कंपाइलर टाइप की सीमाओं के पार इनलाइन और ऑप्टिमाइज़ कर सके
- आपको वैल्यू सिमैंटिक्स के साथ काम करना हो (कोई हीप एलोकेशन नहीं)
- परफ़ॉर्मेंस अहम हो (कसे हुए लूप, न्यूमेरिकल कोड)
वर्चुअल फ़ंक्शन तब इस्तेमाल करें जब:
- टाइप का सेट खुला हो और रनटाइम पर तय होता हो (प्लगइन, यूज़र इनपुट)
- आपको हेटेरोजीनियस कंटेनर चाहिए (जैसे, vector)
- बाइनरी साइज़ स्पीड से ज़्यादा मायने रखता हो (टेम्पलेट ज़्यादा कोड जनरेट करते हैं)
- आप शेयर्ड लाइब्रेरी की सीमाओं के पार स्थिर ABI चाहते हों
- आपको मौजूदा कोड को दोबारा कंपाइल किए बिना नए टाइप जोड़ने हों
टाइप इरेज़र तब इस्तेमाल करें जब:
- आप वर्चुअल डिस्पैच जैसा सरल इंटरफ़ेस चाहते हों, लेकिन उपयोगकर्ताओं को अपनी बेस क्लास से इनहेरिट करने पर मजबूर नहीं करना चाहते
- आपको रनटाइम पॉलीमॉर्फ़िज़्म के साथ वैल्यू सिमैंटिक्स चाहिए (std::function, std::any)
टेम्पलेट मेटाप्रोग्रामिंग शक्तिशाली है, लेकिन इसकी असली कीमतें हैं:
1. कंपाइल टाइम का विस्फोट — गहराई से नेस्टेड टेम्पलेट इंस्टैंशिएशन कंपाइल समय को अस्वीकार्य बना सकते हैं। रिकर्सिव TMP ख़ास तौर पर ख़राब है — फ़ोल्ड एक्सप्रेशन और if constexpr को प्राथमिकता दें।
2. एरर संदेश — कॉन्सेप्ट के बिना, टेम्पलेट एरर से समझ में न आने वाले टेक्स्ट के कई पन्ने बनते हैं। टेम्पलेट को हमेशा सीमित (constrain) करें।
3. डिबगिंग — टेम्पलेट-भारी कोड को डिबगर में स्टेप-थ्रू करना कष्टदायक है। टाइप-इरेज़्ड रैपर इनडायरेक्शन जोड़ते हैं जो असली टाइप को छिपा देता है।
4. पठनीयता — CRTP, पॉलिसी-आधारित डिज़ाइन और एक्सप्रेशन टेम्पलेट जूनियर डेवलपर के लिए समझना कठिन हैं। पैटर्न का नाम और उसका उद्देश्य दस्तावेज़ित करें।
5. ज़रूरत से ज़्यादा इंजीनियरिंग — हर समस्या को टेम्पलेट समाधान नहीं चाहिए। एक साधारण वर्चुअल फ़ंक्शन या यहाँ तक कि switch स्टेटमेंट भी ज़्यादा साफ़ और पर्याप्त हो सकता है।
- CRTP,
Baseसे व्युत्पन्न होकर स्टैटिक पॉलीमॉर्फ़िज़्म और प्रति-क्लास काउंटर/मिक्सिन संभव बनाता है - पॉलिसी-आधारित डिज़ाइन व्यवहार को टेम्पलेट पैरामीटर में बाँटता है — उपयोगकर्ता अपनी ज़रूरत का सटीक संयोजन खुद बनाते हैं
- टाइप इरेज़र टेम्पलेट के लचीलेपन को रनटाइम पॉलीमॉर्फ़िज़्म से जोड़ता है (जैसे,
std::function,std::any) - एक्सप्रेशन टेम्पलेट चेन किए गए ऑपरेशन में अस्थायी ऑब्जेक्ट ख़त्म करते हैं — Eigen, Blaze और ऐसी ही लाइब्रेरी इनका उपयोग करती हैं
- ज्ञात टाइप और शून्य ओवरहेड के लिए टेम्पलेट चुनें; खुले टाइप सेट और रनटाइम लचीलेपन के लिए वर्चुअल डिस्पैच
- ज़रूरत से ज़्यादा इंजीनियरिंग से बचें — वही सबसे सरल मैकेनिज़्म इस्तेमाल करें जो आपकी आवश्यकताएँ पूरी करे
क्विज़ — अपना ज्ञान परखें
(20 XP)1. CRTP में, `class Dog : public Counter<Dog>` से क्या हासिल होता है?
2. टाइप इरेज़र कौन-सी समस्या हल करता है?
3. वर्चुअल फ़ंक्शन को टेम्पलेट से ऊपर कब प्राथमिकता देनी चाहिए?