5.2.18 · D3 · Coding › C++ Programming › Concepts (C++20) — constraining templates
Yeh page C++20 concepts ke liye "har ball phenko" wali drill hai. Hum sirf concepts use nahi karenge — hum har woh situation dhundhenge jis mein ek constraint aa sakti hai, aur unhe poori tarah solve karenge, taaki koi bhi case aisa na ho jo tumne pehle kabhi solve nahi kiya ho.
Intuition Is page ke coloured boxes ko kaise padhein
Poore vault mein, coloured callout boxes ka ek fixed matlab hota hai taaki teri aankh jaldi scan kar sake:
[!intuition] — plain words mein mental picture, koi code nahi.
[!definition] — kisi term ka precise matlab jab wo pehli baar aaye.
[!example] — ek fully worked problem (statement → forecast → steps → verify).
[!mistake] — ek tempting galat idea, kyun sahi lagta hai, aur fix.
[!recall]- — ek collapsible self-test (expand karne ke liye click karo).
[!mnemonic] — ek memory hook.
Ye sirf formatting hai — content hi matter karta hai; colour sirf batata hai ki tum kis mode mein ho.
Intuition "Har scenario" ka concepts ke liye kya matlab hai
Ek concept ek compile-time yes/no question about a type hai. Toh "cases" kisi circle ke quadrants nahi hain — ye wo alag-alag tarike hain jisme ek type us question ka jawab deti hai : wo satisfy karti hai, fail karti hai, partially satisfy karti hai (kuch operations hain, kuch nahi), do concepts dono yes kehti hain (subsumption), koi bhi yes nahi kehta (hard error), aur degenerate edges (empty range, void, ek type jo apne operations ke baare mein jhooth bolta hai). Hum inhe pehle enumerate karte hain, phir ek-ek karke solve karte hain.
Har row ko ek "cell" samjho jo topic tumhe de sakta hai. Neeche ka har worked example us cell ke saath tag kiya gaya hai jo wo cover karta hai.
#
Cell class
Concrete trigger
Kya dikhana hai
C1
Satisfied
int vs Numeric
constraint pass hoti hai, body compile hoti hai
C2
Fails, but another overload still matches
std::string between Numeric aur ek fallback
ek candidate drop hota hai, doosra choose hota hai
C3
All fail → hard error
koi candidate survive nahi karta
clean "no matching function" message
C4
Two overloads, one applies
int between integral/floating_point
disjoint constraints, exactly ek wins
C5
Two overloads, both apply → subsumption
int between Integral aur SignedIntegral
zyada-constrained wins
C6
Ambiguous, neither subsumes
do unrelated requires
compiler error: ambiguous call
C7
Compound requirement (type of result)
{ a+b } -> same_as<T>
expression valid aur T return karta hai
C8
Nested / word problem
ek real "printable range"
multi-part concept banao, real container
C9
Degenerate inputs
empty range, void, self-referential type
limiting behaviour, no crash
C10
Exam twist
requires requires, `
Ab hum C1–C10 ko 10 examples mein cover karte hain (ek per cell).
int, Numeric ko satisfy karta hai, aur kya sq compile hoti hai?
template < typename T >
concept Numeric = std ::integral < T > || std ::floating_point < T > ;
template < Numeric T > T sq ( T x ) { return x * x; }
sq ( 6 ); // T = int
Forecast: andaza lagao — kya yeh compile hoga, aur sq(6) kya return karega?
Predicate Numeric<int> evaluate karo.
Yeh step kyun? Ek constraint body se pehle check hoti hai. std::integral<int> true hai, toh || short-circuit karke true ho jaata hai. Hum std::floating_point<int> ko dekhte bhi nahi.
Constraint satisfied ⇒ int admit hoti hai; body x * x compile hoti hai.
Yeh step kyun? Sirf ab compiler x * x ko typecheck karne ki koshish karta hai. int * int int hai, toh sab theek hai.
Result: sq(6) = 6 * 6 = 36, type int.
Verify: int mein operator* hai, toh agar hum concept skip kar dete toh bhi yeh compile hoti — concept ne koi runtime cost add nahi ki, sirf ek guard add ki. Answer = 36 . ✔
Worked example DO overloads ke saath,
handle(std::string{"hi"}) ka kya hoga?
template < Numeric T > const char* handle ( T ){ return "numeric" ; } // (A)
template < typename T > const char* handle ( T ){ return "generic" ; } // (B) unconstrained fallback
handle ( 6 ); // ?
handle ( std ::string{ "hi" }); // ?
Forecast: kya handle(std::string{"hi"}) error dega, ya fallback hoga? Aur handle(6) ke liye kaun wins karega?
handle(6): dono ko filter karo. Numeric<int>=true ⇒ (A) survive karta hai; unconstrained (B) hamesha survive karta hai.
Yeh step kyun? Ek unconstrained template mein koi constraint nahi hoti fail hone ke liye, toh wo hamesha ek candidate hota hai.
Dono survive karte hain ⇒ subsumption unhe rank karta hai. (A) mein constraint Numeric hai, (B) mein koi nahi ; ek constrained candidate unconstrained se zyada-constrained hota hai, toh (A) wins ⇒ handle(6) = "numeric".
Yeh step kyun? Yeh C5 tie-break rule hai jo "kuch constraint vs koi constraint nahi" pe apply hoti hai.
handle(std::string{"hi"}): dono ko filter karo. Numeric<std::string>=false ⇒ (A) silently remove ho jaata hai. (B) phir bhi survive karta hai.
Yeh step kyun? Ek unsatisfied constraint ek candidate drop karta hai — error nahi deta, exactly isliye kyunki doosra candidate call catch karne ke liye bacha hota hai.
Ek survivor ⇒ (B) call hota hai ⇒ handle(std::string{"hi"}) = "generic". Koi error nahi.
Verify: Dropped-(A) case hi true "C2" scenario hai: ek failing concept quietly side mein ho jaata hai aur fallback jawab deta hai. Results: handle(6)="numeric", handle("hi")="generic". ✔
Worked example Ab fallback HATAO —
sq(std::string{"hi"}) kya karta hai?
template < Numeric T > T sq ( T x ) { return x * x; } // sirf yahi sq hai
sq ( std ::string{ "hi" });
Forecast: kya error operator* ka mention karega jo sq ke andar deep hai, ya kuch aur?
Numeric<std::string> check karo. std::integral<std::string>=false; std::floating_point<std::string>=false; false || false = false.
Yeh step kyun? Concept ek pure boolean hai — hum ise body ka koi reference liye bina compute karte hain.
Constraint fail ⇒ yeh sq candidate set se remove ho jaata hai.
Yeh step kyun? Ek unsatisfied constraint candidate ko body mein dive kiye bina delete kar deti hai, operator* dhundhe bina.
Ab ZERO candidates bache hain ⇒ compiler "no matching function for call to sq" report karta hai — return x * x par nahi, call site par.
Yeh step kyun? Yahi C3 ki defining feature hai: error sirf tab aata hai jab har candidate drop ho jaata hai, aur yeh short aur call ke jagah point karta hai.
Verify: Pre-C++20 duck-typed template<typename T> T sq(T x){return x*x;} se compare karo — woh body compile karta aur std::string ke operator* lookup ke andar error ugal deta. Same failing input, radically cleaner message. C2 aur C3 mein fark exactly yeh hai ki koi doosra candidate survive karta hai ya nahi . ✔
5 aur 3.14 ke liye kaun sa describe chalta hai?
template < std :: integral T > void describe ( T ){ /* "int" */ } // (A)
template < std :: floating_point T > void describe ( T ){ /* "real" */ } // (B)
describe ( 5 ); // ?
describe ( 3.14 ); // ?
Forecast: describe(5) ke liye, kaun se overloads constraint check survive karte hain?
describe(5): dono constraints test karo. integral<int> = true ⇒ (A) survive. floating_point<int> = false ⇒ (B) remove.
Yeh step kyun? Har overload apni constraint se independently filter hoti hai overload resolution kuch bhi rank karne se pehle, toh int ka floating_point fail karna cleanly (B) drop kar deta hai bina error ke.
Ek survivor ⇒ (A) call hota hai. Koi ambiguity nahi kyunki (B) kabhi candidate tha hi nahi.
Yeh step kyun? Ek single survivor ke saath rank karne ke liye kuch nahi — resolution immediately end ho jaati hai.
describe(3.14): integral<double> = false ⇒ (A) remove; floating_point<double> = true ⇒ (B) call.
Yeh step kyun? Same filter, opposite outcome — do constraints disjoint hain, toh kisi bhi input ke liye at most ek kabhi admit hoti hai.
Verify: integral aur floating_point arithmetic types ke liye mutually exclusive hain, toh koi input dono trigger nahi kar sakta. Yahan koi cell nahi hai jahan dono survive karein — woh ek alag cell hai (C5), agle mein handle kiya gaya. ✔
int ke liye, dono Integral aur SignedIntegral true hain — kaun sa overload wins karta hai?
template < class T > concept Integral = std ::integral < T > ;
template < class T > concept SignedIntegral = Integral < T > && std ::is_signed_v < T > ;
template < Integral T > const char* which ( T ){ return "integral" ; } // (P)
template < SignedIntegral T > const char* which ( T ){ return "signed-integral" ; } // (Q)
which ( 7 ); // int BOTH integral aur signed hai
Forecast: kya yeh ambiguous fail hoga, ya ek wins karega? Kyun?
Upar ki figure do circles dikhati hai: bada violet circle Integral hai (saare integer types), aur chota magenta circle us ke andar hai SignedIntegral. Navy dot int hai, dono ke andar baitha hai. Orange arrow rule ki taraf point karta hai: inner (stronger) circle wins .
int ke liye dono constraints pass hoti hain: Integral<int>=true aur SignedIntegral<int>=true. Toh (P) aur (Q) dono filtering survive karte hain — navy dot dono circles mein hai.
Yeh step kyun? Ab hum C5 cell mein hain — do survivors compiler ko unhe order karne par majboor karte hain.
Constraints ko subsumption se compare karo. Figure mein magenta circle violet ke andar draw ki gayi hai kyunki SignedIntegral literally Integral<T> && (extra) ke roop mein define hai. Iski requirement set mein Integral ek sub-term ke roop mein contained hai, toh yeh strictly more maangta hai. Hum kehte hain SignedIntegral Integral ko subsumes karta hai.
Yeh step kyun? Subsumption tie-breaker hai: "agar (Q) ki har requirement (P) ki ek requirement imply karti hai aur (Q) zyada maangta hai, toh (Q) zyada specialized hai."
Zyada-constrained wins ⇒ (Q) which(7) "signed-integral" return karta hai.
Yeh step kyun? Orange arrow follow karo: do survivors diye hue, compiler inner/stronger wala rakhta hai.
Verify: Ek unsigned type jaise unsigned u = 7u ke liye: SignedIntegral<unsigned> = false (is_signed fail), toh sirf (P) survive karta hai ⇒ "integral" return. Same code base dono cleanly handle karta hai — koi enable_if gymnastics nahi. ✔
Common mistake Subsumption sirf tab kaam karta hai jab concepts shared terms rakhte hain
SignedIntegral, Integral ko subsume karta hai kyunki yeh literally apne andar Integral<T> naam leta hai. Agar tumne do independent std::is_signed_v && std::integral expansions likhe jo equivalent hone ke baad bhi containment ko compiler nahi dekh sakta aur tumhe C6 (ambiguity) milta hai. Stronger concepts ko weak walo ke bahar banao — Example 6 dekho.
Worked example Do unrelated requirements jo dono pass hoti hain — kaisa error?
template < class T > concept HasPlus = requires (T a){ a + a; };
template < class T > concept HasMinus = requires (T a){ a - a; };
template < HasPlus T > void g ( T ){} // (X)
template < HasMinus T > void g ( T ){} // (Y)
g ( 3 ); // int mein BOTH + aur - hain
Forecast: kya compiler ek pick karega, ya refuse karega?
Dono survive karte hain: HasPlus<int>=true, HasMinus<int>=true.
Yeh step kyun? Phir se do survivors → humein unhe order karna hai.
Subsumption try karo. HasPlus aur HasMinus mein koi common sub-expression nahi hai — na kisi ki requirement set doosre ki contain karti hai. Koi bhi subsume nahi karta.
Yeh step kyun? Subsumption ke liye ek syntactic containment relationship chahiye; disjoint requires bodies mein koi nahi hoti.
Koi ordering nahi ⇒ ambiguous call — compile error.
Yeh step kyun? C2/C3 ke unlike, yeh ek legitimate hard error hai: language decide nahi kar sakti, aur guess karne se mana karti hai.
Verify: Fix yeh hai ki ek ko doosre se build karo, e.g. concept HasBoth = HasPlus<T> && HasMinus<T>; aur (Y) ko HasBoth require karwao. Tab HasBoth, HasPlus ko subsume karta hai, aur C6 collapse ho ke C5 (unambiguous) ban jaata hai. C5 aur C6 mein fark entirely shared structure ke baare mein hai. ✔
Example se pehle, humein ek naya notation piece earn karna hoga: requires block ke andar ->.
-> (compound requirement) syntax
Ek requires-expression ke andar, ek compound requirement ek expression ko braces mein wrap karta hai aur optionally ek arrow ke baad ek type constraint add karta hai:
{ expr } -> Constraint;
Isko ek saath do demands ke roop mein padha jaata hai: (1) expr compile hona chahiye, aur (2) expr jo type produce karta hai wo Constraint satisfy kare. Yahan -> function-return arrow nahi hai aur pointer arrow bhi nahi — yeh ek special requires-only symbol hai jiska matlab hai "…aur result type satisfy kare…". Constraint khud ek concept hona chahiye jo result type ko apne pehle argument ke roop mein leta hai, e.g. std::same_as<T> ya std::convertible_to<int>.
+ sirf tab accept karo jab a + b exactly T ho.
template < typename T >
concept Addable = requires (T a, T b) {
{ a + b } -> std ::same_as < T > ; // (1) a+b compile hota hai (2) uska type exactly T hai
};
Forecast: int, char, std::string mein se kiske liye Addable true hai?
std::string: s + s compile hota hai aur uska type std::string = T hai ⇒ {...}->same_as<T> pass ⇒ Addable<std::string> = true.
Yeh step kyun? Compound requirement ke do kaam hain (upar ki definition ke according): expression compile hona chahiye aur uska type match hona chahiye . Dono hold karte hain.
char: c + c compile hota hai, lekin integer promotion se uska type int hai, char nahi . Toh same_as<char> fail ⇒ Addable<char> = false.
Yeh step kyun? Yahi subtle case hai — expression valid hai, phir bhi result type doosri demand trip karta hai. Compound requirements exactly yahi pakdte hain.
int: i + i int = T hai ⇒ Addable<int> = true.
Verify: std::same_as<T> ko looser std::convertible_to<T> se replace karo, aur char ab pass kar jaata hai (uska int result char mein convert ho jaata hai). -> ke baad type-constraint ka choice hi poora design decision hai. Truth table: int→true, char→false (same_as ke under), std::string→true. ✔
Worked example "Sirf woh cheezein accept karo jinhe main loop kar sakun AUR har element stream kar sakun."
Ek logging function ko std::vector<int> aur std::vector<std::string> accept karna hai lekin std::vector<NoStream> reject karna hai aur plain int bhi reject karna hai (range nahi hai).
struct NoStream {}; // koi operator<< nahi
template < typename R >
concept Printable = requires (R r) {
std :: begin (r); std :: end (r); // (1) iterable
requires requires ( decltype ( * std :: begin (r)) x) { // (2) nested
std ::cout << x; // streamable element
};
};
template < Printable R > void log_all ( const R & r ){ for ( auto& x: r) std ::cout << x; }
Upar ki figure do gates dikhati hai. Gate 1 (violet) poochta hai "kya yeh iterable hai?" aur Gate 2 (magenta) poochta hai "kya har element streamable hai?". Ek type ko dono gates pass karne hain. Green path vector<int> ko dono se sail karte hue TRUE tak trace karta hai; magenta path vector<NoStream> ko Gate 1 pass karte hue lekin Gate 2 fail karte hue FALSE tak trace karta hai.
Forecast: vector<int>, vector<NoStream>, aur bare int ke liye pass/fail andaza lagao.
vector<int>: begin/end exist karte hain ⇒ (1) pass. Element type int hai, aur cout << int compile hota hai ⇒ (2) pass ⇒ Printable = true. Figure mein yahi green path hai.
Yeh step kyun? Range requirement aur element requirement dono call site par check hoti hain, loop kabhi compile hone se pehle.
vector<NoStream>: begin/end exist karte hain ⇒ (1) pass, lekin cout << NoStream{} compile nahi hota ⇒ (2) fail ⇒ Printable = false. Figure mein yahi magenta path hai.
Yeh step kyun? Nested requires bad element ko pehle se pakad leta hai — for-loop ke andar se errors ki deewar ki jagah.
bare int: std::begin(5) ill-formed hai ⇒ (1) already fail ⇒ Printable = false.
Yeh step kyun? (1) fail hone par short-circuit; streamability test hi nahi hoti.
Verify: Truth values — vector<int>: true; vector<string>: true; vector<NoStream>: false; int: false. Exactly wahi container/element split jo hum chahte the, sab call site par diagnose. ✔
Worked example Empty range,
void, aur self-referential probe — kya ye concept crash karte hain?
Forecast: kya ek empty std::vector<int>{} Printable satisfy karta hai? Kya Printable<void> compile bhi hota hai?
Empty range std::vector<int> v{}: Printable<std::vector<int>> type ki property hai, compile time par decide hoti hai — elements ki sankhya irrelevant hai. Toh size chahe kuch bhi ho, yeh true hai.
Yeh step kyun? Concept kabhi loop run nahi karta; sirf poochta hai "kya syntax begin/end/<< type-check karta hai?" Emptiness ek runtime fact hai, concept ko invisible. for loop runtime par simply zero times iterate karta hai.
Printable<void>: requires(void r) — tum void type ka variable declare nahi kar sakte. Requires-expression apne parameter list mein ill-formed hai, jo rules ke mutabiq false yield karta hai (hard error nahi) ⇒ void silently reject hoti hai.
Yeh step kyun? Yahi key degenerate rule hai: ek malformed requirement concept ko false banata hai, blow up nahi karta. Constraint checking gracefully fail karne ke liye design ki gayi hai.
Self-referential probe requires Printable<R>; Printable ke andar — yeh circular hoga. Compiler ek in-progress concept ko not-yet-satisfied maanta hai aur infinite recursion ki jagah ek diagnostic report karta hai.
Yeh step kyun? Concepts design mein non-recursive hain; kisi concept ko apni definition ke andar naam dena woh jagah hai jahan ye zaroor error karte hain, tumhe infinite checks se bachaate hain.
Verify: Printable<std::vector<int>> = true (size-independent); Printable<void> = false (ill-formed probe ke through); ek self-referential concept = ill-formed. Kisi bhi case mein koi runtime crash nahi hota kyunki concepts pure compile-time predicates hain. ✔
Worked example Iska value explain karo aur kyun yeh typo nahi hai.
template < class T >
concept Sizable = requires requires (T a){ a. size (); }; // TRAP line
// aur alag se:
template < class T >
concept SafeNum = std ::integral < T > || requires (T a){ a. count (); };
Forecast: kya requires requires typo hai? SafeNum<int> ke liye, kya a.count() kabhi test hota hai?
requires requires decomposed. Pehla requires ek requires-clause hai (yeh ek constraint introduce karta hai aur ek bool expect karta hai). Doosra requires(T a){...} ek requires-expression hai (yeh bool banata hai yeh test karke ki a.size() compile hota hai). Toh poori line ka matlab hai "T ko constrain karo: woh expression jo a.size() test karta hai well-formed hai ." Yeh do alag keywords do alag kaam kar rahe hain, ek ke baad ek — valid, typo nahi.
Yeh step kyun? Do grammatical roles pehchanna hi poori trick hai; ek baar clause-then-expression dikh jaaye, doubling galat nahi lagti. Better style: ise ek single requires mein collapse karo expression directly naam dekar: concept Sizable = requires(T a){ a.size(); };.
requires requires kabhi genuinely zaroori hai? Kisi doosre requires-expression ke andar, jab tum ek nested requirement chahte ho jo khud ek expression-test ho. Printable (Ex. 8) se example: requires requires(decltype(*std::begin(r)) x){ std::cout << x; }; — outer requires (nested-requirement clause) kehta hai "yeh constraint hold karna chahiye", aur inner requires(...) wo constraint build karta hai cout << x test karke.
Yeh step kyun? Yeh legitimate use dikhata hai: ek nested requires ko introduce karne ke liye ek clause chahiye aur define karne ke liye ek expression, double keyword honestly produce karta hai.
SafeNum<int> aur short-circuit. std::integral<int> = true. Concepts compile time par short-circuit || use karte hain, toh right operand requires(T a){ a.count(); } kabhi instantiate hi nahi hota . int mein koi .count() nahi, lekin yeh irrelevant hai — left side ne already true answer kar diya. Toh SafeNum<int> = true.
Yeh step kyun? Short-circuit ka matlab hai ki unused branch par ek absent operation concept ko break nahi kar sakta.
SafeNum<std::bitset<8>>. std::integral<std::bitset<8>> = false, toh right side ab test hota hai; bitset mein .count() hai ⇒ true. Toh SafeNum<std::bitset<8>> = true.
Yeh step kyun? Confirm karta hai ki right branch actually use hota hai jab left false ho — short-circuit sirf second operand skip karta hai, delete nahi karta.
Verify: SafeNum<int> = true (left branch; right kabhi check nahi); SafeNum<std::bitset<8>> = true (right branch); SafeNum<std::string> = false (na integral-ness, na .count()). Double requires dono forms mein valid grammar hai jaise dikhaya. ✔
Recall Har example kaun sa cell cover karta hai?
Example 1 :::> C1 satisfied
Example 2 :::> C2 ek overload fail, fallback phir bhi match karta hai
Example 3 :::> C3 sab fail → hard error (no matching function)
Example 4 :::> C4 disjoint overloads, ek apply hota hai
Example 5 :::> C5 subsumption, stronger wins
Example 6 :::> C6 ambiguous, koi subsume nahi karta
Example 7 :::> C7 compound requirement / result type
Example 8 :::> C8 nested concept, real container (word problem)
Example 9 :::> C9 degenerate: empty range, void, self-reference
Example 10 :::> C10 exam twist: requires requires + || short-circuit
Mnemonic Constraint ke paanch fate
PASS · DROP · DIE · RANK · TIE — ek type ya toh Pass hoti hai, silently Drop hoti hai (lekin ek fallback pakad leta hai), sab drop ho jaate hain toh call Die jaati hai (no match), do survivors Rank hote hain subsumption se, ya do survivors Tie jaate hain (ambiguous error).
Parent: Concepts (C++20) — constraining templates · Prereqs: Templates — function & class templates , SFINAE and std::enable_if , type_traits — std::integral, std::floating_point , Overload Resolution & Subsumption , constexpr — compile-time evaluation , Ranges library (C++20) , auto and decltype .