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

SFINAE, टाइप ट्रेट्स और enable_if

Substitution Failure Is Not An Error सिद्धांत को समझें, कंपाइल-टाइम इंट्रोस्पेक्शन के लिए टाइप ट्रेट्स का उपयोग करें, और enable_if से टेम्पलेट को सीमित करें।

SFINAE क्या है?

SFINAE का मतलब है Substitution Failure Is Not An Error। जब कंपाइलर किसी टेम्पलेट को इंस्टैंशिएट करने की कोशिश करता है, तो वह डिडक्ट किए गए या बताए गए टाइप को टेम्पलेट में सब्स्टिट्यूट करता है। अगर यह सब्स्टिट्यूशन टेम्पलेट डिक्लेरेशन के तात्कालिक संदर्भ (immediate context) में कोई अमान्य टाइप या एक्सप्रेशन बनाता है, तो कंपाइलर एरर नहीं देता — वह बस उस टेम्पलेट को ओवरलोड सेट से हटा देता है और दूसरे कैंडिडेट आज़माने आगे बढ़ जाता है।

यह मैकेनिज़्म C++11/14 में टेम्पलेट मेटाप्रोग्रामिंग की बुनियाद है। इससे आप ऐसे टेम्पलेट लिख सकते हैं जो अपने टाइप आर्गुमेंट के गुणों के आधार पर शर्त के साथ उपलब्ध हों।

मुख्य वाक्यांश "immediate context" है। फ़ंक्शन बॉडी के गहरे अंदर होने वाली सब्स्टिट्यूशन विफलताएँ हार्ड एरर हैं, SFINAE नहीं। सिर्फ़ फ़ंक्शन सिग्नेचर (रिटर्न टाइप, पैरामीटर टाइप, टेम्पलेट पैरामीटर लिस्ट, या requires क्लॉज़) की विफलताएँ ही SFINAE को ट्रिगर करती हैं।

std::enable_if

std::enable_if एक मेंबर टाइप type परिभाषित करता है जो T के बराबर होता है, लेकिन सिर्फ़ तब जब Condition true हो। अगर Condition false है, तो मेंबर type मौजूद ही नहीं होता, जिससे सब्स्टिट्यूशन विफलता (SFINAE) होती है। एलियास std::enable_if_t, typename std::enable_if::type का शॉर्टहैंड है।

enable_if.cpp
#include <iostream>
#include <type_traits>

// Only enabled for integral types
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T>
safe_divide(T a, T b) {
    if (b == 0) return 0;  // safe fallback for integers
    return a / b;
}

// Only enabled for floating-point types
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
safe_divide(T a, T b) {
    if (b == 0.0) return std::numeric_limits<T>::quiet_NaN();
    return a / b;
}

// enable_if in template parameter (alternative placement)
template <typename T,
         std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
void process(T value) {
    std::cout << "Processing arithmetic value: " << value << '\n';
}

int main() {
    std::cout << safe_divide(10, 3)    << '\n'; // 3 (integer)
    std::cout << safe_divide(10.0, 3.0) << '\n'; // 3.33333 (double)
    std::cout << safe_divide(10, 0)    << '\n'; // 0 (safe fallback)
    std::cout << safe_divide(10.0, 0.0) << '\n'; // nan

    process(42);
    // process(std::string("hello")); // ERROR: no matching function
}

<type_traits> लाइब्रेरी

हेडर कंपाइल-टाइम टाइप इंट्रोस्पेक्शन और ट्रांसफ़ॉर्मेशन टूल की एक व्यापक लाइब्रेरी देता है। ये SFINAE-आधारित प्रोग्रामिंग की रीढ़ हैं।

क्वेरी ट्रेट्स (::value या _v सफ़िक्स के ज़रिए bool लौटाते हैं):
- std::is_integral_v — क्या T इंटीजर टाइप है?
- std::is_floating_point_v — क्या T फ़्लोटिंग-पॉइंट टाइप है?
- std::is_pointer_v — क्या T पॉइंटर है?
- std::is_class_v — क्या T क्लास/स्ट्रक्ट है?
- std::is_const_v — क्या T const-क्वालिफ़ाइड है?
- std::is_same_v — क्या T और U एक ही टाइप हैं?
- std::is_base_of_v — क्या Base, Derived का बेस है?
- std::is_convertible_v — क्या From इम्प्लिसिटली To में बदल सकता है?
- std::is_constructible_v — क्या T को Args से कंस्ट्रक्ट किया जा सकता है?

ट्रांसफ़ॉर्मेशन ट्रेट्स (::type या _t सफ़िक्स के ज़रिए बदला हुआ टाइप लौटाते हैं):
- std::remove_reference_t — & या && हटाता है
- std::remove_const_t — const हटाता है
- std::decay_t — ऐरे-से-पॉइंटर, फ़ंक्शन-से-पॉइंटर डिके लागू करता है, और cv/रेफ़रेंस हटाता है
- std::conditional_t — कंपाइल-टाइम टर्नरी: अगर Cond तो T वरना F
- std::common_type_t — वह टाइप जिसमें सभी Ts बदल सकते हैं

std::void_t और डिटेक्शन आइडियम

