এই পর্বে যা শিখবেন
- শর্তহীন template-এর আসল সমস্যা: অলিখিত চুক্তি আর template-এর গভীর থেকে আসা লম্বা error
- standard library-র তৈরি concept, আর template-এ শর্ত বসানোর চারটি লেখার রীতি
- requires expression দিয়ে নিজের concept লেখা — simple, type, compound আর nested requirement
- শর্ত দিয়ে overload বাছাই আর subsumption; concept বনাম abstract class;
static_assertআরif constexpr-এ concept - semantic requirement — concept কী যাচাই করতে পারে না, ভালো concept-এর নকশা, আর AI-এর লেখা template-কোড যাচাই
১ · সমস্যাটা আগে — অলিখিত চুক্তি আর পঞ্চাশ লাইনের error
পর্ব ২৮-এ largest নামের একটি function template লিখেছিলেন। তার শরীরে ছিল a < b, তাই T-কে < দিয়ে তুলনা করা যেতেই হবে — কিন্তু এই শর্ত কোথাও লেখা ছিল না। শর্তটি লুকিয়ে ছিল কোডের ভেতরে। যে টাইপে < নেই, তা দিলে compiler template-এর ভেতরে ঢুকে, অর্ধেক কাজ করে, তারপর আটকে যায়। আর error দেয় সেই লাইনে, যেটি আপনি লেখেননি।
ছোট template-এ এটি সহনীয়। কিন্তু standard library-র গভীর template-এ একই ভুল ভয়ংকর রূপ নেয়। ধরুন আপনি একটি std::list সাজাতে পুরোনো std::sort ডাকলেন। std::sort চায় random access — যেকোনো ঘরে সরাসরি লাফ দেওয়ার ক্ষমতা — যা linked list দিতে পারে না। g++ যা বলে, তার একটি ছোট করা রূপ:
std::list<int> marks{70, 45, 90};
std::sort(marks.begin(), marks.end());
In file included from /usr/include/c++/14/algorithm:61,
from main.cpp:1:
/usr/include/c++/14/bits/stl_algo.h: In instantiation of
'void std::__sort(_RandomAccessIterator, _RandomAccessIterator, _Compare)
[with _RandomAccessIterator = std::_List_iterator<int>; ...]':
/usr/include/c++/14/bits/stl_algo.h:...: required from 'void std::sort(...)'
main.cpp:6: required from here
/usr/include/c++/14/bits/stl_algo.h:...: error: no match for 'operator-'
(operand types are 'std::_List_iterator<int>' and 'std::_List_iterator<int>')
... | std::__lg(__last - __first) * 2,
note: candidate: 'template<class _IteratorL, class _IteratorR> ... operator-(
const std::reverse_iterator<_IteratorL>&, ...)'
note: template argument deduction/substitution failed:
note: 'std::_List_iterator<int>' is not derived from
'const std::reverse_iterator<_IteratorL>'
note: candidate: 'template<class _Iterator> ... operator-(
const std::move_iterator<_IteratorL>&, ...)'
... (আরও কয়েক ডজন note — প্রতিটি সম্ভাব্য operator- কেন মেলেনি)
বার্তাটি পড়ে দেখুন। কোথাও লেখা নেই “list-কে std::sort দিয়ে সাজানো যায় না”। আছে __lg, __last - __first, reverse_iterator — library-র ভেতরের এমন সব নাম, যা আপনি কখনো লেখেননি। আসল কারণটি আপনাকে নিজেকেই খুঁজে বের করতে হবে: list-এর iterator-এ বিয়োগ চলে না, তাই random access নেই। পুরো বার্তা সহজেই পঞ্চাশ লাইন ছাড়িয়ে যায়। নতুনরা এখানেই template-কে ভয় পেতে শুরু করেন।
একটি চাকরির বিজ্ঞাপনের কথা ভাবুন: “পদ: হিসাবরক্ষক। যোগ্যতা: স্নাতক, Excel-এ দক্ষতা, বাংলা ও ইংরেজিতে লেখার ক্ষমতা।” আবেদনকারী বিজ্ঞাপন পড়েই বোঝেন তিনি যোগ্য কি না। অযোগ্য হলে অফিস গেটেই বলে দেয়: “Excel-এ দক্ষতা নেই।” কল্পনা করুন বিজ্ঞাপনে কোনো যোগ্যতা লেখা নেই — সবাইকে ভেতরে ডেকে, এক সপ্তাহ কাজ করিয়ে, তারপর কারও হাতে ধরিয়ে দেওয়া হলো একটি লম্বা চিঠি: “তৃতীয় দিনের দুপুরে VLOOKUP সূত্রে আপনার ভুল হয়েছে, কারণ…” শর্তহীন template ঠিক এই দ্বিতীয় অফিস। concept হলো বিজ্ঞাপনের যোগ্যতার তালিকা — দরজায় যাচাই, পরিষ্কার কারণ।
C++20 এই তালিকা লেখার ব্যবস্থা দিয়েছে: concept. একই কাজ নতুন std::ranges::sort দিয়ে করলে, যার শর্ত concept দিয়ে লেখা, error শুরু হয় ডাকার লাইনেই, আর বলে দেয় কোন শর্ত মেলেনি:
std::ranges::sort(marks);
main.cpp:6: error: no match for call to '(const std::ranges::__sort_fn) (std::list<int>&)'
note: candidate: ... requires random_access_range<_Range> && sortable<...>
note: constraints not satisfied
note: the required expression 'std::ranges::begin(...) + n' is invalid ...
required for the satisfaction of 'random_access_iterator<...>'
(সংক্ষেপে)
এখনো নিখুঁত নয়, কিন্তু চাবি-শব্দটি এখন চোখের সামনে: random_access_range — শর্তটি মেলেনি। list-এর জন্য সঠিক পথ হলো তার নিজের marks.sort(), পর্ব ৩০-এ যেমন দেখেছেন।
concept হলো একটি টাইপের ওপর নাম দেওয়া শর্ত, যা compile-এর সময় সত্য বা মিথ্যা হয়। std::integral<int> সত্য, std::integral<double> মিথ্যা। template-এর মাথায় এমন শর্ত বসালে তিনটি লাভ: পাঠক ঘোষণা দেখেই বোঝেন কোন টাইপ চলবে; ভুল টাইপ দিলে error আসে ডাকার জায়গায়, ছোট হয়ে; আর একই নামের কয়েকটি template-এর মধ্যে শর্ত দেখে বাছাই করা যায়। চলার সময় concept-এর কোনো অস্তিত্ব নেই, কোনো খরচও নেই।
C-তে concept নেই, কারণ template-ই নেই। C-র “যেকোনো টাইপ” ফাংশন qsort চুক্তিটি রাখে শুধু documentation-এ: “তুলনার ফাংশন ঋণাত্মক, শূন্য বা ধনাত্মক ফেরত দেবে, আর ক্রমটি সংগত হবে।” compiler এর কিছুই যাচাই করে না — void* সব টাইপকে সমান করে দেয়। C++-এর concept সেই কাগুজে চুক্তির ব্যাকরণের অংশটুকু compiler-এর হাতে তুলে দেয়। function pointer আর callback-এর C-রীতি গভীরে দেখতে C কোর্সের Function Pointer ও Callback পর্ব পড়ুন।
২ · standard library-র তৈরি concept — আগে এগুলো চিনুন
নিজের concept লেখার আগে জেনে নিন, standard library কী কী তৈরি করে রেখেছে। বেশিরভাগ দৈনন্দিন শর্তের জন্য নতুন কিছু লিখতেই হয় না। header <concepts>-এর সবচেয়ে কাজের কয়েকটি:
| concept | মানে | সত্য | মিথ্যা |
|---|---|---|---|
| std::integral<T> | পূর্ণসংখ্যার টাইপ | int, long, unsigned, char, bool | double, std::string |
| std::floating_point<T> | দশমিক সংখ্যার টাইপ | float, double, long double | int |
| std::same_as<T, U> | দুটি হুবহু একই টাইপ | same_as<int, int> | same_as<int, long> |
| std::convertible_to<F, T> | F থেকে T-তে নিজে থেকে রূপান্তর হয় | int → double | std::string → int |
| std::totally_ordered<T> | ছয়টি তুলনা অপারেটর চলে | int, std::string | std::complex<double> |
| std::copyable<T> | কপি করে বানানো আর assign করা যায় | std::string, std::vector | std::unique_ptr |
| std::invocable<F, Args...> | F-কে এই argument দিয়ে ডাকা যায় | lambda, ফাংশন pointer | int |
| std::predicate<F, Args...> | ডাকা যায়, আর ফল bool-এর মতো | [](int x){ return x > 0; } | যে lambda string ফেরত দেয় |
তালিকায় একটি চমক খেয়াল করুন: bool আর char দুটিই std::integral. C++-এর চোখে এরা পূর্ণসংখ্যার টাইপ। তাই “শুধু সংখ্যা চাই” লিখতে গিয়ে শুধু std::integral বসালে true + true-এর গড়ও compile হয়ে যাবে। এমন ক্ষেত্রে শর্ত মেলাতে হয় — ৫ নম্বর অংশে দেখবেন।
আরেকটি সূক্ষ্মতা: std::invocable আর std::predicate-এর প্রথম টাইপটি callable, বাকিগুলো তাকে যে argument দিয়ে ডাকা হবে তাদের টাইপ। std::predicate<F, int> মানে “F-কে একটি int দিয়ে ডাকা যায়, আর ফলটিকে সত্য-মিথ্যা হিসেবে পরীক্ষা করা যায়”। পর্ব ৩৩-এর std::count_if-এ যে predicate দিয়েছিলেন, ranges সংস্করণে ঠিক এই concept দিয়ে তাকে যাচাই করা হয়।
ranges-এর concept
পর্ব ৩৫-এর ranges লাইব্রেরি পুরোটাই concept-এর ওপর দাঁড়িয়ে। std::ranges::range<R> মানে R-এর ওপর begin আর end ডাকা যায় — অর্থাৎ range-for দিয়ে হাঁটা যায়। তার ওপর ধাপে ধাপে কড়া শর্ত: input_range (একবার পড়া যায়), forward_range (বারবার পড়া যায়), bidirectional_range (পেছনেও যাওয়া যায়), আর random_access_range (যেকোনো ঘরে সরাসরি লাফ)। প্রতিটি আগেরটির সব শর্ত মেনে আরও কিছু চায় — পর্ব ৩২-এর iterator category-র সিঁড়ি, এখন compiler-এর পড়ার মতো রূপে। std::vector সিঁড়ির সবচেয়ে ওপরে, std::list bidirectional পর্যন্ত। তাই std::ranges::sort vector নেয়, list নেয় না।
ডাকার সময় compiler কীভাবে একটি concept-এর সাথে ডাকটি মেলায়, ধাপে ধাপে দেখুন। এখানে Number একটি নিজের লেখা concept (৫ নম্বর অংশে এর পুরো কোড): “পূর্ণসংখ্যা বা দশমিক সংখ্যা”।
1template <Number T>2double average(const std::vector<T>& v);34average(std::vector{70, 85, 90});5average(std::vector<std::string>{"a"});
| T | |
| শর্তের হিসাব | |
| ফল |
- ঘোষণা। compiler template-টি মনে রাখল, সাথে তার শর্ত: T-কে
Numberহতে হবে। এখনো কোনো ফাংশন তৈরি হয়নি, কারণ কেউ ডাকেনি। - প্রথম ডাক: deduction.
std::vector{70, 85, 90}একটিstd::vector<int>(পর্ব ২৯-এর CTAD). পর্ব ২৮-এর template argument deduction-এর নিয়মে T = int. এই পর্যন্ত সব আগের মতোই। - শর্ত যাচাই, তারপর instantiation. নতুন ধাপ: শরীরে ঢোকার আগে compiler হিসাব করে
Number<int>. int পূর্ণসংখ্যা, তাই শর্ত সন্তুষ্ট। এরপরই সে instantiation করেaverage<int>বানায়। - দ্বিতীয় ডাক: শর্ত মিথ্যা। এবার T = std::string. compiler শর্তের দুটি অংশই পরীক্ষা করল: string পূর্ণসংখ্যা নয়, দশমিক সংখ্যাও নয়। শর্ত মিথ্যা, তাই এই template এই ডাকের জন্য বিবেচনা থেকেই বাদ — যেন কখনো ছিলই না।
- ছোট, সরাসরি error. আর কোনো প্রার্থী নেই, তাই error — কিন্তু ডাকার লাইনে, আর কারণসহ: কোন শর্ত, কোন টাইপ। শরীরের ভেতরের
sum += xনিয়ে একটি শব্দও নেই, কারণ শরীর কখনো instantiate হয়নি। এটিই concept-এর বড় উপহার।
৩ · শর্ত বসানোর চারটি উপায়
একটি template-এ concept বসানোর চারটি লেখার রীতি আছে। চারটিই একই কাজ করে; পার্থক্য শুধু কোথায় লেখা হয় আর কতটা জটিল শর্ত লেখা যায়। ধরুন শর্ত হলো “T একটি পূর্ণসংখ্যার টাইপ”:
#include <concepts>
// ১. template parameter-এর জায়গায় concept-এর নাম
template <std::integral T>
T half(T x) { return x / 2; }
// ২. requires clause: template-এর ঠিক পরে
template <typename T>
requires std::integral<T>
T half2(T x) { return x / 2; }
// ৩. trailing requires: ফাংশনের ঘোষণার শেষে
template <typename T>
T half3(T x) requires std::integral<T> { return x / 2; }
// ৪. abbreviated: parameter-এর টাইপে "concept auto"
auto half4(std::integral auto x) { return x / 2; }প্রথম রীতিতে concept-এর নামটি typename-এর জায়গায় বসে। একে বলে type constraint. পড়তে সবচেয়ে সহজ: “T একটি integral টাইপ”। যখন শর্ত একটিমাত্র concept আর একটিমাত্র টাইপ নিয়ে, তখন এটিই প্রথম পছন্দ।
দ্বিতীয় রীতি requires clause — পর্ব ২৮-এ এক ঝলক দেখেছিলেন। requires-এর পরে যেকোনো compile-time সত্য-মিথ্যা শর্ত লেখা যায়, && আর || দিয়ে মিলিয়ে: requires std::integral<T> || std::floating_point<T>. যখন শর্তে একাধিক concept বা একাধিক টাইপের সম্পর্ক থাকে (requires std::convertible_to<A, B>), তখন এটি কাজে লাগে।
তৃতীয় রীতি trailing requires clause — শর্ত ফাংশনের ঘোষণার শেষে, parameter-এর তালিকার পরে। এর বিশেষ সুবিধা: শর্তে parameter-এর নাম ব্যবহার করা যায়। আরেকটি জায়গায় এটি অপরিহার্য — class template-এর ভেতরের সাধারণ member function-এ, যার নিজের কোনো template parameter নেই। যেমন একটি Box<T>-এর void print() const requires Printable<T>; — print আছে শুধু তখনই, যখন T ছাপানো যায়।
চতুর্থ রীতি পর্ব ২৮-এর abbreviated function template-এর শর্তযুক্ত রূপ: std::integral auto x. লিখতে সবচেয়ে ছোট। মনে রাখুন, শর্তহীন auto-র মতোই প্রতিটি concept auto parameter একটি আলাদা template parameter. “দুটি argument একই টাইপের” — এই সম্পর্ক বোঝাতে প্রথম বা দ্বিতীয় রীতিতে ফিরে যান। আর এই রূপটি ভ্যারিয়েবলেও চলে: std::integral auto n = count(); — count() পূর্ণসংখ্যা না দিলে compile error. এটি AI-এর লেখা কোডে টাইপ সম্পর্কে নিজের ধারণা পরীক্ষার একটি সস্তা উপায়।
| রীতি | কখন বেছে নেবেন |
|---|---|
template <std::integral T> | এক টাইপ, এক concept — সবচেয়ে প্রচলিত |
requires A<T> && B<T> | কয়েকটি শর্ত মেলানো, বা দুটি টাইপের সম্পর্ক |
শেষে requires ... | class template-এর member function, বা parameter-এর নাম লাগলে |
std::integral auto x | ছোট সহায়ক ফাংশন, যেখানে T-এর নাম লাগে না |
একটি ব্যাকরণের ফাঁদ। requires clause-এ শর্ত হিসেবে একটি concept বা সরল সত্য-মিথ্যা মান সরাসরি লেখা যায়, কিন্তু একটু জটিল expression হলে বন্ধনী লাগে: requires (sizeof(T) <= 8). বন্ধনী ছাড়া লিখলে compiler error দেয়। নিয়মটি মনে রাখার চেয়ে সহজ অভ্যাস: concept-এর নাম ছাড়া অন্য কিছু হলে বন্ধনীতে মুড়ে দিন।
৪ · নিজের concept লেখা — requires expression
standard concept যখন আপনার চাহিদা বলতে পারে না — যেমন “যার area() নামে একটি কাজ আছে” — তখন নিজের concept লিখুন। গঠনটি সরল:
template <typename T>
concept নাম = শর্ত; // শর্ত: compile-time-এ true বা falseশর্তটি অন্য concept-এর মিশ্রণ হতে পারে (std::integral<T> || std::floating_point<T>), অথবা একটি requires expression — concept লেখার আসল যন্ত্র। requires expression দেখতে একটি ফাংশনের মতো: বন্ধনীতে কাল্পনিক parameter, আর ধনুর্বন্ধনীর ভেতরে শর্তের তালিকা। কিন্তু এর ভেতরের কোড কখনো চলে না। compiler শুধু দেখে, লেখাগুলো বৈধ C++ কি না। চার রকম শর্ত লেখা যায়:
simple requirement
s.area(); — এই expression কি লেখা যায়? ফলের টাইপ বা মান নিয়ে কোনো কথা নেই, শুধু বৈধতা।
type requirement
typename T::unit; — T-এর ভেতরে unit নামে একটি টাইপ আছে কি?
compound requirement
{ s.area() } -> std::convertible_to<double>; — লেখা যায়, আর ফলটি double-এ রূপান্তর হয়।
nested requirement
requires sizeof(T) <= 16; — একটি compile-time শর্ত, যার মান সত্য হতে হবে।
compound requirement-এ তীর চিহ্নের পরে একটি concept বসে, কিন্তু তার প্রথম টাইপটি লেখা হয় না — compiler নিজে বসিয়ে দেয় ধনুর্বন্ধনীর expression-এর টাইপ। -> std::convertible_to<double> আসলে পড়া হয় std::convertible_to<(area()-র টাইপ), double> হিসেবে। চারটি একসাথে একটি concept-এ রেখে একটি টাইপের ওপর লাইন ধরে ধরে হিসাব করে দেখুন:
1template <typename T>2concept SmallShape = requires(const T& s) {3 s.area(); // simple4 typename T::unit; // type5 { s.area() } -> std::convertible_to<double>; // compound6 requires sizeof(T) <= 16; // nested7};89struct Tile { using unit = int; int w, h; int area() const { return w * h; } };
| যা যাচাই হচ্ছে | |
| উত্তর | |
| এ পর্যন্ত |
- কাল্পনিক parameter.
requires(const T& s)-এরsএকটি কল্পনার ভ্যারিয়েবল — কোনো object বানানো হয় না, কোনো constructor ডাকা হয় না। শুধু শর্ত লেখার সুবিধার জন্য একটি নাম।const T&নেওয়ায় শর্তটি বলছে: const object-এর ওপরও কাজগুলো চলতে হবে। - simple requirement.
s.area();— compiler শুধু দেখে, const Tile-এর ওপরarea()লেখা বৈধ কি না। Tile-এরarea()একটি const member function, তাই বৈধ। ফাংশনটি ডাকা হয় না, ফলের টাইপও দেখা হয় না। - type requirement.
typename T::unit;— Tile-এর ভেতরেunitনামে একটি টাইপ আছে কি?using unit = int;আছে, তাই হ্যাঁ। Tile-এ যদিint unit;নামে একটি ভ্যারিয়েবল থাকত, উত্তর হতো না — নামটি টাইপ নয়। - compound requirement. এবার দুটি প্রশ্ন একসাথে: লেখা যায় কি, আর ফলের টাইপ শর্ত মানে কি।
s.area()-র টাইপ int; compiler হিসাব করেstd::convertible_to<int, double>— সত্য। যদিarea()একটি std::string ফেরত দিত, এখানে মিথ্যা হতো, যদিও simple requirement পাস করত। - nested requirement.
requires sizeof(T) <= 16;— এবার মানটি নিজেই সত্য হতে হবে। দুটি int-এর Tile সাধারণত 8 byte, তাই সত্য। এখানে লাইনের শুরুরrequiresশব্দটি অপরিহার্য — পরের ধাপে কারণ। - ফল, আর একটি ফাঁদ। সব শর্ত সত্য, তাই
SmallShape<Tile>সত্য। এখন ফাঁদটি: শেষ লাইনেrequiresবাদ দিয়ে শুধুsizeof(T) <= 16;লিখলে সেটি হয়ে যেত simple requirement — compiler দেখত শুধু “এই তুলনাটি লেখা বৈধ কি না”, যা সবসময় বৈধ। ১০০ byte-এর টাইপও পাস করত, কোনো warning ছাড়াই।
এবার একটি পূর্ণ প্রোগ্রাম। একটি Shape concept, যা চায় area() double-এর মতো কিছু ফেরত দেবে আর name() একটি লেখা দেবে। দুটি struct — কোনো base class নেই, কোনো virtual নেই — শুধু কাজগুলো আছে:
#include <concepts>
#include <print>
#include <string_view>
template <typename T>
concept Shape = requires(const T& s) {
{ s.area() } -> std::convertible_to<double>;
{ s.name() } -> std::convertible_to<std::string_view>;
};
struct Circle { // no base class, no virtual
double r;
double area() const { return 3.14 * r * r; }
std::string_view name() const { return "circle"; }
};
struct Rect {
double w, h;
double area() const { return w * h; }
std::string_view name() const { return "rect"; }
};
void report(const Shape auto& s) { std::println("{}: {:.2f}", s.name(), s.area()); }
static_assert(Shape<Circle> && Shape<Rect> && !Shape<int>);
int main() { report(Circle{2.0}); report(Rect{3.0, 4.5}); }আউটপুট দুই লাইন: circle: 12.56 আর rect: 13.50. report একটি abbreviated template, যার parameter-এর শর্ত Shape. Circle আর Rect কোনোভাবেই সম্পর্কিত নয় — একই base class থেকে আসেনি, কোথাও লেখা নেই “আমি Shape”. তবু দুটিই শর্ত মানে, কারণ দরকারি কাজ দুটি তাদের আছে। concept টাইপের গঠন দেখে, বংশপরিচয় নয় — একে বলে structural typing. static_assert লাইনটি compile-এর সময়েই নিশ্চিত করে যে দুটি shape শর্ত মানে আর int মানে না; কেউ ভবিষ্যতে Circle-এর name() মুছে দিলে প্রোগ্রাম আর compile হবে না, আর error বলবে কোন শর্ত ভাঙল।
আরও দুটি বাস্তব concept: logging আর payment
ধরুন আপনি একটি logging ফাংশন লিখছেন, যা যেকোনো ছাপার মতো মান log-এ লেখে। “ছাপার মতো” মানে std::ostream-এ << দিয়ে পাঠানো যায়:
#include <concepts>
#include <ostream>
template <typename T>
concept Printable = requires(std::ostream& os, const T& x) {
{ os << x } -> std::same_as<std::ostream&>;
};
void log_info(std::ostream& out, const Printable auto& value) {
out << "[INFO] " << value << '\n';
}লক্ষ করুন, এখানে std::same_as নেওয়া হয়েছে, কারণ <<-কে chain করতে (out << a << b) ফলটি ঠিক সেই stream-এর reference হওয়া চাই। আর একটি কথা: C++23 থেকে <format>-এ std::formattable নামে তৈরি concept আছে — “std::format দিয়ে ছাপানো যায়”। আপনার logger যদি std::println ব্যবহার করে, তাহলে নিজের Printable না লিখে সেটিই নিন।
দ্বিতীয় উদাহরণ একটি দোকানের checkout. টাকা নেওয়ার কাজ করতে পারে মোবাইল ওয়ালেট, কার্ড, বা পরীক্ষার জন্য একটি নকল gateway. checkout-এর শুধু একটি জিনিস দরকার: charge(int) নামে একটি কাজ, যা সফল হলো কি না জানায়।
template <typename P>
concept PaymentGateway = requires(P& p, int taka) {
{ p.charge(taka) } -> std::convertible_to<bool>;
};
template <PaymentGateway P>
bool checkout(P& gateway, int taka) {
if (taka <= 0) return false;
return gateway.charge(taka);
}
struct MobileWallet { bool charge(int taka); }; // আসল লেনদেন
struct FakeGateway { bool charge(int) { return true; } }; // টেস্টের জন্যটেস্টে FakeGateway, আসল দোকানে MobileWallet — checkout-এর কোড একটুও বদলায় না, আর কোনো virtual ডাকও নেই। কেউ ভুল করে এমন একটি টাইপ দিলে যার charge নেই, বা যার charge একটি string নেয়, error আসবে checkout ডাকার লাইনেই: “PaymentGateway শর্ত মেলেনি”।
৫ · শর্ত দিয়ে overload — বেশি নির্দিষ্ট শর্ত জেতে
পর্ব ৯-এ দেখেছেন, একই নামের কয়েকটি ফাংশন থাকলে compiler argument দেখে বাছাই করে (overload resolution). concept এই বাছাইয়ে নতুন একটি মাপকাঠি যোগ করে: যার শর্ত মেলে না, সে বাদ; আর দুজনেরই মিললে, যার শর্ত বেশি নির্দিষ্ট, সে জেতে। প্রথমে একটি প্রোগ্রাম, যাতে দুটি ঘটনাই আছে — শুধু সংখ্যার জন্য একটি average, আর একই নামের দুটি describe:
#include <concepts>
#include <print>
#include <vector>
template <typename T>
concept Number = (std::integral<T> || std::floating_point<T>)
&& !std::same_as<T, bool>;
template <Number T>
double average(const std::vector<T>& v) {
double sum = 0;
for (T x : v) sum += x;
return v.empty() ? 0.0 : sum / v.size();
}
void describe(std::integral auto) { std::println("integral"); }
void describe(std::signed_integral auto) { std::println("signed integral (more specific)"); }
int main() {
std::println("{:.2f} {:.2f}", average(std::vector{70, 85, 90}), average(std::vector{2.5, 3.0}));
describe(42u);
describe(-7);
}আউটপুট তিন লাইন: 81.67 2.75, integral আর signed integral (more specific). Number concept তিনটি শর্ত মেলায়: পূর্ণসংখ্যা অথবা দশমিক, এবং bool নয় — ২ নম্বর অংশের চমকটির সমাধান। খালি vector-এ শূন্য দিয়ে ভাগ এড়াতে v.empty() পরীক্ষা আছে। এবার describe: 42u একটি unsigned int — integral, কিন্তু signed নয়, তাই শুধু প্রথমটি প্রার্থী। -7 একটি int — integral-ও, signed-ও। দুটি overload-ই শর্ত মানে, অথচ error নেই; compiler দ্বিতীয়টি বেছে নিয়েছে। কেন, নিচের stepper-এ।
এই বাছাইয়ের নিয়মের নাম subsumption, বাংলায় বলা যায় “অন্তর্ভুক্তি”। শর্ত A যদি শর্ত B-র সবকিছু ধারণ করে, আর তার ওপর আরও কিছু চায়, তাহলে A বেশি নির্দিষ্ট — A “B-কে subsume করে”। standard library-তে std::signed_integral-এর সংজ্ঞা হলো std::integral<T> && std::is_signed_v<T> — integral-এর সব, আর তার ওপর signed. ধাপে ধাপে দেখুন:
void describe(std::integral auto); // ক void describe(std::signed_integral auto); // খ describe(-7); // -7 হলো int
ক: integral<int> → true ✔ টিকে রইল খ: signed_integral<int> → true ✔ টিকে রইল parameter-এর মিল দুটিতেই হুবহু এক (int) → সাধারণ নিয়মে কেউ জেতে না
ক-এর শর্ত: [ is_integral_v<T> ]
খ-এর শর্ত: signed_integral<T>
= integral<T> && is_signed_v<T>
= [ is_integral_v<T> ] && [ is_signed_v<T> ]
খ সত্য হলে ক-ও সত্য হবেই (খ-এর ভেতরে ক-এর অংশটি আছে) ক সত্য হলেও খ সত্য না-ও হতে পারে (যেমন unsigned) → খ বেশি নির্দিষ্ট → খ জেতে আউটপুট: signed integral (more specific)
ক: integral<unsigned> → true ✔ খ: signed_integral<unsigned> → false ✘ বাদ একজনই প্রার্থী → ক আউটপুট: integral
template <typename T> concept Num1 = std::is_integral_v<T>;
template <typename T> concept Num2 = std::is_integral_v<T>
&& std::is_signed_v<T>;
void g(Num1 auto); void g(Num2 auto);
g(-7); // error: ambiguous!
// Num2-এর is_integral_v আর Num1-এরটি আলাদা জায়গায় লেখা,
// তাই compiler এদের "একই শর্ত" ধরে না।
// সমাধান: Num2 = Num1<T> && std::is_signed_v<T>;
- দুটি প্রার্থী। একই নামের দুটি ফাংশন, পার্থক্য শুধু শর্তে: ক চায় পূর্ণসংখ্যা, খ চায় চিহ্নযুক্ত (signed) পূর্ণসংখ্যা। ডাক
describe(-7), আর -7 একটি int. - শর্ত যাচাই। প্রথমে প্রতিটি প্রার্থীর শর্ত হিসাব হয়। int পূর্ণসংখ্যা আর signed — দুজনেই টিকে রইল। argument-এর মিলও দুটিতে হুবহু এক, তাই পর্ব ৯-এর সাধারণ নিয়মে টাই। concept না থাকলে এখানেই ambiguous call-এর error হতো।
- শর্ত ভেঙে দেখা। টাই ভাঙতে compiler প্রতিটি শর্তকে তার ক্ষুদ্রতম অংশে ভাঙে — concept-এর নামগুলো তাদের সংজ্ঞায় খুলে, যতক্ষণ না সরল সত্য-মিথ্যা টুকরো (atomic constraint) পাওয়া যায়, যেগুলো
&&আর||দিয়ে জোড়া। ক-তে একটি টুকরো, খ-তে দুটি; আর খ-এর প্রথম টুকরোটি হুবহু ক-এর টুকরো, কারণ খ সংজ্ঞায় integral-কেই ব্যবহার করেছে। - Subsumption. খ সত্য হলে ক সত্য হবেই, কিন্তু উল্টোটা নয়। তাই খ বেশি নির্দিষ্ট (more constrained), আর বেশি নির্দিষ্ট শর্তের overload জেতে। এটি দৈনন্দিন যুক্তির মতোই: “যেকোনো পূর্ণসংখ্যা” আর “চিহ্নযুক্ত পূর্ণসংখ্যা” — দুই দলের নিয়ম থাকলে ছোট, নির্দিষ্ট দলের নিয়মটিই প্রযোজ্য।
- শুধু একজন যোগ্য।
42u-তে খ-এর শর্ত মিথ্যা, তাই খ বিবেচনা থেকেই বাদ। subsumption-এর প্রশ্ন ওঠে শুধু তখন, যখন একাধিক প্রার্থী সমান ভালোভাবে টিকে থাকে। - যে ফাঁদে অভিজ্ঞরাও পড়েন। compiler “একই শর্ত” চেনে লেখার জায়গা দেখে, অর্থ দেখে নয়। দুটি আলাদা concept-এ একই কথা আলাদাভাবে লিখলে compiler সেগুলোকে আলাদা টুকরো ধরে, subsumption হয় না, ফলে ambiguous. শিক্ষা: বড় concept-কে ছোট concept-এর নাম দিয়ে গড়ুন, শর্ত কপি-পেস্ট করে নয়।
এই কৌশলের একটি পরিচিত ব্যবহার iterator-কে n ঘর এগোনো। random access iterator পেলে এক লাফে n ঘর এগোনো যায়, শুধু forward iterator পেলে একে একে n বার। পুরোনো std::advance-এ এই বাছাই হতো “tag dispatch” নামের এক কৌশলে, যাতে বাছাইয়ের জন্য আলাদা নকল টাইপ বানিয়ে পাঠাতে হতো। concept দিয়ে এখন দুটি overload লিখলেই হয় — একটির শর্ত std::random_access_iterator, অন্যটির std::forward_iterator — আর subsumption নিজেই ঠিকটি বেছে নেয়।
৬ · concept বনাম abstract class — compile-time-এর interface
পর্ব ২৬-এ abstract class দিয়ে interface বানিয়েছিলেন: pure virtual function-এর একটি তালিকা, যা প্রতিটি derived class-কে বাস্তবায়ন করতেই হবে। concept-ও একটি চুক্তি — “এই কাজগুলো থাকতে হবে”। তাহলে দুটির পার্থক্য কী? একই Shape চুক্তি দুই ভাবে পাশাপাশি দেখুন:
struct IShape {
virtual double area() const = 0;
virtual ~IShape() = default;
};
struct Circle : IShape { // বংশ ঘোষণা বাধ্যতামূলক
double r;
double area() const override { return 3.14 * r * r; }
};
void report(const IShape& s); // সাধারণ ফাংশন, একটিই
template <typename T>
concept Shape = requires(const T& s) {
{ s.area() } -> std::convertible_to<double>;
};
struct Circle { // কোনো base নেই
double r;
double area() const { return 3.14 * r * r; }
};
void report(const Shape auto& s); // template, টাইপ-প্রতি একটি
abstract class: object → vptr → vtable → area-র ঠিকানা → ডাক
(পরোক্ষ ডাক, সাধারণত inline হয় না)
concept: Circle::area সরাসরি ডাক
(compile-এর সময় টাইপ জানা, প্রায়ই inline)
// third_party/geo.h (ছোঁয়া যাবে না)
struct Polygon { double area() const; };
abstract class: Polygon IShape থেকে আসেনি →
একটি adapter class লিখে মুড়াতে হবে
concept: Polygon-এ area() আছে → Shape<Polygon> সত্য,
সরাসরি report(poly) চলে
std::vector<std::unique_ptr<IShape>> all; // ✔ Circle, Rect মিশিয়ে all.push_back(std::make_unique<Circle>(...)); std::vector<Shape> all; // ✘ concept কোনো টাইপ নয়! // নানা টাইপ এক জায়গায় রাখতে চলার সময়ের ব্যবস্থা লাগে: // virtual, std::variant (পর্ব ৩৮) বা type erasure
- abstract class-এর চুক্তি। চুক্তিটি একটি টাইপ:
IShape. যে টাইপ চুক্তি মানতে চায়, তাকে নাম ধরে ঘোষণা দিতে হয় —: IShape— আরoverrideকরতে হয়। একে বলে nominal typing: সম্পর্ক নাম দিয়ে ঘোষিত।reportএকটি সাধারণ ফাংশন, যার কোড একবারই তৈরি হয়। - concept-এর চুক্তি। চুক্তিটি টাইপ নয়, একটি শর্ত। Circle কোথাও বলে না সে Shape; তার
area()আছে, তাই সে Shape. কোনো base class নেই, তাই কোনো vptr নেই — object-এর মাপও ছোট।reportএকটি template, যে টাইপে ডাকা হয় তার জন্য আলাদা ফাংশন। - খরচ। virtual ডাকে চলার সময় vtable থেকে ঠিকানা খুঁজতে হয় (পর্ব ২৫)। concept-এর পথে টাইপ compile-এর সময়েই জানা, তাই ডাক সরাসরি, প্রায়ই inline — এটি পর্ব ২৮-এর static polymorphism. দাম হলো টাইপ-প্রতি আলাদা কোড আর বেশি compile সময়।
- পুরোনো বা অন্যের টাইপ। concept-এর একটি বড় সুবিধা: যে টাইপ আপনি বদলাতে পারেন না, সেটিও চুক্তি মানতে পারে, যদি কাজগুলো তার থাকে। abstract class-এ সেই টাইপকে মুড়ে একটি adapter লিখতে হয়। আবার উল্টো দিকও আছে: কাকতালীয়ভাবে
area()নামের কাজ থাকা একটি সম্পূর্ণ ভিন্ন অর্থের টাইপও Shape হয়ে যাবে। - যেখানে virtual-ই উত্তর। concept কোনো টাইপ নয়, তাই
std::vector<Shape>লেখা যায় না। চলার সময় ব্যবহারকারী বেছে নেবেন কোন shape, বা একটি তালিকায় নানা shape রাখতে হবে — তখন চাই চলার সময়ের ব্যবস্থা: virtual,std::variant, বা type erasure. প্রশ্নটি পর্ব ২৮-এর মতোই: টাইপটি কে জানে, আর কখন?
| প্রশ্ন | concept | abstract class |
|---|---|---|
| চুক্তি মানা কীভাবে | কাজগুলো থাকলেই (structural) | base থেকে inherit করে (nominal) |
| যাচাই কখন | compile-এর সময় | compile-এর সময় (override), ডাক চলার সময় |
| vtable / vptr | নেই | আছে |
| ডাকের খরচ | সরাসরি, প্রায়ই inline | পরোক্ষ ডাক |
| এক container-এ নানা টাইপ | না | হ্যাঁ, base pointer দিয়ে |
| ফেরত টাইপ বা অপারেটরের শর্ত | হ্যাঁ (-> convertible_to, a < b) | শুধু member function-এর signature |
| দাম | টাইপ-প্রতি কোড, header-এ কোড | heap object, পরোক্ষ ডাক |
দুটি প্রতিদ্বন্দ্বী নয়, সঙ্গী। বড় প্রোগ্রামে প্রায়ই দুটিই থাকে: ভেতরের দ্রুত, টাইপ-জানা অংশে concept, আর প্লাগইনের মতো চলার সময়ে বদলানো অংশে virtual. একটি ভালো নিয়ম: আগে জিজ্ঞেস করুন “নানা টাইপকে কি একসাথে, এক তালিকায় রাখতে হবে?” — হ্যাঁ হলে virtual বা variant; না হলে concept.
৭ · static_assert আর if constexpr-এ concept
concept শুধু template-এর মাথায় বসে না। Shape<Circle> নিজেই একটি compile-time bool মান, তাই যেখানে compile-time সত্য-মিথ্যা চলে, সেখানে concept-ও চলে। দুটি জায়গা বিশেষ কাজের।
static_assert — নিজের টাইপের পক্ষে প্রতিশ্রুতি
shapes.cpp-এ দেখেছেন: static_assert(Shape<Circle>);. static_assert compile-এর সময় একটি শর্ত পরীক্ষা করে, মিথ্যা হলে compile থামিয়ে দেয়। টাইপের সংজ্ঞার ঠিক পরে এমন একটি লাইন রাখা একটি ভালো অভ্যাস। কেন? template থেকে দূরে বসে কেউ Circle বদলালে ভাঙনটি ধরা পড়ত অনেক পরে, অন্য কোনো ফাইলে, যেখানে Circle-কে template-এ পাঠানো হয়। static_assert থাকলে ভাঙনটি ধরা পড়ে Circle-এর নিজের ফাইলে। এটি abstract class-এর override শব্দের মতো একটি স্ব-ঘোষণা — structural typing-এ যা নিজে থেকে নেই, তা আপনি হাতে যোগ করছেন।
if constexpr — একই ফাংশনে টাইপ-ভেদে শাখা
পর্ব ৩৯-এ দেখেছেন, if constexpr compile-এর সময় শাখা বাছে, আর বাদ পড়া শাখা instantiate হয় না। শর্ত হিসেবে concept, এমনকি একটি requires expression সরাসরি বসানো যায়:
template <typename T>
void log_value(const T& x) {
if constexpr (std::integral<T>)
std::println("int: {}", x);
else if constexpr (requires { x.name(); }) // ad-hoc শর্ত
std::println("object: {}", x.name());
else
std::println("(unprintable)");
}দ্বিতীয় শাখার requires { x.name(); } একটি নামহীন requires expression: “x-এর name() ডাকা যায় কি?” Circle দিলে দ্বিতীয় শাখা চলে; int দিলে প্রথমটি, আর দ্বিতীয় শাখা — যেখানে int-এর ওপর .name() লেখা — কখনো instantiate হয় না, তাই error-ও হয় না। একটিমাত্র জায়গায় লাগে এমন ছোট শর্তের জন্য নামহীন রূপ সুবিধাজনক। কিন্তু একই শর্ত দুই জায়গায় লিখতে হলে তাকে নাম দিন — concept বানান।
কখন overload, কখন if constexpr? শাখাগুলো ছোট আর একই কাজের অংশ হলে একটি ফাংশনে if constexpr পড়তে সহজ। শাখাগুলো বড়, বা ভবিষ্যতে নতুন টাইপের জন্য নতুন শাখা অন্য কেউ যোগ করবে — তখন আলাদা overload ভালো, কারণ পুরোনো ফাংশন না ছুঁয়েই নতুন overload লেখা যায়।
৮ · concept ব্যাকরণ দেখে, অর্থ নয় — আর ভালো concept-এর নকশা
এখন সবচেয়ে গুরুত্বপূর্ণ সীমাটি বুঝুন। ধরুন কেউ এমন একটি টাইপ লিখল:
struct Weird {
int v;
bool operator==(const Weird&) const { return true; } // সবাই সমান?
auto operator<=>(const Weird&) const { return std::strong_ordering::less; }
}; // a < b সত্য, আবার b < a-ও সত্য!
static_assert(std::totally_ordered<Weird>); // পাস করে!এখানে a < b আর b < a দুটিই সত্য, আবার a == b-ও সত্য — কোনো যুক্তিসংগত ক্রমের সাথে মেলে না। তবু std::totally_ordered<Weird> সত্য, কারণ concept শুধু দেখে ছয়টি তুলনা লেখা যায় আর ফল bool-এর মতো। এই টাইপের vector-কে std::ranges::sort দিলে compile হবে, কিন্তু ফল অর্থহীন — আর কিছু বাস্তবায়নে sort-এর লুপ array-র সীমার বাইরেও চলে যেতে পারে।
standard এই পার্থক্যকে দুটি শব্দে ভাগ করে। একটি টাইপ একটি concept-কে satisfy করে, যখন ব্যাকরণের শর্তগুলো মেলে — compiler এটুকুই যাচাই করে। আর টাইপটি concept-কে model করে, যখন তার ওপর semantic requirement-ও খাটে — যেমন totally_ordered-এর বেলায়: a < b আর b < a একসাথে সত্য হতে পারে না, a < b আর b < c হলে a < c. এই অংশ লেখা থাকে শুধু standard-এর পাতায়। standard library-র ফাংশনে এমন টাইপ দিলে, যা satisfy করে কিন্তু model করে না, প্রোগ্রামটির আচরণের কোনো নিশ্চয়তা থাকে না, আর compiler-এর কিছু বলার দায়ও নেই।
এর একটি পরিচিত বাস্তব উদাহরণ double. সাধারণ সংখ্যায় double-এর তুলনা নিখুঁত। কিন্তু NaN (“not a number”, যেমন 0.0 / 0.0) এলে NaN < x, x < NaN, NaN == x — সবই মিথ্যা, এমনকি NaN == NaN-ও। standard-এর ব্যাখ্যা হলো, শর্তগুলো প্রযোজ্য শুধু সেই মানগুলোর ওপর, যা নিয়ে আসলে কাজ হচ্ছে। NaN-ভরা ডেটা sort করার আগে NaN বাদ দেওয়া তাই আপনার দায়িত্ব — কোনো concept আপনাকে মনে করিয়ে দেবে না।
concept প্রশ্ন করে “এটি লেখা যায় কি?”, কখনো “এটি ঠিক কাজ করে কি?” নয়। ব্যাকরণের দিক compiler-এর, অর্থের দিক আপনার। তাই একটি concept-এর নামের সাথে তার অর্থও ঠিক করে documentation-এ লিখুন, আর টেস্টে (পর্ব ৪৮) সেই অর্থ যাচাই করুন — যেমন sort-এর পরে ক্রমটি সত্যিই সাজানো কি না।
ভালো concept-এর তিনটি নিয়ম
- আগে standard concept. সংখ্যা, তুলনা, কপি, ডাকা যায় এমন কিছু, range — প্রায় সবই
<concepts>,<iterator>আর<ranges>-এ আছে, নিখুঁত সংজ্ঞা আর subsumption-এর সঠিক গঠনসহ। নিজেরComparableলিখলে সেটিstd::totally_ordered-এর সাথে subsumption-এ মিলবে না (৫ নম্বর অংশের ফাঁদ), আর পাঠককে নতুন একটি নাম শিখতে হবে। - নাম দিন ক্ষমতার নামে।
Shape,Printable,PaymentGateway— টাইপটি কী করতে পারে।HasAreaFunctionবাHasChargeMethod-এর মতো নাম বাস্তবায়নের খুঁটিনাটি বলে, অর্থ বলে না; আর দুই-তিনটি কাজের শর্ত মিলিয়ে একটি অর্থপূর্ণ ধারণা তৈরিই concept-এর উদ্দেশ্য। - বেশি শর্ত দেবেন না। template-এর শরীর আসলে যা ব্যবহার করে, শুধু তা-ই চান। শুধু পড়ে এমন ফাংশনে
std::copyableচাইলেstd::unique_ptr-এর মতো move-only টাইপ অকারণে বাদ পড়ে।-> std::same_as<double>চাইলে যেarea()float ফেরত দেয়, সে বাদ পড়ে —convertible_toযথেষ্ট ছিল। অতিরিক্ত শর্ত over-constraining: নিরাপত্তা বাড়ায় না, শুধু কাজের টাইপ আটকায়।
উল্টো ভুলও আছে। concept-এ শর্ত কম, কিন্তু শরীর বেশি ব্যবহার করে — ধরুন Shape শুধু area() চায়, অথচ report-এর শরীরে s.name()-ও আছে। C++ concept template-এর শরীর যাচাই করে না; সে শুধু ডাকার জায়গায় শর্ত দেখে। ফলে name() ছাড়া একটি টাইপ শর্ত পাস করবে, তারপর শরীরের ভেতর থেকে পুরোনো ধাঁচের লম্বা error আসবে। concept আর শরীর মিলিয়ে রাখা তাই আপনার কাজ।
“concept” শব্দটি C++20-এর আবিষ্কার নয়। ১৯৯০-এর দশকে Alexander Stepanov-এর STL-এর documentation-এ “InputIterator” বা “LessThanComparable”-এর মতো concept-এর বিস্তারিত তালিকা ছিল — কিন্তু শুধু কাগজে। ভাষায় আনার প্রথম বড় চেষ্টা C++0x-এর concepts, যা ২০০৯ সালের জুলাইয়ে Frankfurt-এর সভায় standard কমিটি বাদ দেয়। ২০১৩ সালে Andrew Sutton, Bjarne Stroustrup আর Gabriel Dos Reis “Concepts Lite” নামে একটি সরল নকশা প্রস্তাব করেন; সেটি ২০১৫ সালে Concepts TS হিসেবে প্রকাশিত হয়, আর ২০১৭ সালে C++20-এর খসড়ায় ঢোকে। পুরো গল্প ভাবনার প্রশ্ন ০১-এ।
- concept কী, আর C++-এর কোন সংস্করণে এসেছে? — template parameter-এর ওপর নাম দেওয়া compile-time শর্ত; C++20.
- template-এ শর্ত বসানোর উপায়গুলো কী কী? —
template <C T>, requires clause, trailing requires clause আরC autoparameter. - requires expression-এ কয় রকম requirement লেখা যায়? — চার: simple, type, compound আর nested.
- দুটি constrained overload দুটিই মিললে কোনটি ডাকা হয়? — যার শর্ত অন্যটিকে subsume করে, অর্থাৎ বেশি নির্দিষ্ট; কেউ কাউকে না করলে ambiguous.
- concept আর abstract class-এর মূল পার্থক্য কী? — concept compile-time-এর structural শর্ত, vtable বা inheritance লাগে না; abstract class একটি টাইপ, চলার সময় virtual dispatch দেয়।
৯ · AI-এর যুগে — concept-কোড যাচাই
AI টুল বিপুল পরিমাণ পুরোনো C++ কোড দেখে শিখেছে, যার বেশিরভাগ C++20-এর আগের। তাই template-এ শর্ত চাইলে এরা প্রায়ই লেখে std::enable_if — SFINAE নামের পুরোনো কৌশল, যা দেখতে এমন:
// পুরোনো পথ (C++11)
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T half(T x) { return x / 2; }
// একই কথা C++20-এ
template <std::integral T>
T half(T x) { return x / 2; }দুটি একই কাজ করে, কিন্তু প্রথমটি পড়তে কঠিন, error বার্তা দুর্বোধ্য, আর এই রূপে দুটি overload লিখলে আরেকটি ফাঁদ আছে — default template argument আলাদা হলেও signature এক থাকে, তাই compiler “একই template দুবার ঘোষণা” বলে আপত্তি করে। C++20-এর প্রজেক্টে নতুন কোডে enable_if দেখলে concept-এ বদলাতে বলুন। AI-এর আরও কয়েকটি প্রবণতা:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
enable_if, void_t, SFINAE-এর কৌশল | পড়া কঠিন, error লম্বা, overload-এ ফাঁদ | standard concept বা requires clause |
নিজের Integral, Comparable, Callable বানায় | std-এর concept-এর নকল; subsumption-এ মেলে না | std::integral, std::totally_ordered, std::invocable |
সব জায়গায় std::copyable, same_as<double> | over-constraining: কাজের টাইপ বাদ পড়ে | শরীর যা ব্যবহার করে, শুধু তা |
nested requirement-এ requires শব্দ বাদ | শর্ত শুধু বৈধতা দেখে, মান নয় — সব টাইপ পাস | requires sizeof(T) <= 16; |
| concept-এ যা নেই, শরীরে তা ব্যবহার | শর্ত পাস করে, তবু শরীর থেকে লম্বা error | concept আর শরীর মিলিয়ে দেখা |
| “concept থাকায় কোড নিরাপদ” দাবি | semantic requirement যাচাই হয় না | অর্থের নিয়ম documentation আর টেস্টে |
AI-কে concept-সহ template লিখতে বলার সময় নিয়মগুলো আগেই দিয়ে দিন, আর প্রমাণ চান:
AI-এর concept-কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১C++11-এ concepts প্রায় চূড়ান্ত হয়েও শেষ মুহূর্তে বাদ পড়েছিল। কেন, আর C++20-এ পৌঁছাতে আরও এগারো বছর লাগল কেন?
২০০০-এর দশকের মাঝামাঝি “C++0x” নামে পরের standard-এর কাজ চলছিল, আর concepts ছিল তার সবচেয়ে উচ্চাভিলাষী অংশ। দুটি দল প্রথমে আলাদা নকশা নিয়ে কাজ করে — Indiana University-র Douglas Gregor, Jaakko Järvi, Jeremy Siek, Andrew Lumsdaine প্রমুখ, আর Texas A&M-এ Bjarne Stroustrup ও Gabriel Dos Reis — পরে দুটি মিলিয়ে একটি প্রস্তাব হয়। Douglas Gregor GCC-র একটি পরীক্ষামূলক শাখা ConceptGCC-তে এটি বাস্তবায়নও করেন। নকশাটি বিশাল ছিল: concept-এর সংজ্ঞা, “concept map” (কোনো টাইপ কীভাবে একটি concept মানে, তার স্পষ্ট ঘোষণা), axiom (অর্থের নিয়ম লেখার ব্যবস্থা), আর template-এর শরীর যাচাই — শরীরে শুধু concept-এ লেখা কাজই ব্যবহার করা যাবে।
সমস্যা দেখা দিল ব্যবহারে। একটি বড় বিতর্ক ছিল, কোনো টাইপ concept মানে কি না, তা কি প্রোগ্রামারকে concept map দিয়ে স্পষ্ট ঘোষণা দিতে হবে, নাকি compiler নিজে বুঝে নেবে। Stroustrup ২০০৯ সালে লেখেন যে নকশাটি সাধারণ প্রোগ্রামারের জন্য খুব জটিল হয়ে যাচ্ছে। standard library-র পুরোটা নতুন concept-এ লিখতে হচ্ছিল, compile-এর সময় নিয়েও দুশ্চিন্তা ছিল, আর বাস্তব প্রজেক্টে ব্যবহারের অভিজ্ঞতা ছিল খুব কম। শেষে ২০০৯ সালের জুলাইয়ে Frankfurt-এর সভায় কমিটি ভোট দিয়ে concepts বাদ দেয়, যাতে বাকি standard আর দেরি না করে। সেটিই ২০১১ সালে C++11 হলো।
এরপর পথ বদলাল। ২০১৩ সালে Andrew Sutton, Stroustrup আর Dos Reis “Concepts Lite” প্রস্তাব করেন: শুধু ডাকার জায়গায় শর্ত যাচাই, কোনো concept map নেই, শরীর যাচাই নেই, axiom নেই। লক্ষ্য ছিল ছোট, বাস্তবায়নযোগ্য একটি ভিত্তি। ২০১৫ সালে এটি একটি Technical Specification (Concepts TS) হিসেবে প্রকাশিত হলো, আর GCC 6 সেটি -fconcepts flag দিয়ে বাস্তবায়ন করল — অর্থাৎ এবার standard-এ ঢোকার আগেই মানুষ ব্যবহার করে দেখার সুযোগ পেল। সেই অভিজ্ঞতা থেকে ব্যাকরণে কিছু বদল হয়, আর ২০১৭ সালের জুলাইয়ে Toronto-র সভায় concepts C++20-এর খসড়ায় গৃহীত হয়। সংক্ষিপ্ত std::integral auto রূপটি নিয়ে বিতর্ক চলে আরও এক বছরের বেশি, ২০১৮ সালের শেষে সেটির মীমাংসা হয়।
মূল উপলব্ধি: concepts-এর ইতিহাস দেখায়, ভাষার একটি বড় সুবিধা শুধু তাত্ত্বিকভাবে সুন্দর হলেই চলে না — বাস্তবায়ন, শেখা আর পুরোনো কোডের সাথে মেলানো যায় কি না, সেটাও প্রমাণ করতে হয়। ছোট করে শুরু করে, ব্যবহার করে দেখে, তারপর standard-এ আনা — এই পদ্ধতিই শেষ পর্যন্ত কাজ করেছে।
প্র ০২Stepanov-এর STL documentation-এ concept ছিল কাগজে। সেই কাগুজে concept-এ এমন কী ছিল, যা C++20-এর concept আজও প্রকাশ করতে পারে না?
পর্ব ২৮-এর ভাবনার প্রশ্নে দেখেছেন, Alexander Stepanov generic programming-কে দেখতেন গণিতের মতো করে: একটি algorithm-কে তার সবচেয়ে সাধারণ শর্তের ওপর দাঁড় করানো। ১৯৯০-এর দশকে SGI-র প্রকাশিত STL documentation-এ প্রতিটি template-এর পাশে লেখা থাকত সে কোন concept চায় — “InputIterator”, “ForwardIterator”, “LessThanComparable”, “Assignable”. প্রতিটি concept-এর পাতায় ছিল তিন রকম তথ্য: কোন expression বৈধ হতে হবে (valid expressions), সেগুলোর অর্থ কী (semantics, যেমন “++i-এর পরে i পরের উপাদানে যায়”), আর কাজগুলোর জটিলতা (complexity guarantees, যেমন “এই কাজ amortized constant time-এ হবে”)। একটি টাইপ কোনো concept মানলে বলা হতো সে সেই concept-এর “model”, আর এক concept আরেকটির সব শর্ত নিয়ে আরও কিছু চাইলে সেটি “refinement”.
C++20-এর concept এই তিনটির প্রথমটি — বৈধ expression — প্রায় পুরোপুরি প্রকাশ করতে পারে। refinement-এর ধারণা এসেছে subsumption হিসেবে: std::random_access_iterator হলো std::bidirectional_iterator-এর refinement. এমনকি “model” শব্দটিও standard রেখে দিয়েছে, satisfy-এর বিপরীতে। কিন্তু বাকি দুটি — অর্থ আর জটিলতা — আজও শুধু standard-এর লেখায়। কোনো compiler জানে না আপনার iterator-এর ++ সত্যিই constant time-এ চলে, নাকি প্রতিবার পুরো list ঘুরে আসে। অথচ STL-এর নকশায় জটিলতাই ছিল চুক্তির মূল অংশ: std::sort random access চায় কারণ ছাড়া O(n log n) সম্ভব নয়।
Stepanov পরে Paul McJones-এর সাথে ২০০৯ সালে “Elements of Programming” বইটি লেখেন, যেখানে প্রতিটি algorithm-এর মাথায় শর্তগুলো কোডের মতো করে লেখা হয়েছে — তখনো ভাষায় সুবিধা না থাকায় macro দিয়ে, যাতে compiler সেগুলো উপেক্ষা করে। বইটি বলতে গেলে concepts-এর একটি হাতে-লেখা খসড়া। তাঁর দৃষ্টিতে concept কখনো শুধু ব্যাকরণ ছিল না — ছিল একটি গাণিতিক তত্ত্ব, যার কিছু নিয়ম আর কিছু খরচের প্রতিশ্রুতি।
মূল উপলব্ধি: C++20 Stepanov-এর concept-এর ব্যাকরণের অংশটুকু compiler-এ এনেছে; অর্থ আর জটিলতার চুক্তি এখনো মানুষের হাতে। তাই নিজের concept লেখার সময় documentation-এ তিনটিই লিখুন — কী লেখা যায়, তার অর্থ কী, আর কত খরচে।
প্র ০৩SFINAE-কে বলা হয় একটি দুর্ঘটনাক্রমে পাওয়া সুবিধা। নিয়মটি আসলে কেন তৈরি হয়েছিল, আর কীভাবে এটি পুরো একটি প্রজন্মের কৌশল হয়ে উঠল?
SFINAE মানে “Substitution Failure Is Not An Error”. নিয়মটি এসেছিল একটি সাধারণ প্রয়োজন থেকে। ধরুন একই নামের দুটি function template আছে, যার একটির ফেরত টাইপ লেখা typename T::size_type. এখন কেউ int দিয়ে ডাকল। overload resolution-এর জন্য compiler প্রতিটি template-এ T = int বসিয়ে দেখে — আর প্রথমটিতে দাঁড়ায় int::size_type, যা অর্থহীন। এটি যদি সরাসরি error হতো, তাহলে একটি template-এর অস্তিত্বই অন্য সব নিরীহ ডাক ভেঙে দিত। তাই নিয়ম হলো: ঘোষণায় টাইপ বসাতে গিয়ে (substitution) ব্যর্থ হলে সেটি error নয় — শুধু এই প্রার্থীকে বাদ দাও, বাকিদের মধ্যে বাছাই চালিয়ে যাও। উদ্দেশ্য ছিল overloading-কে নিরাপদ রাখা, কোনো নতুন কৌশল তৈরি নয়।
কিন্তু প্রোগ্রামাররা উল্টো দিক থেকে ভাবলেন: যদি ইচ্ছা করে এমন একটি টাইপ লিখি, যা শুধু কোনো শর্ত মিথ্যা হলে ব্যর্থ হয়, তাহলে শর্ত দিয়েই প্রার্থী বাদ দেওয়া যায়! এভাবে জন্ম নিল enable_if: একটি ছোট template, যার ভেতরে type থাকে শুধু শর্ত সত্য হলে। ২০০০-এর দশকের শুরুতে এটি Boost লাইব্রেরিতে আসে, আর C++11-এ std::enable_if হিসেবে standard-এ। “SFINAE” নামটি জনপ্রিয় হয় Daveed Vandevoorde আর Nicolai Josuttis-এর ২০০২ সালের বই “C++ Templates: The Complete Guide”-এর মাধ্যমে। C++11-এর decltype এটিকে আরও শক্তিশালী করল (expression SFINAE: “এই expression লেখা যায় কি?”), আর C++17-এ Walter Brown-এর প্রস্তাবিত std::void_t “কোনো টাইপে এই সদস্য আছে কি” পরীক্ষাকে একটি পরিচিত রীতি বানাল।
দুর্ঘটনার দাম ছিল চড়া। শর্তটি লেখা হতো ফেরত টাইপে বা একটি নকল template parameter-এ লুকিয়ে, তাই পড়ে বোঝা কঠিন। error বার্তায় আসল কারণের বদলে আসত “no type named 'type' in enable_if<false>”. একই রূপের দুটি overload লিখলে compiler সেগুলোকে একই template ধরে আপত্তি করত। আর “বেশি নির্দিষ্ট শর্ত জেতে” — এমন কোনো নিয়ম ছিল না; সব শর্ত পরস্পরবিরোধী করে লিখতে হতো, নইলে ambiguous. requires expression আসলে SFINAE-এর “এই expression লেখা যায় কি” প্রশ্নটিরই পরিচ্ছন্ন, ভাষায় স্বীকৃত রূপ — একই ক্ষমতা, কিন্তু নাম, subsumption আর পড়ার মতো error সহ।
মূল উপলব্ধি: SFINAE দেখায়, ভাষার একটি রক্ষাকবচ-নিয়ম কীভাবে অপ্রত্যাশিত কৌশলের ভিত্তি হয়ে উঠতে পারে। concepts সেই কৌশলের প্রয়োজনকে স্বীকার করে তাকে সরাসরি, পড়ার মতো ব্যাকরণ দিয়েছে — তাই নতুন কোডে SFINAE নয়, concept.
প্র ০৪Rust-এর trait আর Haskell-এর type class দেখতে C++ concept-এর মতো। কিন্তু সেখানে টাইপকে নাম ধরে ঘোষণা দিতে হয়। structural আর nominal — দুই পথের লাভ-ক্ষতি কী?
Haskell-এর type class এসেছে Philip Wadler আর Stephen Blott-এর ১৯৮৯ সালের গবেষণাপত্র থেকে। class Eq a where (==) :: a -> a -> Bool লিখে একটি শর্ত বানানো হয়, আর কোনো টাইপ সেটি মানে শুধু তখনই, যখন কেউ স্পষ্ট লেখে instance Eq Point where .... Rust-এর trait একই পথে: impl Display for Point { ... } না লিখলে Point-এর fmt নামের ফাংশন থাকলেও সে Display নয়। এই পথকে বলে nominal — সম্পর্ক ঘোষণা দিয়ে। C++ concept structural: কাজগুলো থাকলেই হলো, কোনো ঘোষণা লাগে না। (মজার ব্যাপার, C++0x-এর concept map ছিল nominal-এর দিকে একটি পদক্ষেপ, যা বাদ পড়েছিল।) Go-র interface-ও structural, যদিও সেটি চলার সময়ের ব্যবস্থা।
nominal পথের লাভ: কাকতালীয় মিল হয় না। একটি Gun-এর shoot() আর একটি Camera-র shoot() একই নামের হলেও Rust কাউকে ভুল trait-এ ঢুকতে দেবে না। ঘোষণার জায়গাটি একটি documentation-ও: “আমি জেনেবুঝে এই চুক্তি মানছি, তার অর্থের নিয়মসহ।” Rust আর Haskell আরেকটি বড় কাজ করে, যা C++ করে না — generic ফাংশনের শরীর যাচাই। trait-এ যা নেই, শরীরে তা ব্যবহার করলে সংজ্ঞার জায়গাতেই error, কোনো ডাক ছাড়াই। C++-এ শর্তের বাইরে কিছু ব্যবহার করলে ধরা পড়ে শুধু instantiation-এর সময়।
structural পথের লাভ: পুরোনো বা অন্যের টাইপ কোনো বদল ছাড়াই চুক্তিতে ঢোকে। C++-এর বিপুল পুরোনো কোড আর standard library-র শত শত টাইপের জন্য এটি জরুরি ছিল — প্রতিটি টাইপের জন্য ঘোষণা লিখতে হলে concepts কখনো গৃহীত হতো না। nominal ভাষাগুলোকে এই সমস্যার জন্য আলাদা নিয়ম বানাতে হয়: Rust-এ অন্যের টাইপের জন্য অন্যের trait বাস্তবায়ন করা যায় না (orphan rule), তাই প্রায়ই একটি মোড়ক টাইপ লিখতে হয়। আর structural পথে template-এর শরীর যাচাই না থাকায় C++ নমনীয় থাকে — শরীরে debug-এর জন্য একটি লাইন যোগ করলেই concept বদলাতে হয় না।
C++ তাই দুই দিকের কিছুটা নিয়েছে: structural যাচাই, আর প্রয়োজনে হাতে nominal-এর স্বাদ — static_assert(Shape<Circle>) একটি স্ব-ঘোষণা, আর কিছু library একটি বিশেষ সদস্য বা trait-ভ্যারিয়েবল চায় (যেমন ranges-এ enable_borrowed_range), যাতে টাইপটি ইচ্ছা করে “আমি এটি মানি” বলে।
মূল উপলব্ধি: structural typing পুরোনো টাইপকে সহজে জায়গা দেয়, nominal typing কাকতালীয় মিল আটকায় আর অর্থের দায় স্পষ্ট করে। C++ তার বিপুল পুরোনো কোডের কারণে structural বেছে নিয়েছে — সেই দামটি মনে রেখে concept-এর নাম আর অর্থ যত্ন করে লিখুন।
প্র ০৫compiler যদি এত কিছু যাচাই করতে পারে, তাহলে “< একটি সংগত ক্রম দেয়” — এই অর্থের নিয়মটি কেন যাচাই করতে পারে না? C++0x-এর axiom কী ছিল?
প্রথম কারণটি গণিতের। ১৯৫৩ সালে Henry Gordon Rice প্রমাণ করেন, একটি প্রোগ্রামের আচরণ সম্পর্কে যেকোনো অর্থপূর্ণ প্রশ্ন — “এই ফাংশন কি সবসময় সংগত ক্রম দেয়?” “এটি কি কখনো থামে?” — সাধারণভাবে কোনো algorithm দিয়ে নিশ্চিতভাবে উত্তর দেওয়া যায় না (Rice's theorem, halting problem-এর সম্প্রসারণ)। আপনার operator< একটি সাধারণ C++ ফাংশন; তাতে লুপ, শর্ত, global ভ্যারিয়েবল — সবই থাকতে পারে। “সব সম্ভাব্য a, b, c-র জন্য a < b আর b < c হলে a < c” প্রমাণ করা মানে সেই ফাংশনের সব আচরণ সম্পর্কে একটি উপপাদ্য প্রমাণ করা। কিছু বিশেষ ক্ষেত্রে সম্ভব, সাধারণভাবে নয়।
দ্বিতীয় কারণটি ব্যবহারিক। এমনকি যেখানে প্রমাণ সম্ভব, সেখানেও তা ধীর আর জটিল — প্রতিটি compile-এ একটি theorem prover চালানো কেউ চায় না। Coq, Lean বা Idris-এর মতো ভাষা প্রমাণ-যাচাই করে, কিন্তু সেখানে প্রমাণটি প্রোগ্রামারকেই লিখে দিতে হয়, আর সেটি সাধারণ কোড লেখার চেয়ে অনেক বেশি পরিশ্রমের।
C++0x-এর concepts-এ একটি মাঝামাঝি চেষ্টা ছিল: axiom. concept-এর ভেতরে লেখা যেত, যেমন “a == b হলে b == a”. compiler এগুলো যাচাই করত না; উদ্দেশ্য ছিল documentation, আর optimizer-কে অনুমতি দেওয়া — যেমন axiom বললে যোগ বিনিময়যোগ্য, তাহলে compiler ক্রম বদলে দ্রুত কোড বানাতে পারে। কিন্তু এখানেই বিপদ: axiom ভুল হলে optimizer ভুল কোড বানাবে, আর কোনো error আসবে না। C++0x concepts বাদ পড়ার সাথে axiom-ও বাদ পড়ে; Concepts Lite ইচ্ছা করেই এটি আনেনি।
C++20 তাই দায়িত্ব ভাগ করেছে স্পষ্টভাবে: standard-এর লেখায় প্রতিটি concept-এর semantic requirement আছে; standard library ধরে নেয় আপনার টাইপ সেগুলো model করে; আর না করলে আচরণের কোনো নিশ্চয়তা নেই, compiler-এর সতর্ক করার দায়ও নেই। অর্থ যাচাইয়ের বাস্তব হাতিয়ার তাই টেস্ট — বিশেষ করে property-based testing, যেখানে হাজারো এলোমেলো a, b, c দিয়ে পরীক্ষা করা হয় “a < b আর b < a একসাথে কখনো সত্য নয়”। sanitizer আর debug-mode library-ও কিছু ক্ষেত্রে ধরে — যেমন কিছু standard library-র debug সংস্করণ sort-এর সময় তুলনা ফাংশনের অসংগতি পরীক্ষা করে।
মূল উপলব্ধি: অর্থ যাচাই সাধারণভাবে অসম্ভব (Rice), আর সম্ভব ক্ষেত্রেও ব্যয়বহুল। তাই concept ব্যাকরণ যাচাই করে, আর অর্থের চুক্তি থাকে documentation, টেস্ট আর প্রোগ্রামারের সততায় — concept পাস করা মানে “সম্ভবত ঠিক”, প্রমাণিত নয়।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
সত্য না মিথ্যা: নিচের প্রতিটির জন্য
std::integral<T>কী হবে? (ক)int(খ)bool(গ)char(ঘ)double(ঙ)std::size_t(চ)const int.শুধু (ঘ) double মিথ্যা; বাকি সব সত্য। bool আর char পূর্ণসংখ্যার টাইপ — অনেকের কাছে চমক।
std::size_tএকটি unsigned পূর্ণসংখ্যার alias. আরstd::integralconst-কে উপেক্ষা করে (is_integralconst int-কেও integral ধরে)। শিক্ষা: “শুধু আসল সংখ্যা চাই” বোঝাতে bool আলাদা করে বাদ দিতে হয়, যেমনNumberconcept-এ!std::same_as<T, bool>. -
পুরোনো কোড নতুন রূপে: একটি পুরোনো ফাংশনের ঘোষণা:
template <typename F, typename = std::enable_if_t<std::is_invocable_r_v<bool, F, int>>> int count_matching(const std::vector<int>& v, F pred);— এটি কী শর্ত দিচ্ছে, আর C++20-এ কীভাবে লিখবেন?শর্তটি হলো: F-কে একটি int দিয়ে ডাকা যায়, আর ফল bool-এ রূপান্তর হয় — অর্থাৎ F একটি int-এর predicate. C++20-এ প্রায় একই অর্থে:
template <std::predicate<int> F>, তারপর একই ঘোষণা। অথবা abbreviated রূপে parameter-এstd::predicate<int> auto pred. খেয়াল করুন, type constraint-এ concept-এর প্রথম টাইপ (F) নিজে বসে যায়, তাই শুধু বাকি argument int লিখতে হয়। নতুন রূপটি শর্তের নামই বলে দেয় — predicate. -
কে Shape? shapes.cpp-এর
Shapeconcept নিন (area()double-এ রূপান্তরযোগ্য,name()string_view-তে রূপান্তরযোগ্য, দুটিই const object-এ)। নিচের কোনগুলো Shape? (ক)area()একটিfloatফেরত দেয়,name()একটিconst char*. (খ)area()একটিstd::stringফেরত দেয়। (গ) সব ঠিক, কিন্তুarea()const নয়। (ঘ)areaএকটি double ভ্যারিয়েবল, ফাংশন নয়।শুধু (ক) Shape: float থেকে double আর const char* থেকে string_view — দুটিই রূপান্তর হয়। (খ) ব্যর্থ: string থেকে double-এ রূপান্তর নেই, compound requirement মিথ্যা। (গ) ব্যর্থ: requires-এর parameter
const T&, আর const object-এ non-const member function ডাকা যায় না — const-correctness (পর্ব ২০) এখানে সরাসরি concept-এর শর্ত। (ঘ) ব্যর্থ:s.area()লেখা যায় না, কারণ double ভ্যারিয়েবলকে ডাকা যায় না। যদি concept-এsame_as<double>থাকত, (ক)-ও ব্যর্থ হতো — over-constraining-এর একটি ছোট নমুনা। -
নিজে লিখুন (কোড): একটি concept
Sizedলিখুন, যা চায়c.size()লেখা যায় আর ফলstd::size_t-এ রূপান্তরযোগ্য। তারপর একটি ফাংশনprint_size, যা শুধু Sized টাইপ নেয় আর আকার ছাপায়।std::vector<int>,std::stringআরintদিয়ে ডেকে দেখুন, আর সাথে একটিstatic_assertযোগ করুন।একটি সমাধান:
template <typename T> concept Sized = requires(const T& c) { { c.size() } -> std::convertible_to<std::size_t>; };, তারপরvoid print_size(const Sized auto& c) { std::println("{}", c.size()); }, আরstatic_assert(Sized<std::string> && !Sized<int>);. vector{1, 2, 3}দিলে 3, string"Dhaka"দিলে 5. int দিলে error আসবে ডাকার লাইনে, বলবেc.size()বৈধ নয়। বাড়তি চিন্তা: standard-এ এর কাছাকাছি একটি concept আছে —std::ranges::sized_range; আপনার চাহিদা যদি “আকার জানা range” হয়, সেটিই নিন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমC++20-এ শর্তযুক্ত function template-এর একটি ডাক compiler কীভাবে বিচার করে — ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ডাকের নাম দেখে একই নামের সব প্রার্থী ফাংশন ও template খুঁজে বের করে
- প্রতিটি template-এর জন্য argument দেখে T deduce করে
- deduce করা T বসিয়ে প্রতিটি প্রার্থীর concept-শর্ত হিসাব করে
- যাদের শর্ত মিথ্যা, তাদের প্রার্থী-তালিকা থেকে বাদ দেয়
- বাকিদের মধ্যে সেরাটি বাছে; সমান হলে বেশি নির্দিষ্ট শর্তেরটি
- বিজয়ী template-এর শরীর instantiate করে
C++20-এ template <std::integral T> T half(T x); ঘোষিত আছে। half(2.5) লিখলে কী হয়?
- 2.5 কে int-এ রূপান্তর করে half<int> ডাকা হয়, ফল 1
- Compile error: শর্ত মেলেনি, ডাকার জায়গাতেই
- প্রোগ্রাম চলার সময় একটি exception ছোড়া হয়
- Compile error, কিন্তু template-এর শরীরের ভেতরের লাইন থেকে
C++-এ template <typename T> concept Small = requires { sizeof(T) <= 4; }; সংজ্ঞায়িত। Small<double>-এর মান কী (double = 8 byte ধরে)?
- false, কারণ 8 <= 4 মিথ্যা
- Compile error, কারণ শর্তটি ভুলভাবে লেখা
- true, কারণ শুধু expression-টির বৈধতা যাচাই হয়, মান নয়
- platform-এর ওপর নির্ভর করে true বা false
C++20-এ void f(std::integral auto); আর void f(std::signed_integral auto); দুটি overload আছে। f(-3) কোনটি ডাকে?
- signed_integral-এর overload, কারণ এর শর্ত বেশি নির্দিষ্ট
- integral-এর overload, কারণ এটি আগে ঘোষিত
- Compile error: ambiguous call
- কোনোটিই নয় — int integral নয়
C++-এ concept দিয়ে লেখা interface আর abstract class দিয়ে লেখা interface-এর তুলনায় কোনটি সঠিক?
- concept মানতে টাইপকে একটি বিশেষ base class থেকে inherit করতে হয়
- concept প্রতিটি object-এ একটি vtable pointer যোগ করে
- concept দিয়ে এক vector-এ নানা টাইপের object রাখা যায়
- concept-এ ডাক সরাসরি হয়, vtable লাগে না, আর inheritance-এরও দরকার নেই
C++-এ একটি টাইপের ছয়টি তুলনা অপারেটরই সংজ্ঞায়িত, কিন্তু operator< সবসময় true ফেরত দেয়। std::totally_ordered এই টাইপের জন্য কী হবে?
- false, কারণ compiler বুঝতে পারে ক্রমটি অসংগত
- true — ব্যাকরণ মেলে, যদিও অর্থের নিয়ম ভাঙা
- Compile error
- চলার সময় প্রথম তুলনার পরে ঠিক হয়
C++ ভাষায় concepts প্রথম কোন standard-এ যুক্ত হয়?
- C++11
- C++17
- C++20
- C++23
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- concept = নাম দেওয়া শর্ত: compile-এর সময় true বা false; ভুল টাইপে error আসে ডাকার লাইনে, ছোট হয়ে, শরীর instantiate হওয়ার আগেই।
- আগে standard:
std::integral,std::floating_point,std::same_as,std::convertible_to,std::totally_ordered,std::copyable,std::invocable,std::predicate, আর ranges-এর concept — মনে রাখুন bool আর char-ও integral. - চার রীতি, চার requirement:
template <C T>, requires clause, trailing requires,C auto; requires expression-এ simple, type, compound আর nested — nested-এrequiresশব্দটি ভুলবেন না। - বেশি নির্দিষ্ট শর্ত জেতে: subsumption কাজ করে শুধু তখন, যখন বড় concept ছোট concept-এর নাম দিয়ে গড়া। concept structural, vtable নেই; নানা টাইপ এক তালিকায় রাখতে virtual বা variant.
- ব্যাকরণ, অর্থ নয়: concept যাচাই করে কী লেখা যায়, ঠিক কাজ করে কি না নয়। ক্ষমতার নামে নাম দিন, বেশি শর্ত দেবেন না, আর অর্থের নিয়ম টেস্টে রাখুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪১ · লাইব্রেরি ভ্রমণ — chrono, random, filesystemপরবর্তী পর্বstandard library-র রোজকার কাজের অংশগুলো এক নজরে।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ২৮ · function templateফিরে দেখুনdeduction, instantiation আর শর্তহীন template-এর লম্বা error — concept যে সমস্যার সমাধান।