C++20 कॉन्सेप्ट और कंस्ट्रेंट
कॉन्सेप्ट की मदद से टेम्पलेट आर्गुमेंट पर सटीक आवश्यकताएँ तय करें। requires क्लॉज़, एब्रिविएटेड फ़ंक्शन टेम्पलेट और स्टैंडर्ड लाइब्रेरी कॉन्सेप्ट के साथ ज़्यादा साफ़-सुथरा जेनेरिक कोड लिखें।
कॉन्सेप्ट किस समस्या को हल करते हैं
कॉन्सेप्ट से पहले, टेम्पलेट के एरर संदेश रहस्यमय होते थे — जिस टाइप में operator< न हो, उस पर std::sort इस्तेमाल करने से स्टैंडर्ड लाइब्रेरी इम्प्लीमेंटेशन के भीतर से निकले एरर टेक्स्ट के कई पन्ने बन सकते थे। कॉन्सेप्ट इसे इस तरह हल करते हैं कि आप टेम्पलेट पैरामीटर पर आवश्यकताएँ पहले ही बता देते हैं।
कॉन्सेप्ट एक नामित बूलियन प्रेडिकेट है जिसका मूल्यांकन कंपाइल टाइम पर होता है। यह जाँचता है कि कोई टाइप सिंटैक्टिक और सिमैंटिक आवश्यकताओं का एक सेट पूरा करता है या नहीं। जब कोई कॉन्सेप्ट पूरा नहीं होता, तो कंपाइलर एक साफ़, संक्षिप्त एरर संदेश देता है जो अधूरी आवश्यकता की ओर इशारा करता है — टेम्पलेट इंस्टैंशिएशन बैकट्रेस की दीवार नहीं।
कॉन्सेप्ट C++20 में आए, और टेम्पलेट के बाद जेनेरिक प्रोग्रामिंग में सबसे अहम सुधार हैं।
कॉन्सेप्ट परिभाषित करना
कॉन्सेप्ट concept कीवर्ड से परिभाषित होता है, जिसके बाद एक requires एक्सप्रेशन आता है जो बताता है कि टाइप को कौन-से ऑपरेशन सपोर्ट करने होंगे। requires एक्सप्रेशन वैध एक्सप्रेशन, रिटर्न टाइप, नेस्टेड टाइप और बहुत कुछ जाँच सकता है।
#include <concepts>
#include <iostream>
#include <string>
// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a+b must return something convertible to T
{ a == b } -> std::convertible_to<bool>; // must be comparable
requires std::copy_constructible<T>; // nested requirement
};
// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.empty() } -> std::convertible_to<bool>;
};
// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
return a + b + c;
}
template <Sizable Container>
void report_size(const Container& c) {
std::cout << "Size: " << c.size()
<< (c.empty() ? " (empty)" : "") << '\n';
}
int main() {
std::cout << sum_three(1, 2, 3) << '\n'; // 6
std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0
std::string s = "hello";
report_size(s); // Size: 5
std::vector<int> v{1, 2, 3};
report_size(v); // Size: 3
}requires क्लॉज़ और एब्रिविएटेड सिंटैक्स
किसी टेम्पलेट पर कॉन्सेप्ट लागू करने के कई तरीके हैं। requires क्लॉज़ टेम्पलेट पैरामीटर लिस्ट के बाद या फ़ंक्शन सिग्नेचर के बाद आता है। एब्रिविएटेड फ़ंक्शन टेम्पलेट सिंटैक्स पैरामीटर टाइप के रूप में Concept auto इस्तेमाल करता है, जो सबसे संक्षिप्त रूप है।
#include <concepts>
#include <iostream>
// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
while (b != 0) {
T temp = b;
b = a % b;
a = temp;
}
return a;
}
// Method 2: requires clause after template parameters
template <typename T>
requires std::integral<T>
T lcm(T a, T b) {
return (a / gcd(a, b)) * b;
}
// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
return value < 0 ? -value : value;
}
// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
std::cout << "Integral: " << value << '\n';
}
// Concept auto works for return types too (C++20)
std::integral auto compute() {
return 42; // must return an integral type
}
int main() {
std::cout << gcd(12, 8) << '\n'; // 4
std::cout << lcm(12, 8) << '\n'; // 24
std::cout << absolute(-42) << '\n'; // 42
print_integral(100); // Integral: 100
// print_integral(3.14); // ERROR: constraint not satisfied
}कॉन्सेप्ट कंपोज़िशन और स्टैंडर्ड कॉन्सेप्ट
कॉन्सेप्ट && (कन्जंक्शन) और || (डिस्जंक्शन) के साथ स्वाभाविक रूप से कंपोज़ होते हैं। स्टैंडर्ड लाइब्रेरी में कॉन्सेप्ट का समृद्ध सेट देती है:
कोर लैंग्वेज कॉन्सेप्ट:
- std::same_as — T और U एक ही टाइप हैं
- std::derived_from — Derived, Base से व्युत्पन्न है
- std::convertible_to — From इम्प्लिसिटली To में बदला जा सकता है
- std::integral, std::floating_point, std::signed_integral
तुलना कॉन्सेप्ट:
- std::equality_comparable — == और != सपोर्ट करता है
- std::totally_ordered — <, >, <=, >=, ==, != सपोर्ट करता है
ऑब्जेक्ट कॉन्सेप्ट:
- std::movable, std::copyable, std::semiregular, std::regular
कॉलेबल कॉन्सेप्ट:
- std::invocable — F को Args के साथ कॉल किया जा सकता है
- std::predicate — F एक प्रेडिकेट है जो bool लौटाता है
इटरेटर कॉन्सेप्ट ():
- std::input_iterator, std::forward_iterator, std::random_access_iterator, std::contiguous_iterator
रेंज कॉन्सेप्ट ():
- std::ranges::range, std::ranges::sized_range, std::ranges::random_access_range
कॉन्सेप्ट सबसम्प्शन और ओवरलोड रेज़ोल्यूशन
जब कई कंस्ट्रेंड ओवरलोड मैच करते हैं, तो कंपाइलर सबसे ख़ास वाला चुनने के लिए कॉन्सेप्ट सबसम्प्शन इस्तेमाल करता है। कॉन्सेप्ट A, कॉन्सेप्ट B को सबसम करता है अगर A की कंस्ट्रेंट में B की कंस्ट्रेंट तार्किक रूप से निहित हैं। इससे "रिफ़ाइनमेंट" संभव होता है — ज़्यादा ख़ास कॉन्सेप्ट, ज़्यादा सामान्य कॉन्सेप्ट पर भारी पड़ता है।
#include <concepts>
#include <iostream>
// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
{ os << t };
};
template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;
// Less constrained overload
template <Printable T>
void display(const T& value) {
std::cout << "[generic] " << value << '\n';
}
// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
std::cout << "[number] " << value << '\n';
}
int main() {
display(42); // [number] 42 — PrintableNumber is more specific
display(std::string("hi")); // [generic] hi — only Printable matches
display(3.14); // [generic] 3.14 — not integral
}requires requires पैटर्न
कुछ अजीब-सा दिखने वाला requires requires तब आता है जब आप कोई नामित कॉन्सेप्ट परिभाषित किए बिना, एड-हॉक requires एक्सप्रेशन को सीधे requires क्लॉज़ में इस्तेमाल करते हैं। पहला requires कंस्ट्रेंट क्लॉज़ शुरू करता है; दूसरा requires requires एक्सप्रेशन शुरू करता है।
#include <iostream>
#include <string>
// Ad-hoc constraint without naming a concept
template <typename T>
requires requires(T a, T b) {
{ a + b } -> std::same_as<T>;
}
T add(T a, T b) {
return a + b;
}
// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template <AddableToSelf T>
T add_named(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << '\n'; // 3
std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
std::cout << add_named(10, 20) << '\n'; // 30
}कॉन्सेप्ट डिज़ाइन करते समय:
- कॉन्सेप्ट का नाम इस आधार पर रखें कि टाइप क्या है, इस आधार पर नहीं कि फ़ंक्शन को क्या चाहिए — HasLessThan नहीं, Sortable इस्तेमाल करें। कॉन्सेप्ट को सिमैंटिक श्रेणियाँ पकड़नी चाहिए।
- स्टैंडर्ड लाइब्रेरी कॉन्सेप्ट को प्राथमिकता दें — अपना लिखने से पहले देखें कि , या में आपकी ज़रूरत की चीज़ पहले से है या नहीं।
- कॉन्सेप्ट छोटे और कंपोज़ होने लायक रखें — एक बड़ी आवश्यकता बनाने की बजाय छोटे कॉन्सेप्ट को && से जोड़ें।
- एड-हॉक requires requires की जगह नामित कॉन्सेप्ट इस्तेमाल करें — नामित रूप ज़्यादा पठनीय और दोबारा इस्तेमाल करने लायक है।
- सिमैंटिक आवश्यकताओं को कमेंट में दस्तावेज़ित करें — कॉन्सेप्ट सिंटैक्स जाँचते हैं, सिमैंटिक्स नहीं। अगर आपको operator+ का क्रमविनिमेय (commutative) होना ज़रूरी है, तो कमेंट में लिख दें।
- कॉन्सेप्ट नामित बूलियन प्रेडिकेट हैं जो टेम्पलेट पैरामीटर को सीमित करते हैं और साफ़ एरर संदेश देते हैं
requiresएक्सप्रेशन वैध एक्सप्रेशन, रिटर्न टाइप और नेस्टेड आवश्यकताएँ जाँचता है- कॉन्सेप्ट लागू करने के चार तरीके: टेम्पलेट पैरामीटर पर कंस्ट्रेंट, requires क्लॉज़, ट्रेलिंग requires, या
Concept auto - कॉन्सेप्ट
&&और||से कंपोज़ होते हैं; सबसम्प्शन सबसे ज़्यादा कंस्ट्रेंड ओवरलोड चुनता है - स्टैंडर्ड लाइब्रेरी
,औरमें कॉन्सेप्ट देती है requires requiresएक एड-हॉक इनलाइन कंस्ट्रेंट है — पठनीयता के लिए नामित कॉन्सेप्ट को प्राथमिकता दें
क्विज़ — अपना ज्ञान परखें
(20 XP)1. `void print(std::integral auto value)` का क्या मतलब है?
2. अगर कॉन्सेप्ट A को `concept A = B<T> && C<T>` के रूप में परिभाषित किया गया है, तो जब B-कंस्ट्रेंड और A-कंस्ट्रेंड, दोनों ओवरलोड मैच करते हैं तब क्या होता है?
3. `requires requires(T a) { a.size(); }` में 'requires' कीवर्ड दो बार क्यों है?