std::void_t<...> (C++17) टाइप के किसी भी क्रम को void से मैप करता है। इसकी ताक़त SFINAE में है: अगर लिस्ट का कोई भी टाइप अमान्य है, तो स्पेशलाइज़ेशन हटा दिया जाता है। इससे डिटेक्शन आइडियम संभव होता है — यह जाँचना कि किसी टाइप में कोई ख़ास मेंबर, मेथड या नेस्टेड टाइप है या नहीं।

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

// Primary template: fallback, T does NOT have .size()
template <typename T, typename = void>
struct has_size : std::false_type {};

// Specialization: matches only if T has a .size() method
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

template <typename T>
constexpr bool has_size_v = has_size<T>::value;

// Use the trait to branch behavior
template <typename T>
void describe(const T& value) {
    if constexpr (has_size_v<T>) {
        std::cout << "Container with " << value.size() << " elements\n";
    } else {
        std::cout << "Not a container: " << value << '\n';
    }
}

int main() {
    static_assert(has_size_v<std::string>);     // true
    static_assert(has_size_v<std::vector<int>>); // true
    static_assert(!has_size_v<int>);             // false

    describe(std::vector<int>{1, 2, 3}); // Container with 3 elements
    describe(42);                        // Not a container: 42
}

SFINAE के साथ decltype

किसी एक्सप्रेशन के वैध होने के आधार पर SFINAE ट्रिगर करने के लिए आप ट्रेलिंग रिटर्न टाइप में decltype इस्तेमाल कर सकते हैं। इस तकनीक को अक्सर कॉमा ऑपरेटर के साथ जोड़ा जाता है, ताकि एक्सप्रेशन का नतीजा छोड़ दिया जाए और मनचाहा टाइप लौटाया जाए।

decltype_sfinae.cpp
#include <iostream>
#include <type_traits>

// Only callable if a + b is valid
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}

// Only callable if t.begin() and t.end() are valid
template <typename Container>
auto element_count(const Container& c)
    -> decltype(c.begin(), c.end(), std::size_t{}) {
    std::size_t count = 0;
    for (auto it = c.begin(); it != c.end(); ++it) ++count;
    return count;
}

int main() {
    std::cout << add(1, 2.5)   << '\n'; // 3.5
    std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab

    std::vector<int> v{1, 2, 3, 4};
    std::cout << element_count(v) << '\n'; // 4
    // add(std::string("a"), 5); // SFINAE: no match, a+b invalid
}
सामान्य गलती

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

ज़रा सोचिए: आप safe_divide("hello", "world") कॉल करते हैं। कंपाइलर दोनों ओवरलोड आज़माता है, दोनों SFINAE में विफल हो जाते हैं, और आपको ऐसा एरर संदेश मिलता है जिसमें enable_if, is_integral, is_floating_point और टाइप ट्रेट्स के आंतरिक ब्योरे का ज़िक्र है — इनमें से कुछ भी यह समझने में मदद नहीं करता कि असली समस्या "स्ट्रिंग संख्याएँ नहीं हैं" है।

C++20 कॉन्सेप्ट के पीछे यह सबसे मज़बूत कारणों में से एक है, जो साफ़, पढ़ने लायक एरर संदेश देते हैं, जैसे: "the constraint std::integral was not satisfied for T = const char*"।

सर्वोत्तम अभ्यास

अगर आप C++20 कोड लिख रहे हैं, तो लगभग सभी मामलों में SFINAE की जगह कॉन्सेप्ट को प्राथमिकता दें। कॉन्सेप्ट पढ़ने में आसान होते हैं, स्वाभाविक रूप से कंपोज़ होते हैं, और कहीं बेहतर एरर संदेश देते हैं। SFINAE को इनके लिए रखें:

- ऐसा कोड जिसे C++11/14/17 के साथ कंपाइल होना ज़रूरी है
- जटिल डिटेक्शन आइडियम जिनका कोई मानक कॉन्सेप्ट समकक्ष नहीं है
- लाइब्रेरी कोड जिसे C++ मानक के कई वर्शन सपोर्ट करने हैं

C++17 कोड में भी, जब आप लॉजिक को एक ही फ़ंक्शन के भीतर व्यक्त कर सकें, तो enable_if की जगह टाइप ट्रेट्स के साथ if constexpr को प्राथमिकता दें।

मुख्य बातें
  • SFINAE: अगर टेम्पलेट आर्गुमेंट सब्स्टिट्यूट करने से तात्कालिक संदर्भ में अमान्य टाइप बनता है, तो टेम्पलेट को ओवरलोड रेज़ोल्यूशन से चुपचाप हटा दिया जाता है
  • std::enable_if_t क्लासिक SFINAE टूल है — यह सिर्फ़ तब मौजूद होता है जब Condition सही हो
  • कंपाइल-टाइम क्वेरी ट्रेट्स (is_integral_v) और ट्रांसफ़ॉर्मेशन ट्रेट्स (remove_reference_t) देता है
  • std::void_t डिटेक्शन आइडियम संभव बनाता है — यह जाँचना कि किसी टाइप में ख़ास मेंबर या ऑपरेशन हैं या नहीं
  • SFINAE के एरर संदेश कुख्यात रूप से अपठनीय हैं — C++20 कॉन्सेप्ट इसका आधुनिक विकल्प हैं

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

(20 XP)

1. SFINAE का पूरा रूप क्या है और इसका क्या मतलब है?

2. टेम्पलेट स्पेशलाइज़ेशन में `std::void_t<decltype(std::declval<T>().size())>` क्या करता है?

3. `std::decay_t<T>` क्या करता है?