5.2.17 · D4 · HinglishC++ Programming

ExercisesSFINAE — substitution failure is not an error

2,586 words12 min read↑ Read in English

5.2.17 · D4 · Coding › C++ Programming › SFINAE — substitution failure is not an error

Vocabulary ka ek chhota sa reminder, taaki koi bhi symbol anjana na lage:

Related tools jinhe aap use kar sakte ho: 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).


Level 1 — Recognition

L1.1 — Signature hai ya body?

Har snippet ke liye batao ki failure SFINAE hai (silently candidate remove karta hai) ya HARD error hai (compilation rok deta hai).

// (a)
template <typename T>
typename T::value_type a(const T&);      // called with T = int
 
// (b)
template <typename T>
void b(const T&) { typename T::value_type x; }   // called with T = int
Recall Solution
  • (a) Illegal cheez int::value_type return type mein hai = signature. → SFINAE. Candidate a silently drop ho jaata hai. Agar yahi ek candidate tha to aapko "no matching function" milega.
  • (b) Signature void b(const T&) T = int ke liye bilkul valid hai. Illegal int::value_type body ke andar hai. Body immediate context nahi hai → HARD error. Ek-line rule: toota hua header = résumé phenk do; toota hua andar = interview crash ho jaata hai.

L1.2 — ::type kya hai?

Inki value (ya non-existence) batao: std::enable_if<true, double>::type aur std::enable_if<false, double>::type.

Recall Solution
  • enable_if<true, double>::type = double (true partial specialization using type = T; define karti hai).
  • enable_if<false, double>::type = exist nahi karta. Primary template mein koi ::type nahi hai, isliye ise name karna substitution failure hai → overload drop ho jaata hai.

Level 2 — Application

L2.1 — Output predict karo

#include <cstdio>
template <typename T>
auto f(const T& t) -> decltype(t.size(), int{}) { return 1; }  // A
template <typename T>
int f(...) { return 2; }                                       // B
 
int main() {
    std::printf("%d %d\n", f(std::string("hi")), f(42));
}

Yeh kya print karta hai, aur har number kyun?

Recall Solution

Print karta hai 1 2.

  • f(std::string("hi")): std::string mein .size() hai, isliye decltype(t.size(), int{}) valid hai → A survive karta hai aur B ke variadic ... se better match hai → A jeet jaata hai → 1.
  • f(42): int mein koi .size() nahi, isliye A ka return type substitution failure hai → A drop ho jaata hai → sirf B bacha (variadic, worst match, hamesha valid) → B jeet jaata hai → 2. decltype ke andar comma operator: t.size() sirf validity ke liye evaluate hoti hai, phir discard ho jaati hai; poora decltype int{}'s type, yaani int, yield karta hai.

L2.2 — Overlap fix karo

template <typename T>
std::enable_if_t<std::is_integral_v<T>, void> g(T);           // A
template <typename T>
std::enable_if_t<sizeof(T) >= 1, void> g(T);                  // B

g(5) call karo. Kya galat hota hai aur kaise fix karoge?

Recall Solution

T = int ke liye: condition A true hai (int integral hai) aur condition B sizeof(int) >= 1 bhi true hai. Dono overloads survive karte hain → ambiguous call → hard error, "compiler ek choose karta hai" nahi. Fix: conditions ko ek clean partition banao. B ki condition ko A ke negation se replace karo:

template <typename T>
std::enable_if_t<!std::is_integral_v<T>, void> g(T);   // B fixed

Ab har T ke liye A / B mein se exactly ek viable hoga.


Level 3 — Analysis

L3.1 — Return-type enable_if twins par kyun break hota hai?

template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void h(T);
template <typename T, typename = std::enable_if_t<!std::is_integral_v<T>>>
void h(T);

Yeh dono alag lagte hain (opposite conditions). Lekin compiler inhe "redefinition / cannot be overloaded" ke saath reject kar sakta hai. Explain karo kyun, aur safe idiom do.

Recall Solution

Kyun break hota hai: ek default template argument overloads distinguish karne ke purpose ke liye function ki signature ka hissa nahi hota. Dono templates same signature template<class T, class> void h(T) pe reduce ho jaate hain. Identical signatures wale do templates = same template ki redeclaration → error, SFINAE run hone se pehle. Safe idiom — enable_if ko ek non-type template parameter mein daalo jo substitution mein participate kare:

template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void h(T);
template <typename T, std::enable_if_t<!std::is_integral_v<T>, int> = 0>
void h(T);

Yahan std::enable_if_t<cond,int> ek defaulted non-type parameter ka type hai. Jab cond false hoti hai, woh type exist nahi karti → SFINAE cleanly overload drop kar deta hai, aur dono alag rehte hain.

L3.2 — Drop trace karo

Parent note se getSize ke liye:

template <typename T>
auto getSize(const T& t) -> decltype(t.size(), size_t{}) { return t.size(); }  // A
template <typename T> size_t getSize(...) { return 0; }                        // B

getSize(3.14) (ek double) ke liye exact substitution steps list karo.

Recall Solution
  1. Deduce T = double const T& t se double argument match karke.
  2. A ke return type mein Substitute karo: decltype( (3.14).size(), size_t{} ). Sub-expression t.size() ko double::size() chahiye — double mein koi member size nahi → substitution failure in the immediate context (return type).
  3. SFINAE: A silently overload set se remove ho jaata hai.
  4. B mein parameter ... hai (hamesha viable, worst rank) aur koi constraint nahi → B akela survivor hai → choose ho jaata hai.
  5. getSize(3.14) 0 return karta hai. Koi hard error nahi, "double has no size" ka koi message nahi.

