सामग्री पर जाएं

टेम्पलेट स्पेशलाइज़ेशन और टैग डिस्पैच

फ़ुल और पार्शियल स्पेशलाइज़ेशन में महारत हासिल करें, जानें कि ओवरलोडिंग को कब प्राथमिकता देनी चाहिए, और कंपाइल-टाइम ब्रांचिंग के विकल्प के रूप में टैग डिस्पैच और if constexpr को समझें।

स्पेशलाइज़ क्यों करें?

प्राइमरी टेम्पलेट एक जेनेरिक इम्प्लीमेंटेशन देता है जो ज़्यादातर टाइप के लिए काम करता है। लेकिन कभी-कभी किसी ख़ास टाइप को बुनियादी रूप से अलग ट्रीटमेंट चाहिए होता है। उदाहरण के लिए, std::vector एक स्पेशलाइज़ेशन है जो जगह बचाने के लिए bool को बिट में पैक करता है (हालाँकि इसे व्यापक रूप से स्टैंडर्ड लाइब्रेरी की एक गलती माना जाता है)।

टेम्पलेट स्पेशलाइज़ेशन आपको ख़ास टेम्पलेट आर्गुमेंट के लिए एक वैकल्पिक डेफ़िनिशन देने देता है। इसके दो प्रकार हैं:

1. फ़ुल (एक्सप्लिसिट) स्पेशलाइज़ेशन — आप सभी टेम्पलेट पैरामीटर को ख़ास टाइप पर तय कर देते हैं।
2. पार्शियल स्पेशलाइज़ेशन — आप कुछ पैरामीटर तय करते हैं या उन्हें सीमित करते हैं (जैसे, सभी पॉइंटर टाइप के लिए स्पेशलाइज़ करना)। पार्शियल स्पेशलाइज़ेशन सिर्फ़ क्लास (और वेरिएबल) टेम्पलेट के लिए उपलब्ध है, फ़ंक्शन टेम्पलेट के लिए नहीं।

फ़ुल स्पेशलाइज़ेशन

फ़ुल स्पेशलाइज़ेशन टेम्पलेट आर्गुमेंट के एक ख़ास सेट के लिए पूरी तरह कस्टम इम्प्लीमेंटेशन देता है। इसके सिंटैक्स में template <> (एक खाली पैरामीटर लिस्ट) आता है, जिसके बाद ठोस आर्गुमेंट के साथ क्लास या फ़ंक्शन लिखा जाता है।

full_specialization.cpp
#include <iostream>
#include <cstring>

// Primary template
template <typename T>
class Formatter {
public:
    static std::string format(const T& value) {
        return std::to_string(value);
    }
};

// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
    static std::string format(const char* value) {
        return std::string("\"" ) + value + "\"";
    }
};

// Full specialization for bool
template <>
class Formatter<bool> {
public:
    static std::string format(bool value) {
        return value ? "true" : "false";
    }
};

int main() {
    std::cout << Formatter<int>::format(42)            << '\n'; // 42
    std::cout << Formatter<const char*>::format("hi")  << '\n'; // "hi"
    std::cout << Formatter<bool>::format(true)          << '\n'; // true
}

पार्शियल स्पेशलाइज़ेशन

पार्शियल स्पेशलाइज़ेशन कुछ टेम्पलेट पैरामीटर को सीमित करता है और बाकी को जेनेरिक छोड़ देता है। टाइप के पूरे परिवारों — सभी पॉइंटर, सभी ऐरे, सभी पेयर आदि — से मेल बैठाने के लिए यह बेहद शक्तिशाली है। पार्शियल स्पेशलाइज़ेशन सिर्फ़ क्लास/स्ट्रक्ट/वेरिएबल टेम्पलेट में समर्थित है।

partial_specialization.cpp
#include <iostream>
#include <string>
#include <typeinfo>

// Primary template
template <typename T>
struct TypeInfo {
    static std::string name() { return "general type"; }
};

// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
    static std::string name() {
        return "pointer to " + TypeInfo<T>::name();
    }
};

// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
    static std::string name() {
        return "reference to " + TypeInfo<T>::name();
    }
};

// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
    static std::string name() {
        return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
    }
};

int main() {
    std::cout << TypeInfo<int>::name()     << '\n'; // general type
    std::cout << TypeInfo<int*>::name()    << '\n'; // pointer to general type
    std::cout << TypeInfo<int**>::name()   << '\n'; // pointer to pointer to general type
    std::cout << TypeInfo<int[5]>::name()  << '\n'; // array of 5 general type
}
सामान्य गलती

फ़ंक्शन टेम्पलेट को पार्शियली स्पेशलाइज़ नहीं किया जा सकता — सिर्फ़ फ़ुली स्पेशलाइज़ किया जा सकता है। इससे भी बुरी बात यह है कि फ़ंक्शन टेम्पलेट स्पेशलाइज़ेशन ओवरलोड रेज़ोल्यूशन में उस तरह हिस्सा नहीं लेते जैसी आप उम्मीद करते हैं। कंपाइलर पहले ओवरलोड रेज़ोल्यूशन से सबसे अच्छा बेस टेम्पलेट चुनता है, और उसके बाद ही यह जाँचता है कि उस बेस टेम्पलेट का कोई मेल खाता स्पेशलाइज़ेशन है या नहीं।

इससे चौंकाने वाला व्यवहार होता है:

template <typename T> void f(T)   { /* #1 - प्राइमरी */ }
template <typename T> void f(T*)  { /* #2 - पॉइंटर के लिए ओवरलोड */ }
template <> void f<int*>(int*)     { /* #3 - #1 का स्पेशलाइज़ेशन */ }

f((int*)nullptr) कॉल करने पर #2 चुना जाता है (बेहतर ओवरलोड मैच), #3 नहीं (जो #1 को स्पेशलाइज़ करता है, #2 को नहीं)। समाधान: फ़ंक्शन टेम्पलेट स्पेशलाइज़ेशन की जगह सामान्य फ़ंक्शन ओवरलोडिंग इस्तेमाल करें। अगर आपको कंपाइल-टाइम टाइप ब्रांचिंग चाहिए, तो if constexpr या कॉन्सेप्ट इस्तेमाल करें।

टैग डिस्पैच पैटर्न

टैग डिस्पैच कंपाइल टाइम पर ओवरलोड चुनने के लिए खाली स्ट्रक्ट टाइप ("टैग") इस्तेमाल करता है। स्टैंडर्ड लाइब्रेरी इसका भरपूर उपयोग करती है — std::advance इटरेटर कैटेगरी टैग पर डिस्पैच करता है। if constexpr से पहले कंपाइल-टाइम ब्रांचिंग की यही प्रमुख तकनीक थी।

tag_dispatch.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

namespace detail {
    // Tags are empty structs used purely for overload selection
    template <typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1) — random access
        std::cout << "random access advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
        // O(n) — step one at a time, forward or backward
        while (n > 0) { ++it; --n; }
        while (n < 0) { --it; ++n; }
        std::cout << "bidirectional advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        while (n > 0) { ++it; --n; }
        std::cout << "input iterator advance\n";
    }
}

template <typename Iter>
void my_advance(Iter& it, int n) {
    // iterator_traits extracts the category tag
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    auto vit = v.begin();
    my_advance(vit, 3);  // random access advance

    std::list<int> l{1, 2, 3, 4, 5};
    auto lit = l.begin();
    my_advance(lit, 3);  // bidirectional advance
}

if constexpr: आधुनिक विकल्प

C++17 का if constexpr एक ही फ़ंक्शन के भीतर कंपाइल टाइम पर ब्रांच करने का ज़्यादा साफ़-सुथरा तरीका देता है। छोड़ी गई ब्रांच इंस्टैंशिएट नहीं होती, इसलिए दिए गए टाइप के लिए उसका कंपाइल होना ज़रूरी नहीं है। यह सरल मामलों में टैग डिस्पैच और SFINAE की काफ़ी हद तक जगह ले लेता है।

