5.2.17 · D3 · Coding › C++ Programming › SFINAE — substitution failure is not an error
Intuition Yeh page kya hai
Parent SFINAE note ne tumhe rule bataya tha. Yahan hum machine ko chalate hain har us input par jo usse mil sakti hai: valid substitution, invalid-in-signature, invalid-in-body, enable_if ka on/off switch, ambiguous overlaps, empty/degenerate cases, aur ek exam trap. Har case ek cell hai ek matrix mein taaki tum dekh sako ki kuch bhi chhuta nahi hai.
Kisi bhi example se pehle, ek reminder simple words mein. Ek template ek recipe hai jisme ek blank T hota hai. Substitution = compiler ek real type us blank mein paste karta hai. Signature ek function ka "header" hota hai — uska return type aur parameter types, woh part jo { } se pehle likha hota hai. Body woh { ... } wala part hai. SFINAE sirf header ko dekhta hai.
Har situation jo SFINAE face kar sakta hai, un cells mein se kisi ek mein aati hai:
#
Cell class
Kya diya jaata hai
Expected outcome
1
Valid substitution
aisa type jo signature mein fit ho jaata hai
candidate rehta hai, compile hota hai
2
Failure in signature
aisa type jisme header mein named member missing ho
candidate silently drop ho jaata hai (SFINAE)
3
Failure in body
header theek hai, { } mein missing member use hoti hai
hard error — NOT SFINAE
4
enable_if = true
condition hold karti hai → ::type exist karta hai
overload enabled
5
enable_if = false
condition fail hoti hai → koi ::type nahi
overload disabled (SFINAE)
6
Overlapping conditions
do overloads dono valid hain
ambiguous hard error
7
Degenerate / empty input
empty container par .size()
valid — 0 return karta hai, koi SFINAE involved nahi
8
Limiting: last candidate removed
koi viable overload survive nahi karta
"no matching function" error
9
Word problem
real logging function paths choose karti hai
compile time par sahi path pick hota hai
10
Exam twist
identical templates ke return type mein enable_if
"redefinition" trap
Neeche ke examples har cell ko cover karte hain. Label [Cell N] batata hai konsa cell hai.
Worked example Happy path
template < typename T >
typename T :: value_type first ( const T & c) { return * c. begin (); }
auto x = first ( std ::vector <int> { 10 , 20 , 30 });
Forecast: x ka type kya hai, aur usmein kaunsi value hai? Padhne se pehle guess karo.
Step 1 — T deduce karo.
Argument std::vector<int> hai, const T& se match hota hai, toh T = std::vector<int>.
Yeh step kyun? Substitution ke liye pehle ek concrete type chahiye; deduction woh supply karta hai.
Step 2 — Return type mein substitute karo.
typename T::value_type ban jaata hai std::vector<int>::value_type, jo int hai.
Yeh step kyun? Yahi exact expression hai jo SFINAE watch karta hai — header ka return type.
Step 3 — Validity check karo.
std::vector<int> mein has ek member type value_type hota hai. Koi failure nahi → candidate survive karta hai → compile hota hai.
Yeh step kyun? SFINAE sirf failure par act karta hai; yahan koi nahi hai, toh kuch dramatic nahi hota.
Verify: *c.begin() pehle element 10 ko dereference karta hai. Toh x == 10 aur decltype(x) == int. Sanity check: vector ka front element int hai; units match karte hain. ✅
Worked example Header toota — silently
Same first template. Ab:
first ( 42 ); // 42 is int
Forecast: compile error hoga, ya silent skip? Aur agar error hota hai toh message kya kehta hai?
Step 1 — Deduce karo. T = int.
Kyun? 42 ko const T& se match kiya.
Step 2 — Return type mein substitute karo. typename int::value_type.
Kyun? Dobara header pehle check hota hai.
Step 3 — Yeh invalid hai: int mein koi value_type member nahi hai.
Kyunki yeh failure signature mein hai (return type), SFINAE fire hota hai: yeh candidate drop hota hai, screamed at nahi.
Kyun? Failure immediate context mein hai — woh part jo { se pehle likha hota hai.
Step 4 — Koi doosra first overload exist nahi karta.
Saare candidates gone hone ke baad, call khud fail ho jaati hai.
Verify: Compiler print karta hai "no matching function for call to first(int)" , NOT "int has no member value_type". Yeh difference hi proof hai ki SFINAE chala: usne failure ko "yeh résumé viable nahi hai" treat kiya, na ki "compilation over hai." ✅
Dekho Templates and Type Deduction yeh jaanne ke liye ki deduction substitution se pehle kyun hoti hai.
Worked example Woh trap jo sabko fool karta hai
template < typename T >
void poke ( const T & t ) { // signature: (const T&) -> void — hamesha valid!
t. quack (); // body: assume karta hai T mein .quack() hai
}
poke ( std ::string{ "hi" });
Forecast: kya yeh Example 2 ki tarah silently drop hoga, ya crash karega?
Step 1 — Signature deduce aur substitute karo. T = std::string. Signature hai void poke(const std::string&) — bilkul valid.
Kyun? Sirf header SFINAE region mein hai.
Step 2 — Candidate substitution se SURVIVE karta hai.
Header mein .quack() ka koi mention nahi, toh wahan fail karne ke liye kuch nahi hai.
Yeh kyun matter karta hai: SFINAE apna kaam pehle hi khatam kar chuka — successfully — body dekhne se pehle.
Step 3 — Ab body compile hoti hai. t.quack() — std::string mein koi quack() nahi → hard error .
Kyun? Body immediate context mein nahi hai; wahan failures ordinary errors hain.
Verify: Error message hai "no member named 'quack' in std::string" aur compilation ruk jaati hai. Example 2 ke "no matching function" se compare karo. Alag message class = alag mechanism. Ise SFINAE-friendly banana ke liye tumhe .quack() ko header mein decltype ke zariye lift karna hoga (dekho decltype and Trailing Return Types ). ✅
Worked example Switch ON hai
template < typename T >
std :: enable_if_t < std :: is_integral_v < T >, const char* > label ( T ) {
return "integer" ;
}
Forecast: label(5) ke liye, kya overload exist karta hai? enable_if_t kya resolve hota hai?
Step 1 — Condition evaluate karo. std::is_integral_v<int> true hai.
Kyun? enable_if ka pehla argument on/off boolean hota hai.
Step 2 — ::type lookup karo. std::enable_if_t<true, const char*> define hota hai const char* ke roop mein (true specialization mein using type = T; hota hai).
Kyun? Sirf true specialization mein member type hota hai.
Step 3 — Signature ban jaati hai const char* label(int). Valid → overload enabled.
Kyun? Ek real return type appear hua, toh header well-formed hai.
Verify: label(5) string "integer" return karta hai. Return type const char* hai. Dekho Type Traits (std::is_integral, std::void_t) yeh jaanne ke liye ki is_integral_v kya test karta hai. ✅
Worked example Switch OFF hai
Same label, lekin label(3.14) call karo (ek double).
Forecast: sirf is ek overload ke saath, kya yeh compile hoga?
Step 1 — Condition evaluate karo. std::is_integral_v<double> false hai.
Step 2 — ::type lookup karo. std::enable_if<false, const char*> primary template hai struct enable_if {}; — isme koi member type nahi hai.
Kyun? Aisa member name karna jo exist nahi karta ek substitution failure hai.
Step 3 — SFINAE overload drop kar deta hai. Header form nahi ho saka.
Kyun? Failure return type build karte waqt hua = signature = immediate context.
Step 4 — Koi doosra overload nahi. Call fail ho jaati hai.
Verify: Error hai "no matching function for call to label(double)" . Doubles handle karne ke liye ek doosra overload add karo !std::is_integral_v<T> ke saath — dekho Example 6 aur std::enable_if and Tag Dispatch . ✅
Worked example Do switches ek saath ON hain
template < typename T >
std :: enable_if_t < std :: is_integral_v < T >, void > h ( T ) {} // A
template < typename T >
std :: enable_if_t <sizeof (T) >= 1 , void> h (T) {} // B (almost always true!)
h ( 7 ); // T = int
Forecast: A jeetega, B jeetega, ya kuch aur bura hoga?
Step 1 — int ke liye condition A test karo. is_integral_v<int> = true → A enabled.
Step 2 — int ke liye condition B test karo. sizeof(int) >= 1 = true → B bhi enabled.
Yeh bug kyun hai: do conditions ek clean partition nahi hain — dono int ke liye hold karte hain.
Step 3 — Overload resolution do equally-good candidates dekhta hai. Koi better match nahi hai.
Hard error kyun, pick kyun nahi? Overload resolution mein ties ambiguous calls hain, ek error — compiler kabhi "sirf choose" nahi karta.
Verify: Error hai "call to 'h' is ambiguous" . Fix: B ko mutually exclusive banao, e.g. !std::is_integral_v<T>. Tab exactly ek survive karta hai (dekho Overload Resolution ). ✅
Worked example SFINAE succeed karta hai, runtime value trivial hai
Parent ka getSize use karte hue:
template < typename T >
auto getSize ( const T & t ) -> decltype (t. size (), size_t {}) { return t. size (); }
template < typename T >
size_t getSize (...) { return 0 ; }
getSize ( std ::vector <int> {}); // ek EMPTY vector
Forecast: kya empty vector SFINAE ko confuse karta hai? Kaunsa number aata hai?
Step 1 — Constrained overload mein substitute karo. decltype(t.size(), size_t{}). Expression t.size() ki validity check hoti hai, value nahi. std::vector<int> mein .size() hai, toh yeh emptiness se regardless valid hai.
Kyun? SFINAE poochta hai "kya yeh expression compile hota hai?", kabhi nahi poochta "runtime par kya return karta hai?"
Step 2 — Constrained overload survive karta hai , ... fallback worse hai → constrained wala choose hota hai.
Kyun? Ek empty container mein phir bhi .size() member hota hai.
Step 3 — Run karo. t.size() ek empty vector par 0 return karta hai.
Kyun? Emptiness ek runtime fact hai; yeh kabhi compile-time decision mein enter nahi karta.
Verify: Return value 0, return type size_t. Note: yahan answer 0 real .size() se aata hai, ... fallback se NAHI (jo bhi 0 hota — ek coincidence). Constrained overload jeeta. Compare getSize(7) se, jahan int mein .size() nahi, SFINAE overload A drop kar deta hai, aur 0 fallback se aata hai. ✅
Worked example Sab kuch filter out ho jaata hai
template < typename T >
auto onlyHasBegin ( const T & t ) -> decltype (t. begin (), void ()) {}
onlyHasBegin ( 42 ); // int, koi .begin() nahi
Forecast: koi fallback overload nahi hone par kya hoga?
Step 1 — Substitute karo. decltype(t.begin(), void()) with T = int → 42.begin() invalid hai.
Kyun? begin() probe trailing return type = signature mein rehta hai.
Step 2 — SFINAE sole candidate drop kar deta hai.
Kyun? Immediate context mein failure.
Step 3 — Overload set ab empty hai.
Yeh limiting boundary kyun hai: SFINAE ka kaam hai "ise remove karo," lekin agar woh last wale ko remove kare toh call ke liye kuch bachta nahi.
Verify: Error "no matching function for call to onlyHasBegin(int)" — silent-drop phir bhi hua, bas empty set chhod gaya. Yeh same message hai jaise Example 2 aur 5 mein, confirm karta hai ki woh ek hi mechanism share karte hain. ✅
Worked example Real-world: log-with-count vs log-plain
Tum logIt(x) likh rahe ho. Agar x ek container hai (iska .size() hai), toh "N items" log karo. Warna bas value log karo. Compile time par choose karo, zero runtime if.
template < typename T >
auto logIt ( const T & t ) -> decltype (t. size (), std ::string{}) {
return std :: to_string (t. size ()) + " items" ; // path P
}
template < typename T >
std :: string logIt (...) { return "scalar" ; } // path Q
logIt ( std ::vector <int> { 1 , 2 , 3 , 4 }); // (a)
logIt ( 99 ); // (b)
Forecast: (a) aur (b) kaunsi strings produce karte hain?
Step 1 — Case (a), T = vector<int>. t.size() valid → path P enabled aur ... se prefer kiya jaata hai.
P ko Q se prefer kyun? Ek concrete parameter hamesha ... variadic ko beat karta hai.
Step 2 — P run karo. .size() 4 hai, toh "4" + " items".
Step 3 — Case (b), T = int. 99.size() invalid → SFINAE P drop kar deta hai. Sirf ... path Q bachta hai.
Kyun? Same signature-probe mechanism jaise Example 2 mein.
Step 4 — Q run karo. "scalar" return karta hai.
Verify: (a) → "4 items", (b) → "scalar". Koi runtime branch nahi likha; branch overload set hi hai. Exactly yahi hai jo Concepts (C++20) requires ke saath readable banata hai. ✅
Return type mein enable_if identical templates mein kyun todta hai
Ek student likhta hai:
template < typename T > std :: enable_if_t < std :: is_integral_v < T >, void > k ( T ) {}
template < typename T > std :: enable_if_t < ! std :: is_integral_v < T >, void > k ( T ) {}
Forecast: cleanly compile hota hai, ya complain karta hai?
Step 1 — Dekho ki do templates ko kya alag karta hai. Sirf return-type computation se implied default template arguments differ karte hain. Default template arguments aur return types function template ki signature ka distinctness ke purpose ke liye hissa nahi hote.
Yeh kyun matter karta hai: do function templates jo sirf return type mein differ karte hain unhe same template ki redeclarations treat kiya ja sakta hai.
Step 2 — Safe idiom. enable_if ko ek defaulted non-type template parameter mein move karo, jo template ki identity ka hissa hota hai:
template < typename T , std :: enable_if_t < std :: is_integral_v < T >, int > = 0 > void k ( T ) {}
template < typename T , std :: enable_if_t < ! std :: is_integral_v < T >, int > = 0 > void k ( T ) {}
int = 0 kyun? Jab condition true ho tab humein ek valid non-type parameter type chahiye; int with default 0 idiomatic filler hai. Jab condition false ho, enable_if_t mein koi ::type nahi hota → SFINAE overload drop kar deta hai — exactly yahi chahiye.
Step 3 — Ab do templates apne template parameter lists mein differ karte hain → distinct → koi redefinition nahi.
Verify: Kisi bhi T ke liye, is_integral_v<T> / !is_integral_v<T> mein se exactly ek true hota hai, toh exactly ek k survive karta hai — kabhi ambiguous nahi (contrast Example 6), kabhi redefinition nahi. Yeh canonical exam answer hai. ✅
Recall Kaunse cells kaunsa compiler behaviour produce karte hain?
Silent drop cells 2, 5, 8 mein hota hai (signature failure). Hard error 3 mein (body), 6 mein (ambiguous), 10-naive mein (redefinition). Cells 1, 4, 7, 9 sab compile aur run hote hain. Message ko mechanism se match karo: "no matching function" = SFINAE ne candidates remove kiye; "no member named X" = body error; "ambiguous" = overlapping conditions.
Har failure ke baare mein poochho: "Kya yeh header mein hai ya heart mein?" Header → SFINAE (silent). Heart (body) → hard error (loud).
SFINAE — substitution failure is not an error (index 5.2.17) — parent
Templates and Type Deduction
std::enable_if and Tag Dispatch
decltype and Trailing Return Types
Type Traits (std::is_integral, std::void_t)
void_t Detection Idiom
Overload Resolution
Concepts (C++20)