Level 4 — Synthesis

L4.1 — void_t se has_begin detector banao

void_t Detection Idiom use karke ek trait has_begin<T> likho jo true ho jab T mein member begin() ho aur false otherwise. Phir har moving part explain karo.

Recall Solution
#include <type_traits>
// Primary: assume NO begin. Second param defaulted to void.
template <typename T, typename = void>
struct has_begin : std::false_type {};
 
// Specialization: only viable when decltype(...) is a valid type -> void.
template <typename T>
struct has_begin<T, std::void_t<decltype(std::declval<T&>().begin())>>
    : std::true_type {};

Moving parts:

  • std::void_t<...> kisi bhi valid type list ko void mein map karta hai. Iska poora kaam ek "kya yeh expression compile hua?" switch hona hai.
  • decltype(std::declval<T&>().begin()): declval<T&>() ek T& value bina construct kiye conjure karta hai (hume sirf unevaluated decltype mein iska type chahiye). Agar T mein begin nahi hai, to yeh substitution failure hai — lekin yeh specialization ke template argument mein hai, isliye SFINAE specialization ko sirf non-viable bana deta hai.
  • Jab .begin() valid hai → doosra argument void hai, jo primary ke default = void se match karta hai, isliye specialization (more specialized) choose hoti hai → true_type inherit karti hai.
  • Jab .begin() invalid hai → specialization drop out ho jaati hai → primary (false_type) use hoti hai. Usage: has_begin<std::vector<int>>::value true hai; has_begin<int>::value false hai.

L4.2 — Same idea ek Concept (C++20) se

L4.1 detector ke intent ko ek concept use karke rewrite karo aur ek function constrain karo.

Recall Solution
#include <concepts>
template <typename T>
concept HasBegin = requires (T& t) { t.begin(); };   // does t.begin() compile?
 
template <HasBegin T> void use(T& t) { /* iterate */ }
template <typename T> void use(T&)   { /* fallback */ }

requires(T& t){ t.begin(); } block exactly wahi SFINAE test hai jo readable bana diya gaya hai: yeh true hai iff t.begin() ek well-formed expression hai. Concepts wahi removal from the overload set karte hain jo SFINAE karta hai — lekin constraint named hai aur error messages cryptic no-match ki jagah HasBegin point karte hain.


Level 5 — Mastery

L5.1 — Clean partition ke saath three-way dispatch

describe(T) ke teen overloads likho:

  • integral types → 1 return karta hai
  • floating-point types → 2 return karta hai
  • baaki sab → 3 return karta hai

enable_if use karke, kisi bhi T ke liye koi ambiguity guarantee nahi. Phir describe(7), describe(2.0), describe("hi") predict karo.

Recall Solution
#include <type_traits>
template <typename T,
  std::enable_if_t<std::is_integral_v<T>, int> = 0>
int describe(T) { return 1; }
 
template <typename T,
  std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
int describe(T) { return 2; }
 
template <typename T,
  std::enable_if_t<!std::is_integral_v<T> && !std::is_floating_point_v<T>, int> = 0>
int describe(T) { return 3; }

Teeno conditions ek partition hain: integral, floating, aur "na integral na floating." Kisi bhi T ke liye exactly ek true hoga, isliye exactly ek overload survive karega — koi ambiguity nahi.

  • describe(7)7 int hai (integral) → 1.
  • describe(2.0)2.0 double hai (floating) → 2.
  • describe("hi") → literal "hi" const char[3] hai, na integral na floating → 3. (Neeche partition diagram dekho — har type exactly ek region mein aata hai.)
Figure — SFINAE — substitution failure is not an error

L5.2 — Crash debug karo

Ek student ek "detector" likhta hai jo hamesha hard-crash karta hai:

template <typename T>
struct is_addable {
    static constexpr bool value = requires { std::declval<T>() + std::declval<T>(); };
    T probe = std::declval<T>() + std::declval<T>();   // <-- line X
};

Exact line identify karo jo T = int ke liye bhi hard error banati hai, aur detector fix karo.

Recall Solution

Line X bug hai: T probe = std::declval<T>() + std::declval<T>(); ek evaluated member initializer struct body ke andar hai. Do problems:

  1. std::declval<T>() sirf unevaluated contexts (decltype, sizeof, requires, noexcept) mein appear ho sakta hai — ise real initializer mein use karna har T ke liye ill-formed hai, int ke liye bhi.
  2. Yah compile bhi ho jaata, tab bhi initializer body mein hai, isliye non-addable T hard error deta, clean false nahi. Fix — probe ko unevaluated rakho, member hataao:
template <typename T>
struct is_addable {
    static constexpr bool value =
        requires (T a, T b) { a + b; };   // pure compile-time test, no body probe
};

Ab is_addable<int>::value == true, is_addable<std::mutex>::value == false, aur dono taraf koi hard error nahi.


Recall Self-quiz ke liye one-line summaries

SFINAE ke liye failure kahan rehni chahiye? ::: Signature mein (immediate context) — kabhi body mein nahi. Do equally-viable SFINAE overloads → ? ::: Ambiguous call = hard error. Twin overloads ko distinct kaise banate hain? ::: std::enable_if_t<cond,int> = 0 as a non-type param, default ke roop mein nahi. std::void_t<decltype(expr)> kya test karta hai? ::: Kya expr ek well-formed type/expression hai. std::declval<T>() kahan appear ho sakta hai? ::: Sirf unevaluated contexts mein (decltype/sizeof/noexcept/requires).


Connections

  • Parent: SFINAE
  • 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)