if_constexpr.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

template <typename T>
std::string stringify(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + value + '"';
    } else if constexpr (std::is_same_v<T, const char*>) {
        return std::string("\"" ) + value + "\"";
    } else {
        return "[non-printable]";
    }
}

int main() {
    std::cout << stringify(42)              << '\n'; // 42
    std::cout << stringify(3.14)            << '\n'; // 3.140000
    std::cout << stringify(std::string("hello")) << '\n'; // "hello"
    std::cout << stringify("world")         << '\n'; // "world"
}
सर्वोत्तम अभ्यास

कंपाइल-टाइम ब्रांचिंग के लिए इस निर्णय-गाइड का उपयोग करें:

- if constexpr (C++17) — सबसे सरल, एक ही फ़ंक्शन के भीतर काम करता है, किसी ख़ास पैटर्न की ज़रूरत नहीं। सीधे-सादे टाइप-आधारित लॉजिक के लिए इसे प्राथमिकता दें।
- कॉन्सेप्ट/requires (C++20) — फ़ंक्शन ओवरलोड या पूरे टेम्पलेट को सीमित करने के लिए सबसे अच्छा। यह खुद-दस्तावेज़ीकृत है और साफ़ एरर संदेश देता है।
- टैग डिस्पैच — तब भी उपयोगी है जब आपके पास पहले से कोई टैग हायरार्की हो (जैसे इटरेटर कैटेगरी)। C++17 से पहले के कोडबेस इस पर बहुत निर्भर हैं।
- SFINAE / enable_if — सबसे पुरानी तकनीक। पढ़ने में कठिन, एरर रहस्यमय। अगर कॉन्सेप्ट या if constexpr काम कर जाएँ, तो नए कोड में इससे बचें।
- फ़ुल स्पेशलाइज़ेशन — क्लास टेम्पलेट के लिए तब इस्तेमाल करें जब किसी एक ख़ास टाइप के लिए आपको पूरी तरह अलग इम्प्लीमेंटेशन चाहिए।
- फ़ंक्शन टेम्पलेट स्पेशलाइज़ेशन का इस्तेमाल कभी न करें — उसकी जगह ओवरलोडिंग इस्तेमाल करें।

मुख्य बातें
  • फ़ुल स्पेशलाइज़ेशन (template <>) किसी एक ख़ास टाइप के लिए कस्टम इम्प्लीमेंटेशन देता है
  • पार्शियल स्पेशलाइज़ेशन कुछ पैरामीटर को सीमित करता है — यह सिर्फ़ क्लास (और वेरिएबल) टेम्पलेट के लिए उपलब्ध है, फ़ंक्शन टेम्पलेट के लिए नहीं
  • फ़ंक्शन टेम्पलेट स्पेशलाइज़ेशन में गलतियाँ होने की संभावना ज़्यादा है — ओवरलोडिंग या if constexpr को प्राथमिकता दें
  • टैग डिस्पैच iterator_traits के ज़रिए कंपाइल टाइम पर ओवरलोड चुनने के लिए खाली स्ट्रक्ट टाइप इस्तेमाल करता है
  • if constexpr (C++17) आधुनिक, ज़्यादा साफ़-सुथरा विकल्प है जो कंपाइल टाइम पर न चुनी गई ब्रांच को हटा देता है

क्विज़ — अपना ज्ञान परखें

(20 XP)

1. फ़ंक्शन टेम्पलेट को स्पेशलाइज़ करने से क्यों बचना चाहिए?

2. किस तरह के टेम्पलेट पार्शियल स्पेशलाइज़ेशन को सपोर्ट करते हैं?

3. `if constexpr` सामान्य `if` से अलग क्या करता है?