এই পর্বে যা শিখবেন
- class template কী, আর একটি template থেকে compiler কীভাবে আলাদা আলাদা class তৈরি করে
- member function class-এর ভেতরে ও বাইরে লেখা; header-এ সংজ্ঞা রাখার কারণ
- CTAD, deduction guide, default template argument আর non-type parameter
- full ও partial specialization — compiler কোন সংস্করণ কেন বেছে নেয়, আর কোথায় এটি বিপদ
- alias template, static member, variadic template, fold expression,
typename-এর ফাঁদ, আর AI-এর template-কোড যাচাই
১ · একই নকশা, ভিন্ন টাইপ — class template কেন
পর্ব ১৩ থেকে আপনি নিয়মিত std::vector ব্যবহার করছেন। কখনো std::vector<int>-এ নম্বর রেখেছেন, কখনো std::vector<std::string>-এ নাম। একবার ভাবুন: standard library-র লেখকেরা কি int-এর জন্য একটি vector, string-এর জন্য আরেকটি, আর আপনার নিজের Student টাইপের জন্য আরও একটি আলাদা করে লিখে রেখেছেন? অসম্ভব — আপনার Student টাইপ তাঁরা চেনেনই না। তাঁরা লিখেছেন একটিমাত্র নকশা, যেখানে উপাদানের টাইপের জায়গাটি ফাঁকা। আপনি <int> লিখে সেই ফাঁকা জায়গা পূরণ করেন, আর compiler তখন int-এর জন্য একটি পুরো class তৈরি করে নেয়।
পর্ব ২৮-এ আপনি একই ধারণা ফাংশনের জন্য দেখেছেন — largest<T>-এর মতো function template, compiler-এর টাইপ বুঝে নেওয়া (deduction), আর প্রতিটি টাইপের জন্য আলাদা সংস্করণ তৈরি হওয়া (instantiation). আজ সেই একই যন্ত্র — template — class-এ। যে template থেকে পুরো একটি class তৈরি হয়, তাকে বলে class template. std::vector, std::array, std::pair, std::optional, std::map — standard library-র প্রায় প্রতিটি container আর সহায়ক টাইপ আসলে class template. এই পর্ব শেষে আপনি নিজের এমন টাইপ লিখতে পারবেন।
একটি garments কারখানার কাটিং টেবিলের কথা ভাবুন। একটি শার্টের প্যাটার্ন — কাগজের ছাঁচ — একবারই তৈরি হয়। সেই একই ছাঁচ দিয়ে সুতি কাপড় কাটলে সুতির শার্ট, ডেনিম কাটলে ডেনিমের শার্ট, লিনেন কাটলে লিনেনের। ছাঁচ নিজে কোনো শার্ট নয়; সেটি পরা যায় না, বিক্রিও করা যায় না। আর তৈরি হওয়া সুতির শার্ট আর ডেনিমের শার্ট দুটি আলাদা পণ্য, যদিও নকশা হুবহু এক। class template হলো ছাঁচ, টাইপ হলো কাপড়, আর Stack<int> হলো কেটে-সেলাই করা একটি আসল শার্ট।
একটি বাস্তব উদাহরণ নিন। আপনি একটি দোকানের স্টক রাখার সফটওয়্যার লিখছেন — মোবাইল ফোনের দোকানেও চলবে, শাড়ির দোকানেও। দুই জায়গায় কাজ একই: পণ্য যোগ করা, গোনা, খোঁজা। শুধু “পণ্য” জিনিসটি আলাদা। template ছাড়া আপনাকে PhoneInventory আর SareeInventory নামে দুটি প্রায় হুবহু class লিখতে হতো — আর একটিতে বাগ সারালে অন্যটিতে ভুলে যাওয়ার ঝুঁকি থাকত। template দিয়ে একবারই লেখা যায়:
template <typename Item> // Item = যেকোনো পণ্যের টাইপ
class Inventory {
std::vector<Item> items_;
public:
void add(const Item& it) { items_.push_back(it); }
std::size_t count() const { return items_.size(); }
};
Inventory<Phone> phones; // মোবাইলের দোকান
Inventory<Saree> sarees; // শাড়ির দোকানপ্রথম লাইনের template <typename Item> বলে: “এর পরের class-টি একটি নকশা, আর এতে Item নামে একটি টাইপের ফাঁকা জায়গা আছে।” Item-কে বলে template parameter. Inventory<Phone> লিখলে Phone হলো template argument — ফাঁকা জায়গায় যা বসানো হলো। পর্ব ২৮-এর মতোই typename-এর বদলে class লেখাও চলে; দুটির অর্থ এখানে হুবহু এক।
Inventory নিজে কোনো টাইপ নয় — এর object বানানো যায় না। Inventory<Phone> একটি টাইপ, Inventory<Saree> আরেকটি। এদের মধ্যে কোনো inheritance নেই, কোনো সাধারণ base নেই; একটিকে আরেকটিতে assign করা যায় না, একটি std::vector-এ দুটিকে একসাথেও রাখা যায় না। একই ছাঁচ, কিন্তু সম্পূর্ণ আলাদা পণ্য। পর্ব ২৫-এর virtual-এর সাথে এটাই মূল পার্থক্য: সেখানে টাইপ ঠিক হয় চলার সময়, এখানে compile-এর সময় — একে বলে static polymorphism.
২ · প্রথম class template — নির্দিষ্ট মাপের Stack<T, N>
stack হলো এমন একটি পাত্র, যেখানে শেষে যা রাখা হয়, সবার আগে সেটিই বের হয় — হোটেলের রান্নাঘরে ধোয়া প্লেটের স্তূপের মতো। আমরা একটি ছোট stack বানাব, যার সর্বোচ্চ ধারণক্ষমতা আগে থেকেই ঠিক করা, তাই heap-এর মেমরি লাগবে না। ভেতরে থাকবে একটি std::array. দুটি template parameter: উপাদানের টাইপ T, আর ধারণক্ষমতা N — একটি সংখ্যা। একটি member function লেখা হয়েছে class-এর ভেতরে, আরেকটি বাইরে, যাতে দুটি নিয়মই চোখে পড়ে:
#include <array>
#include <print>
#include <string>
template <typename T, std::size_t N = 4> // N has a default value
class Stack {
std::array<T, N> data_{};
std::size_t size_ = 0;
public:
void push(const T& v) { data_[size_++] = v; } // defined inside the class
T pop(); // only declared here
bool empty() const { return size_ == 0; }
};
template <typename T, std::size_t N> // defined outside the class
T Stack<T, N>::pop() { return data_[--size_]; }
int main() {
Stack<int> a; // means Stack<int, 4>
for (int i = 1; i <= 4; ++i) a.push(i * 10);
Stack<std::string, 2> b;
b.push("bKash"); b.push("Nagad");
while (!a.empty()) std::print("{} ", a.pop());
while (!b.empty()) std::print("{} ", b.pop());
}আউটপুট এক লাইনে: 40 30 20 10 Nagad bKash. যা রাখা হয়েছে, বের হয়েছে উল্টো ক্রমে — stack-এর নিয়ম। খেয়াল করুন, একই নকশা int-এর সাথেও চলেছে, string-এর সাথেও। Stack<int> লিখলে N-এর জায়গায় default মান 4 বসে। ভেতরের push-এর সামনে কিছু লিখতে হয়নি, কারণ class-এর ভেতরে T আর N আগে থেকেই পরিচিত। বাইরের pop-এর জন্য পুরো template লাইন আবার লিখতে হয়েছে, আর নামের আগে Stack<T, N>::. একটি সতর্কতা: এই ছোট সংস্করণে ভরা stack-এ push বা খালি stack-এ pop করলে array-এর সীমার বাইরে যাওয়া হয় — undefined behavior. আসল কোডে সেই পরীক্ষা যোগ করতে হবে (অনুশীলনে দেখবেন)।
বাইরে member function লেখার ব্যাকরণ
বাইরের সংজ্ঞাটি ভেঙে পড়ুন: template <typename T, std::size_t N> — “এটিও একটি নকশার অংশ, এই দুটি parameter-সহ।” তারপর ফেরত-টাইপ T. তারপর Stack<T, N>::pop — “Stack-এর এই নির্দিষ্ট রূপের pop.” শুধু Stack::pop লিখলে চলবে না, কারণ Stack একা কোনো class নয়, একটি নকশার নাম। আর default মান (= 4) বাইরের সংজ্ঞায় আবার লেখা যায় না — সেটি শুধু প্রথম ঘোষণায় থাকে। class-এর ভেতরে অবশ্য শুধু Stack লিখলেই compiler বোঝে Stack<T, N>, যেমন একটি copy constructor Stack(const Stack&) লিখতে চাইলে।
প্রতিটি টাইপ, একটি নতুন class
উপরের main-এ compiler আসলে কী করল, ধাপে ধাপে দেখুন। প্রতিটি ধাপে আগে নিজে অনুমান করুন:
1template <typename T, std::size_t N = 4> class Stack {...};2Stack<int> a;3Stack<std::string, 2> b;4a.push(10); b.pop();5a = b; // ?
| এই লাইনে T, N | |
| তৈরি হওয়া class | |
| ভেতরের data_ | |
| কোড তৈরি হলো |
- শুধু নকশা। template-এর সংজ্ঞা পড়ে compiler কোনো মেশিন কোড তৈরি করে না। সে শুধু নকশাটি মনে রাখে, আর ব্যাকরণের মোটা ভুলগুলো দেখে নেয়।
Tকী হবে, তা না জানলেdata_কত বাইট, তাও জানা যায় না। - প্রথম ব্যবহার — Stack<int>. এবার compiler ফাঁকা জায়গা পূরণ করে:
T = int, আর N লেখা নেই বলে default 4. নকশা থেকে একটি আসল class তৈরি হলো, যেন আপনি নিজেইclass Stack_int_4লিখেছেন। এই কাজকে বলে instantiation. - দ্বিতীয় ব্যবহার — আরেকটি class.
Stack<std::string, 2>দেখে compiler একই নকশা থেকে আরেকটি পুরো class বানায়। এখন প্রোগ্রামে দুটি সম্পূর্ণ আলাদা টাইপ আছে, যাদের ভেতরের array, মাপ, কোড — সব আলাদা। পরে আবার কোথাওStack<int>লিখলে নতুন class তৈরি হয় না; আগেরটিই ব্যবহার হয়। - যা ডাকা হয়, শুধু তাই তৈরি। class-এর member function-গুলোর কোড তৈরি হয় তখনই, যখন সেগুলো সত্যিই ডাকা হয়। এখানে
Stack<int>-এর push আরStack<string, 2>-এর pop. এই “অলস” নিয়মের একটি বড় সুবিধা: যে টাইপ কপি করা যায় না, তার জন্যওStackবানানো যায় — যতক্ষণ না আপনি কপি-নির্ভর ফাংশনটি ডাকছেন। - দুটি আলাদা টাইপ, তাই error.
a = bলিখলে compiler দেখে, বাঁ দিকেStack<int>, ডান দিকেStack<std::string, 2>— দুটির মধ্যে কোনো সম্পর্কই নেই। এমনকিStack<int, 4>আরStack<int, 5>-ও আলাদা টাইপ। একই ছাঁচের দুটি শার্ট, কিন্তু একটির কাপড় আরেকটিতে জোড়া লাগানো যায় না।
৩ · টাইপ নিজে বুঝে নেওয়া — CTAD আর default template argument
পর্ব ২৮-এ দেখেছেন, function template-এ argument দেখে compiler নিজেই T বুঝে নেয় — largest(3, 7) লিখলেই চলে, largest<int>(3, 7) লিখতে হয় না। কিন্তু C++14 পর্যন্ত class template-এ এই সুবিধা ছিল না। std::pair বানাতে লিখতে হতো std::pair<int, double> p(1, 2.5);, অথবা টাইপ বুঝে নেওয়ার জন্য একটি সহায়ক ফাংশন ডাকতে হতো — std::make_pair(1, 2.5). C++17 থেকে class-এর ক্ষেত্রেও compiler constructor-এর argument দেখে template argument বুঝে নেয়। একে বলে CTAD — class template argument deduction.
std::pair p{1, 2.5}; // C++17: std::pair<int, double>
std::vector v{3, 1, 4}; // std::vector<int>
std::array a{1, 2, 3}; // std::array<int, 3> — N-ও বুঝে নেয়
std::optional o{42}; // std::optional<int>এটি শুধু কম টাইপ করার সুবিধা নয়। লম্বা টাইপ বারবার লিখলে ভুলের সুযোগ বাড়ে, আর কোড পড়তে কষ্ট হয়। তবে CTAD কীভাবে সিদ্ধান্ত নেয়, তা জানা জরুরি — কারণ মাঝে মাঝে সে আপনার প্রত্যাশার চেয়ে আলাদা টাইপ বেছে নেয়। ধাপে ধাপে দেখুন:
pair-এর একটি constructor: pair(const T1& x, const T2& y)
argument: 1 → int ⇒ T1 = int
2.5 → double ⇒ T2 = double
ফল: std::pair<int, double> p{1, 2.5};
{3, 1, 4} → std::initializer_list<int>
vector(std::initializer_list<T>) ⇒ T = int
ফল: std::vector<int> (৩টি উপাদান)
একইভাবে std::array a{1, 2, 3} → std::array<int, 3>
template <typename T>
struct Box { T value; Box(T v) : value(v) {} };
Box b{"bKash"};
// "bKash" হলো const char[6]; by-value প্যারামিটারে
// array ভেঙে pointer হয় ⇒ T = const char*
// ফল: Box<const char*> — std::string নয়!
Box(const char*) -> Box<std::string>; // class-এর বাইরে, একই namespace-এ
Box b{"bKash"}; // এবার Box<std::string>
Box n{7}; // guide খাটে না ⇒ আগের মতো Box<int>
- constructor-ই সূত্র। compiler class-এর constructor-গুলোকে কল্পনায় function template বানিয়ে নেয়, তারপর পর্ব ২৮-এর deduction-এর নিয়মে argument মেলায়।
1হলো int,2.5হলো double — তাইstd::pair<int, double>. কোনো জাদু নেই; function template-এর পুরোনো নিয়মই। - brace দিলে initializer list.
{3, 1, 4}দেখে compilerinitializer_list<int>নেওয়া constructor বেছে নেয়, তাইT = int. standard library কিছু টাইপের জন্য নিজে বাড়তি নিয়ম লিখে রেখেছে — যেমনstd::array-এর জন্য, যাতে উপাদানের সংখ্যা থেকেN-ও বোঝা যায়। - নিজের টাইপে চমক। string literal আসলে একটি
const chararray. by-value প্যারামিটারে array ভেঙে pointer হয়ে যায় (পর্ব ১৫-এর array decay), তাইT = const char*. Box-এর ভেতরে এখন একটি string নয়, একটি pointer — literal-টি প্রোগ্রামজুড়ে বেঁচে থাকে বলে এখানে বিপদ নেই, কিন্তু একটি অস্থায়ীcharbuffer হলে dangling pointer হতে পারত। - deduction guide দিয়ে ঠিক করা। class-এর বাইরে একটি লাইন লিখে compiler-কে বলে দেওয়া যায়: “const char* দেখলে Box<std::string> বানাও।” একে বলে deduction guide. standard library নিজেও এমন guide ব্যবহার করে — যেমন দুটি iterator থেকে vector বানালে উপাদানের টাইপ বুঝে নেওয়ার জন্য। সন্দেহ হলে টাইপ নিজে লিখে দিন — CTAD বাধ্যতামূলক নয়।
Default template argument — সাধারণ পছন্দ আগে থেকেই দেওয়া
ফাংশনের default argument-এর মতো template parameter-এরও default মান দেওয়া যায় — উপরের Stack-এ N = 4 যেমন। একে বলে default template argument. টাইপ parameter-এরও default হয়। আপনি প্রতিদিন না জেনেই এটি ব্যবহার করছেন: standard-এ std::vector-এর ঘোষণা আসলে এরকম —
template <class T, class Allocator = std::allocator<T>>
class vector; // মেমরি কোথা থেকে আসবে — default: সাধারণ heap
template <class T>
class optional; // C++17: হয় একটি T, নয়তো কিছুই নাstd::vector<int> লিখলে দ্বিতীয় argument নিজে বসে যায়। খুব বিশেষ ক্ষেত্রে — যেমন embedded সিস্টেমে নির্দিষ্ট মেমরি-এলাকা থেকে জায়গা নিতে — নিজের allocator দেওয়া যায়, বাকি সবাই সেটি কখনো দেখেনই না। ভালো default মানে: সাধারণ ব্যবহারকারীর কাছে সরল, বিশেষজ্ঞের কাছে নমনীয়।
এবার নিজের একটি উদাহরণ। কোনো কাজ সফল হলে একটি মান, ব্যর্থ হলে একটি ভুলের বিবরণ ফেরত দিতে চান — যেমন একটি দোকানের অর্ডার-খোঁজা ফাংশন। একটি ছোট Result-ধাঁচের জোড়া বানানো যায়, যেখানে ভুলের টাইপ সাধারণত string:
template <typename Value, typename Error = std::string>
struct Result {
Value value{};
Error error{}; // খালি = কোনো ভুল নেই
bool ok() const { return error == Error{}; }
};
Result<int> found{42}; // Error = std::string (default)
Result<int> missing{0, "order not found"};
Result<double, int> http{0.0, 404}; // নিজের পছন্দের Error টাইপএটি শেখার জন্য একটি সরল নকশা — এখানে “ভুল নেই” বোঝাতে খালি মান ব্যবহার করা হয়েছে, যা সব Error টাইপের জন্য যুক্তিসংগত নয়। আসল কাজের জন্য C++23-এ standard library-তেই এসেছে std::expected<T, E>, যা পর্ব ৩৭-এ দেখবেন। কিন্তু সেটিও ঠিক এই রকমই একটি class template.
৪ · Non-type parameter — sensor-এর জন্য RingBuffer<T, N>
template parameter সবসময় টাইপ হয় না। Stack-এর N একটি সংখ্যা — একে বলে non-type template parameter. পর্ব ২৮-এ ফাংশনের জন্য এটি দেখেছেন; class-এ এর আসল শক্তি দেখা যায়, কারণ মাপটি টাইপেরই অংশ হয়ে যায়। std::array<int, 5> আর std::array<int, 6> দুটি আলাদা টাইপ — তাই ভুল মাপের array ভুল ফাংশনে পাঠালে compiler তখনই ধরে।
একটি বাস্তব প্রয়োজন: একটি কারখানার তাপমাত্রা sensor প্রতি সেকেন্ডে একটি রিডিং দেয়, আর আপনার কাছে দরকার শুধু শেষ ৬০টি রিডিং — পুরোনোগুলো বাদ যাবে। এর জন্য আদর্শ গঠন হলো ring buffer: নির্দিষ্ট মাপের একটি array, যেখানে শেষ ঘরের পরে লেখা আবার প্রথম ঘরে ফিরে যায়, আর সবচেয়ে পুরোনো রিডিংয়ের ওপর নতুনটি বসে। মেট্রো রেলের ঘূর্ণায়মান টার্নস্টাইলের মতো — ঘর সীমিত, ঘুরে ঘুরে ব্যবহার হয়।
template <typename T, std::size_t N>
class RingBuffer {
static_assert(N > 0, "RingBuffer needs at least one slot");
std::array<T, N> buf_{};
std::size_t head_ = 0, count_ = 0;
public:
void add(const T& x) {
buf_[head_] = x; // সবচেয়ে পুরোনোটির ওপর লেখা
head_ = (head_ + 1) % N; // শেষের পরে আবার ০
if (count_ < N) ++count_;
}
std::size_t size() const { return count_; }
static constexpr std::size_t capacity() { return N; }
};
RingBuffer<double, 60> temperature; // শেষ ৬০টি তাপমাত্রা
RingBuffer<int, 8> pulse; // শেষ ৮টি হৃৎস্পন্দনএখানে তিনটি জিনিস খেয়াল করার মতো। এক, N compile-এর সময়ই জানা, তাই মেমরি পুরোপুরি object-এর ভেতরে — কোনো new নেই, heap নেই। ছোট microcontroller-এ, যেখানে heap ব্যবহার এড়ানো হয়, এটা বড় সুবিধা। দুই, static_assert compile-এর সময়েই একটি নিয়ম যাচাই করে: RingBuffer<int, 0> লিখলে প্রোগ্রাম compile-ই হবে না — নইলে % 0 চলার সময় undefined behavior ঘটাত। তিন, capacity() একটি static constexpr ফাংশন: object ছাড়াই RingBuffer<int, 8>::capacity() ডাকা যায়, কারণ উত্তর টাইপের মধ্যেই লেখা।
কোন মান non-type parameter হতে পারে? সবচেয়ে প্রচলিত হলো পূর্ণসংখ্যা (int, std::size_t), bool, enum, আর pointer. শর্ত একটাই: মানটি compile-এর সময় জানা থাকতে হবে। int n; std::cin >> n; RingBuffer<int, n> r; চলবে না — ব্যবহারকারী কত দেবেন, compiler তা জানে না। C++20 থেকে double আর কিছু সরল struct-ও non-type parameter হতে পারে, তবে শুরুতে সংখ্যাই যথেষ্ট। মাপ যদি চলার সময় ঠিক হয়, তাহলে template নয়, std::vector ব্যবহার করুন।
৫ · Specialization — বিশেষ টাইপের জন্য বিশেষ সংস্করণ
কখনো একটি নকশা প্রায় সব টাইপের জন্য ঠিক, কিন্তু একটি বা এক দল টাইপের জন্য আলাদা কিছু দরকার। ধরুন Storage<T> নামে একটি পাত্র অনেকগুলো মান রাখে। bool-এর ক্ষেত্রে প্রতিটি মান রাখতে একটি পুরো বাইট (৮ বিট) খরচ করা অপচয় — একটি বিটেই হয়ে যায়। আবার pointer রাখলে হয়তো আপনি জানতে চান, pointer-গুলো null কি না। এর জন্য C++ দেয় template specialization: নির্দিষ্ট argument-এর জন্য নকশার একটি সম্পূর্ণ আলাদা সংস্করণ লেখা।
Primary template
মূল, সাধারণ নকশা: template <typename T> struct Storage. কোনো বিশেষ সংস্করণ না মিললে এটিই ব্যবহার হয়।
Full specialization
একটি নির্দিষ্ট টাইপের জন্য: template <> struct Storage<bool>. ফাঁকা <> মানে আর কোনো parameter বাকি নেই।
Partial specialization
এক দল টাইপের জন্য: template <typename T> struct Storage<T*> — যেকোনো pointer. এখনো একটি parameter বাকি আছে।
মূল সংস্করণটিকে বলে primary template. ঠিক একটি টাইপের জন্য লেখা সংস্করণকে বলে full (explicit) specialization, আর একটি ধাঁচের জন্য লেখাকে partial specialization. ছোট একটি প্রোগ্রামে দেখুন, compiler কোন অনুরোধে কোন সংস্করণ বেছে নেয়:
#include <print>
template <typename T> // primary template
struct Storage { static constexpr const char* kind = "generic, 1 slot per value"; };
template <typename T> // partial specialization: any pointer
struct Storage<T*> { static constexpr const char* kind = "pointer, null-checked"; };
template <> // full specialization: exactly bool
struct Storage<bool> { static constexpr const char* kind = "bool, packed bits"; };
int main() {
std::println("Storage<int> : {}", Storage<int>::kind);
std::println("Storage<double*> : {}", Storage<double*>::kind);
std::println("Storage<bool> : {}", Storage<bool>::kind);
std::println("Storage<bool*> : {}", Storage<bool*>::kind);
}আউটপুট চার লাইন: Storage<int> : generic, 1 slot per value, Storage<double*> : pointer, null-checked, Storage<bool> : bool, packed bits, আর Storage<bool*> : pointer, null-checked. int-এর জন্য কোনো বিশেষ সংস্করণ নেই, তাই primary. double* একটি pointer, তাই partial. bool হুবহু মেলে full specialization-এর সাথে। আর শেষ লাইনটি ফাঁদ: bool* আর bool এক টাইপ নয় — এটি একটি pointer, তাই pointer-এর সংস্করণ। আসল কোডে প্রতিটি সংস্করণের ভেতরে নিজের ডেটা আর ফাংশন থাকত; এখানে শুধু একটি লেবেল রাখা হয়েছে, যাতে বাছাইয়ের নিয়মটি পরিষ্কার দেখা যায়।
Storage<bool*>-এর জন্য compiler ঠিক কীভাবে ভাবল, ধাপে ধাপে দেখুন:
- অনুরোধ
Storage<bool*>কোন নকশা? - full
Storage<bool>মেলে না - partial
Storage<T*>মেলে, T = bool - primary
Storage<T>মেলে, কম নির্দিষ্ট - বিজয়ী
Storage<T*>সবচেয়ে নির্দিষ্ট
template <typename T> struct Storage { ... }; // primary
template <typename T> struct Storage<T*> { ... }; // partial
template <> struct Storage<bool> { ... }; // full
অনুরোধ: Storage<bool*>
চাওয়া হয়েছে: bool* আছে: bool bool* ≠ bool ⇒ এই সংস্করণ বাদ
ধাঁচ: T* চাওয়া হয়েছে: bool* T = bool ধরলে T* = bool* ⇒ মেলে
ধাঁচ: T (যেকোনো টাইপ) T = bool* ধরলে মেলে — কিন্তু এটি সবচেয়ে সাধারণ নকশা। অন্য কোনো specialization মিললে primary পেছনে পড়ে।
T* শুধু pointer-এর সাথে মেলে T সবকিছুর সাথে মেলে ⇒ T* বেশি নির্দিষ্ট ⇒ Storage<T*> with T = bool আউটপুট: Storage<bool*> : pointer, null-checked
- তিন প্রার্থী।
Storageনামে তিনটি নকশা আছে। compiler প্রতিটির সাথে অনুরোধটি মিলিয়ে দেখবে। লক্ষ করুন, specialization লেখার আগে primary template-এর ঘোষণা থাকতেই হবে — specialization হলো কোনো মূল নকশার “বিশেষ পরিস্থিতির নিয়ম”। - full specialization বাদ। full specialization শুধু হুবহু একই argument-এ খাটে।
bool*আরboolআলাদা টাইপ — যেমন “এক বস্তা চাল” আর “চালের বস্তার ঠিকানা লেখা চিরকুট” আলাদা জিনিস। তাই এটি বাদ। - partial মেলে। partial specialization একটি ধাঁচ: “যেকোনো কিছুর pointer”.
T = boolধরলে ধাঁচ হুবহু মিলে যায়। compiler এখানে পর্ব ২৮-এর deduction-এর মতোই কাজ করে, শুধু function argument-এর বদলে template argument-এর সাথে। - primary-ও মেলে। primary template যেকোনো টাইপ নেয়, তাই এটিও প্রার্থী থাকে। কিন্তু অন্য কোনো মানানসই specialization থাকলে primary কখনো জেতে না — সে শুধু শেষ ভরসা।
- সবচেয়ে নির্দিষ্টটি জেতে। দুই মানানসই প্রার্থীর মধ্যে
T*বেশি নির্দিষ্ট — কারণ প্রতিটি pointer “যেকোনো টাইপ”, কিন্তু প্রতিটি টাইপ pointer নয়। তাই partial সংস্করণ জিতল। দুটি partial specialization সমান নির্দিষ্ট হলে compiler কাউকে বাছতে পারে না, আর ambiguity-র error দেয়।
দুটি জরুরি নিয়ম
প্রথম নিয়ম: specialization কিছুই “উত্তরাধিকারে” পায় না। Storage<bool> একটি সম্পূর্ণ নতুন class. primary-তে যত member function আছে, তার একটিও এখানে আপনা-আপনি আসে না — সব আবার লিখতে হয়। তাই একটি ফাংশন বাদ পড়লে Storage<int>-এর সাথে যে generic কোড চলত, Storage<bool>-এ সেটি হঠাৎ compile হবে না। আরও খারাপ: ফাংশনের নাম একই রেখে আচরণ বদলে দিলে compile হবে, কিন্তু ব্যবহারকারীর প্রত্যাশা ভাঙবে। দ্বিতীয় নিয়ম: partial specialization শুধু class template-এর (আর variable template-এর) জন্য। function template-কে partially specialize করা যায় না; সেখানে overloading ব্যবহার করুন (পর্ব ২৮)। standard library-র <type_traits>-এর অনেক type trait — যেমন একটি টাইপ pointer কি না জানার std::is_pointer — ঠিক এই Storage<T*>-এর কৌশলেই লেখা।
সতর্কবার্তা হিসেবে std::vector<bool>
C++98-এর standard library ঠিক এই Storage<bool>-এর বুদ্ধিটি কাজে লাগিয়েছিল: std::vector<bool> একটি full specialization, যা প্রতিটি bool একটি বিটে রাখে — মেমরিতে আট গুণ সাশ্রয়। কিন্তু একটি বিটের কোনো নিজস্ব ঠিকানা নেই, তাই v[0] একটি আসল bool& ফেরত দিতে পারে না। ফেরত দেয় একটি proxy object — দেখতে bool-এর মতো, আসলে একটি ছোট সহকারী object, যে জানে কোন বাইটের কোন বিট বদলাতে হবে।
std::vector<int> vi{1, 2}; int& ri = vi[0]; // ঠিক
std::vector<bool> vb{true}; bool& rb = vb[0]; // compile error: bool& নয়, proxy
auto x = vb[0]; // x একটি proxy — vb বদলালে x-ও বদলায়!
bool* p = &vb[0]; // compile error: বিটের ঠিকানা নেইঅর্থাৎ যে generic কোড যেকোনো std::vector<T>-এর সাথে কাজ করে বলে লেখা, T = bool হলে সেটি ভেঙে যেতে পারে। একটি container-এর যে মৌলিক প্রতিশ্রুতি — “v[i] দিলে উপাদানের আসল reference পাবেন” — specialization সেটিই ভেঙেছে। এ কারণে অনেক বিশেষজ্ঞ একে standard library-র একটি অনুশোচনার নকশা বলেন (ভাবনার প্রশ্ন ১-এ বিস্তারিত)। শিক্ষাটা আপনার নিজের কোডের জন্য: specialization শুধু ভেতরের বাস্তবায়ন বদলাবে, বাইরের আচরণ আর প্রতিশ্রুতি নয়। সবচেয়ে বেশি দরকারি নিয়ম, অর্থাৎ invariant, সব সংস্করণে এক থাকতে হবে। বিটে bool রাখতে চাইলে আলাদা নামের টাইপ নিন — যেমন std::bitset<N>.
৬ · using, alias template, static member আর Rule of Zero
লম্বা টাইপের ছোট নাম
template-এর টাইপ দ্রুত লম্বা হয়ে যায়: std::vector<std::vector<int>>, বা RingBuffer<double, 60>. বারবার লিখলে কোড পড়া কঠিন হয়, আর মাপ বদলাতে চাইলে দশ জায়গায় বদলাতে হয়। using দিয়ে একটি টাইপের দ্বিতীয় নাম দেওয়া যায় — একে বলে type alias. আর using-এর সামনে template লাইন বসালে পাওয়া যায় alias template — এমন ডাকনাম, যার নিজেরই একটি ফাঁকা জায়গা আছে:
using Taka = long long; // type alias
using MinuteLog = RingBuffer<double, 60>; // মাপ বদলাতে হলে শুধু এখানে
template <typename T>
using Grid = std::vector<std::vector<T>>; // alias template
Grid<int> board(3, std::vector<int>(3)); // ৩×৩ বোর্ড
Grid<char> seats; // বাসের সিটের নকশা
template <typename T>
using Readings = RingBuffer<T, 60>; // টাইপ খোলা, মাপ স্থিরalias কোনো নতুন টাইপ তৈরি করে না — Taka আর long long হুবহু একই টাইপ, একটির জায়গায় অন্যটি নির্দ্বিধায় চলে। এটি শুধু পড়ার সুবিধা আর একটি জায়গায় সিদ্ধান্ত রাখার উপায়। C-এর typedef-ও alias বানায়, কিন্তু typedef-এর সামনে template বসানো যায় না; alias template C++11-এর using-এরই সুবিধা। আধুনিক কোডে তাই using-ই লিখুন।
class template-এর ভেতরেও alias খুব প্রচলিত। standard container-গুলো জানিয়ে দেয় তাদের উপাদানের টাইপ কী — std::vector<int>::value_type হলো int. আপনার Stack-এও লিখতে পারেন using value_type = T;. এতে অন্য generic কোড আপনার container-কে জিজ্ঞেস করতে পারে, “ভেতরে কী আছে?” — ঠিক এই প্রশ্নই ৮ নম্বর অংশের typename-এর ফাঁদে ফিরে আসবে।
static member — প্রতিটি instantiation-এর নিজের
পর্ব ২০-এ দেখেছেন, static member class-এর সব object ভাগ করে নেয় — পুরো প্রোগ্রামে একটিই কপি। class template-এ একটি সূক্ষ্ম কিন্তু যুক্তিসংগত পার্থক্য আছে: যেহেতু প্রতিটি instantiation একটি আলাদা class, প্রত্যেকের static member-ও আলাদা।
template <typename T>
struct Counter {
inline static int created = 0; // C++17: header-এই সংজ্ঞা
Counter() { ++created; }
};
Counter<int> a, b;
Counter<double> c;
// Counter<int>::created == 2, Counter<double>::created == 1Counter<int> আর Counter<double> দুটি আলাদা class, তাই তাদের created দুটি আলাদা ভ্যারিয়েবল — কেউ কারও হিসাব জানে না। ঢাকা আর চট্টগ্রামের দুটি শাখা-অফিস একই নিয়মের খাতা ব্যবহার করে, কিন্তু প্রত্যেকের খাতার পাতা আলাদা। এর মানে: সব টাইপ মিলিয়ে একটি সাধারণ গণনা চাইলে template-এর ভেতরের static member দিয়ে হবে না — একটি সাধারণ, non-template base class-এ রাখতে হবে, বা template-এর বাইরে। inline static (C++17) থাকায় এখন আর আলাদা .cpp ফাইলে static member-এর সংজ্ঞা লিখতে হয় না।
template আর Rule of Zero — একসাথে খুব ভালো কাজ করে
পর্ব ২২-এ শিখেছেন Rule of Zero: রিসোর্স রাখুন std::vector, std::string বা std::unique_ptr-এর মতো member-এ, আর নিজের class-এ destructor, copy বা move নিজে লিখবেন না। class template-এ এই নিয়ম আরও বেশি দামি। আমাদের Stack বা RingBuffer-এ কোনো special member function লেখা হয়নি, তবু প্রতিটি T-এর জন্য compiler ঠিক আচরণ তৈরি করে নেয়। T = std::string হলে কপিতে প্রতিটি string কপি হয়। আর T = std::unique_ptr<int> হলে? unique_ptr কপি করা যায় না, তাই Stack<std::unique_ptr<int>>-এর copy constructor নিজে থেকেই deleted হয়ে যায়, কিন্তু move ঠিকই কাজ করে। একটি অক্ষরও না লিখে আপনার template প্রতিটি টাইপের নিয়ম মেনে চলে।
উল্টোটা ভাবুন: আপনি যদি হাতে new T[N] করে নিজের destructor আর copy constructor লিখতেন, তাহলে প্রতিটি সম্ভাব্য T-এর জন্য সেগুলো ঠিক আছে কি না, আপনাকে নিজেকেই ভাবতে হতো — exception ছুড়লে কী হবে, move-only টাইপ এলে কী হবে, কপি মাঝপথে ব্যর্থ হলে কী হবে। standard library-র লেখকেরা ঠিক এই কাজে বছরের পর বছর ব্যয় করেছেন। তাঁদের সেই কাজ member হিসেবে ধার নিন, নিজে আবার লিখবেন না।
৭ · Variadic template আর fold expression — যত খুশি argument
এ পর্যন্ত প্রতিটি template-এর parameter-এর সংখ্যা ছিল নির্দিষ্ট — একটি T, বা T আর N. কিন্তু std::tuple যেকোনো সংখ্যক মান রাখে: std::tuple<int, std::string, double>, আবার std::tuple<char>. আর std::make_unique আপনার টাইপের constructor-এ যতগুলো argument দরকার, সবগুলো পৌঁছে দেয়। এটি সম্ভব হয় variadic template-এর কারণে — যে template যত খুশি template argument নিতে পারে। এটি এসেছে C++11-এ।
মূল যন্ত্রটির নাম parameter pack: তিনটি বিন্দু (...) দিয়ে লেখা একটি “ব্যাগ”, যার ভেতরে শূন্য বা তার বেশি টাইপ বা মান। typename... Args হলো টাইপের ব্যাগ, Args... args হলো মানের ব্যাগ। ব্যাগে কয়টি জিনিস আছে, জানা যায় sizeof...(Args) দিয়ে। আর ব্যাগ খুলে প্রতিটি জিনিসের ওপর একই কাজ করার সবচেয়ে সহজ উপায় C++17-এর fold expression:
template <typename... Args>
void print_all(const Args&... args) {
(std::print("{} ", args), ...); // fold expression
std::println("");
}
print_all(1, "two", 3.5); // 1 two 3.5
print_all("Dhaka"); // Dhaka(std::print("{} ", args), ...) লাইনের অর্থ: “ব্যাগের প্রতিটি args-এর জন্য বাঁ দিকের কাজটি করো, আর মাঝে comma operator বসিয়ে এগুলো জুড়ে দাও।” comma operator বাঁ থেকে ডানে ক্রমানুসারে চলে, তাই মানগুলো ঠিক ক্রমেই ছাপা হয়। compiler এই এক লাইনকে কীভাবে খুলে দেয়, দেখুন:
1template <typename... Args>2void print_all(const Args&... args) {3 (std::print("{} ", args), ...);4}5print_all(1, "two", 3.5);
| Args (টাইপের ব্যাগ) | |
| sizeof...(Args) | |
| খুলে লেখা লাইন | |
| এখন চলছে |
- ব্যাগ তৈরি।
print_all(1, "two", 3.5)দেখে compiler তিনটি argument-এর টাইপ বুঝে নেয়: int, const char* আর double.Argsব্যাগে এখন এই তিনটি টাইপ,argsব্যাগে তিনটি মান। এই তিন টাইপের জন্যprint_all-এর একটি আলাদা সংস্করণ তৈরি হয় — instantiation-এর পুরোনো নিয়মই। - লাইনটি খুলে যায়, প্রথম কাজ চলে। compile-এর সময়েই fold লাইনটি তিনটি আলাদা ডাকে খুলে যায়, মাঝে comma. কোনো লুপ নেই — চলার সময় কোড দেখতে ঠিক যেন আপনি নিজে তিনবার
std::printলিখেছেন। প্রথম ডাক ছাপে1. - দ্বিতীয় কাজ। comma operator নিশ্চিত করে, বাঁ দিকের কাজ শেষ হলে তবেই ডানেরটি শুরু। তাই এবার
two. লক্ষ করুন, তিনটি মানের টাইপ আলাদা হলেও প্রতিটির জন্য ঠিক format-এর নিয়ম বেছে নেওয়া হয়েছে compile-এর সময়েই। - শেষ কাজ। তৃতীয় ডাকে
3.5. ব্যাগ খালি হলে (print_all()) comma-র fold কিছুই না করে শেষ হয় — এটিও বৈধ। এই একই কৌশলে(args + ...)লিখলে যোগফল,(args && ...)লিখলে “সব সত্য কি না” পাওয়া যায়।
এখানেই থামুন — variadic template-এর আরও গভীর কৌশল (perfect forwarding, recursion দিয়ে ব্যাগ খোলা) মূলত লাইব্রেরি-লেখকদের কাজ। আপনার জন্য মূল উপলব্ধি হলো: std::tuple, std::make_unique, emplace_back, std::format — এগুলো কীভাবে “যত খুশি argument” নেয়, এখন আপনি তা জানেন।
৮ · ঘরোয়া ফাঁদ — typename, header আর দীর্ঘ error
dependent name আর typename শব্দ
ধরুন আপনি একটি ফাংশন লিখছেন, যা যেকোনো container-এর প্রথম উপাদান ছাপে। উপাদানের টাইপ জানতে container-কে জিজ্ঞেস করবেন — C::value_type:
template <typename C>
void show_first(const C& c) {
C::value_type x = c.front(); // error!
typename C::value_type y = c.front(); // ঠিক
std::println("{}", y);
}
// GCC: need 'typename' before 'C::value_type'
// because 'C' is a dependent scopeকেন? template-এর ভেতরে C কী হবে, compiler তা জানে না। C::value_type একটি টাইপও হতে পারে, আবার কোনো static ভ্যারিয়েবলও হতে পারে — C-এর ওপর নির্ভর করে। এমন নামকে বলে dependent name. নিয়ম হলো: অনিশ্চিত হলে compiler ধরে নেয় এটি টাইপ নয়, একটি মান। তাই টাইপ বোঝাতে চাইলে সামনে typename লিখে নিশ্চিত করতে হয়। মনে রাখার সহজ উপায়: template parameter-এর ভেতর থেকে :: দিয়ে কোনো টাইপ বের করলে সামনে typename. C++20 এই নিয়ম কিছু জায়গায় শিথিল করেছে (যেমন using-এর ডান দিকে), কিন্তু ফাংশনের ভেতরের ঘোষণায় আজও লাগে। আর বেশিরভাগ ক্ষেত্রে auto x = c.front(); লিখলে প্রশ্নটাই ওঠে না।
সংজ্ঞা header-এ — নইলে linker error
পর্ব ১১-এ শিখেছেন, সাধারণ class-এর ঘোষণা header file-এ আর সংজ্ঞা .cpp ফাইলে রাখা ভালো অভ্যাস। template-এ এই অভ্যাস মানলে বিপদ। কারণটা instantiation-এর নিয়মেই লুকানো: Stack<int>::pop-এর কোড তৈরি করতে compiler-এর হাতে pop-এর পুরো সংজ্ঞা আর T = int — দুটিই একসাথে থাকা চাই। stack.cpp compile হওয়ার সময় সংজ্ঞা আছে, কিন্তু কেউ সেখানে Stack<int> চায়নি — তাই কিছু তৈরি হয় না। main.cpp compile হওয়ার সময় Stack<int> চাওয়া হলো, কিন্তু সেখানে শুধু ঘোষণা আছে, সংজ্ঞা নেই — তাই compiler ধরে নেয় অন্য কোথাও আছে। শেষে জোড়া দিতে গিয়ে linker কোথাও পায় না:
/usr/bin/ld: main.o: in function `main': main.cpp:(.text+0x2a): undefined reference to `Stack<int, 4ul>::pop()' collect2: error: ld returned 1 exit status
এটি একটি ক্লাসিক linker error. সমাধান: template-এর পুরো সংজ্ঞা — class-এর ভেতরের হোক বা বাইরের — header-এই রাখুন। standard library-র প্রায় সব template এভাবেই header-এ থাকে। বিকল্প একটি পথ আছে, যখন আপনি আগেই জানেন কোন কোন টাইপ লাগবে: .cpp ফাইলে template class Stack<int>; লিখে সেই টাইপের জন্য জোর করে কোড তৈরি করানো। একে বলে explicit instantiation. বড় প্রজেক্টে compile-এর সময় কমাতে এটি কাজে লাগে, কিন্তু অজানা টাইপের ব্যবহারকারীর জন্য এটি চলে না।
error message-এর পাহাড়
template-এ ভুল হলে compiler প্রায়ই কয়েক ডজন লাইনের error দেয়, কারণ সে দেখায় কোন instantiation-এর ভেতরে কোন instantiation-এর ভেতরে ভুলটি ঘটেছে। ভয় পাবেন না, পড়ার কৌশলটা জানুন: প্রথম error: লাইনটি খুঁজুন, তারপর “required from here” লেখা লাইনগুলোর মধ্যে আপনার নিজের ফাইলের লাইনটি খুঁজুন — সেখানেই আপনি template-টিকে ভুল টাইপ দিয়ে ডেকেছেন। template-এর শর্ত শুরুতেই স্পষ্ট করে লেখার উপায় C++20-এর concepts, যা পর্ব ৪০-এ দেখবেন; তখন error message-ও অনেক ছোট আর পরিষ্কার হয়।
C-তে template নেই। generic container বানাতে C-তে দুটি পথ: void* দিয়ে সব টাইপকে একই রকম ধরা (যেমন qsort), অথবা macro দিয়ে প্রতিটি টাইপের জন্য কোড “কপি-পেস্ট” করানো। প্রথমটিতে টাইপ-নিরাপত্তা হারায়, দ্বিতীয়টিতে debug করা কঠিন। C++-এর class template দুটির ভালো দিক একসাথে দেয় — macro-র মতো প্রতিটি টাইপের জন্য আলাদা, দ্রুত কোড, আর compiler-এর পুরো টাইপ-যাচাই। macro-র পথটি কেমন, তা দেখতে C কোর্সের preprocessor ও macro পর্ব পড়ুন।
std::vector<int>আরstd::vector<double>কি একই টাইপ? — না; একই template-এর দুটি instantiation, সম্পূর্ণ আলাদা ও সম্পর্কহীন class.- template-এর member function .cpp ফাইলে লিখলে কী হয়? — অন্য ফাইল থেকে ব্যবহার করলে সাধারণত linker error (undefined reference), কারণ সংজ্ঞা আর টাইপ একসাথে কোথাও নেই; সমাধান header-এ সংজ্ঞা বা explicit instantiation.
- full আর partial specialization-এর পার্থক্য? — full সব argument ঠিক করে দেয় (
template <>), partial একটি ধাঁচ ঠিক করে আর কিছু parameter খোলা রাখে; partial শুধু class (ও variable) template-এ চলে। - CTAD কী, কোন standard থেকে? — C++17 থেকে constructor-এর argument দেখে class template-এর argument বুঝে নেওয়া, যেমন
std::pair p{1, 2.5}. typename T::value_type-এ typename কেন লাগে? —T::value_typeএকটি dependent name; compiler ডিফল্টভাবে একে মান ধরে, typename বলে দেয় এটি টাইপ।
Bjarne Stroustrup ১৯৮৮ সালে “Parameterized Types for C++” নামে template-এর নকশা প্রকাশ করেন, আর ১৯৯০-এর দশকের শুরুতে Cfront 3.0-এ template প্রথম বাস্তবে আসে। তখন লক্ষ্য ছিল সাধারণ — container-এর জন্য macro-র চেয়ে ভালো একটি উপায়। কিন্তু ১৯৯৪ সালে Alexander Stepanov-এর STL standard-এর খসড়ায় গৃহীত হলে দেখা গেল, template দিয়ে container আর algorithm-কে সম্পূর্ণ আলাদা রেখেও দক্ষতার সাথে জোড়া যায়। আর একই বছর C++ কমিটির একটি সভায় Erwin Unruh দেখালেন একটি প্রোগ্রাম, যা compile-ই হয় না, কিন্তু compiler-এর error message-এ মৌলিক সংখ্যার তালিকা ছাপে — template যে নিজেই একটি পুরো প্রোগ্রামিং ভাষা, তার প্রথম চমকপ্রদ প্রমাণ।
৯ · AI-এর যুগে — template-কোডের ভুল ধরা
AI টুল template লিখতে খুব আগ্রহী। “এটা generic করে দাও” বললেই সে প্রতিটি class-এর সামনে template <typename T> বসিয়ে দেয় — এমনকি যেখানে আসলে একটিই টাইপ কখনো ব্যবহার হবে। template-কোড compile হলেও তাতে লুকিয়ে থাকতে পারে এমন ভুল, যা শুধু নির্দিষ্ট টাইপ দিয়ে ব্যবহার করলে ধরা পড়ে — কারণ মনে রাখবেন, যে member function ডাকা হয় না, তার কোড তৈরিই হয় না, তাই তার ভুলও অনেক সময় চোখে পড়ে না। সাধারণ ভুলগুলো:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
| সবকিছু template — একটিই টাইপ ব্যবহার হলেও | error দীর্ঘ, compile ধীর, কোড পড়া কঠিন; কোনো লাভ নেই | দ্বিতীয় টাইপের সত্যিকার দরকার এলে তবেই template |
| member function-এর সংজ্ঞা আলাদা .cpp ফাইলে | অন্য ফাইল থেকে ব্যবহারে linker error | পুরো সংজ্ঞা header-এ, বা জেনে-বুঝে explicit instantiation |
dependent টাইপের আগে typename নেই | compile error; বা AI “ঠিক” করতে গিয়ে অদ্ভুত cast বসায় | typename C::value_type, অথবা auto |
| specialization-এ কিছু ফাংশন বাদ, বা আচরণ বদলানো | generic কোড সেই টাইপে ভাঙে; invariant ভাঙে | primary-র পুরো interface ও প্রতিশ্রুতি হুবহু রাখা |
template-এর ভেতরে হাতে new T[N] আর নিজের destructor | copy/move ভুল, exception-এ leak, move-only টাইপে ভাঙে | std::array/std::vector member — Rule of Zero |
T-এর ওপর অলিখিত শর্ত (<, কপি) কোথাও লেখা নেই | ভুল টাইপ দিলে বিশাল error, কারণ বোঝা যায় না | শর্ত মন্তব্যে বা static_assert-এ, পরে concepts (পর্ব ৪০) |
AI-কে template লিখতে বলার সময় আগে জিজ্ঞেস করুন template আদৌ দরকার কি না, আর নিয়মগুলো স্পষ্ট করে দিন:
AI-এর template-কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১std::vector<bool>-কে অনেক বিশেষজ্ঞ standard library-র একটি “অনুশোচনার” নকশা বলেন। মেমরি আট গুণ বাঁচানো তো ভালো জিনিস — তাহলে সমস্যা কোথায়?
C++98 যখন standard হলো, তখন মেমরি আজকের মতো সস্তা ছিল না। কমিটির কাছে মনে হয়েছিল, bool-এর একটি vector-এ প্রতিটি মানের জন্য পুরো একটি বাইট খরচ করা অপচয় — তাই std::vector<bool>-কে একটি full specialization বানানো হলো, যা প্রতিটি মান একটি বিটে রাখে। উদ্দেশ্য ছিল ভালো: সবাই বিনা খরচে একটি দক্ষ bit-array পাবেন।
সমস্যা হলো, এটি “vector” নামের প্রতিশ্রুতি ভেঙেছে। প্রতিটি vector-এর একটি মৌলিক নিয়ম: v[i] উপাদানটির একটি আসল reference দেয়, আর &v[0] একটি pointer দেয়, যা ধরে পাশাপাশি সাজানো উপাদানে হাঁটা যায়। একটি বিটের কোনো আলাদা ঠিকানা হয় না, তাই vector<bool> দেয় একটি proxy object. ফলে bool& r = v[0]; compile হয় না; auto x = v[0]; লিখলে x একটি proxy, যা মূল vector-এর সাথে যুক্ত থাকে — vector ধ্বংস হলে x dangling হয়ে যায়। generic কোড, যেমন template <typename T> void f(std::vector<T>& v) { T& first = v[0]; ... }, T = bool ছাড়া সব টাইপে চলে। আর একাধিক thread যদি পাশাপাশি দুটি আলাদা উপাদান লেখে, অন্য vector-এ সেটি নিরাপদ, কিন্তু vector<bool>-এ দুটি বিট একই বাইটে থাকতে পারে, ফলে data race.
Herb Sutter ১৯৯০-এর দশকের শেষেই লিখেছিলেন যে vector<bool> আসলে standard-এর container-এর শর্তই পূরণ করে না। কমিটিতে বহুবার আলোচনা হয়েছে একে deprecate করে বিট-ভিত্তিক আলাদা একটি টাইপ আনার, কিন্তু ততদিনে কোটি কোটি লাইনের কোড এর ওপর নির্ভর করে — তাই এটি আজও আছে। মজার বিষয়, বিট-ভিত্তিক সংরক্ষণ চাইলে সমাধান আগে থেকেই ছিল: নির্দিষ্ট মাপের জন্য std::bitset, আর পরিবর্তনশীল মাপের জন্য Boost-এর dynamic_bitset. যাঁর আসল bool-এর vector দরকার, তিনি আজ std::vector<char> বা std::deque<bool> দিয়ে কাজ চালান।
এর আসল শিক্ষা অপ্টিমাইজেশন নিয়ে নয়, নামকরণ আর প্রতিশ্রুতি নিয়ে। একই কাজের দক্ষ সংস্করণ আলাদা নামে দিলে কেউ ক্ষতিগ্রস্ত হতেন না। একই নামের নিচে লুকিয়ে দেওয়ায় যিনি এটি চাননি, তিনিও বাধ্য হয়ে নিয়েছেন।
মূল উপলব্ধি: specialization ভেতরের বাস্তবায়ন বদলানোর হাতিয়ার, বাইরের চুক্তি বদলানোর নয়। আলাদা আচরণ চাইলে আলাদা নাম দিন — ব্যবহারকারী যেন নিজে বেছে নিতে পারেন।
প্র ০২template নকশা করা হয়েছিল container-এর জন্য, অথচ মানুষ এটি দিয়ে compile-এর সময়েই হিসাব চালাতে শুরু করল। template metaprogramming কীভাবে জন্মাল, আর Boost ও Alexandrescu-র বইয়ের ভূমিকা কী?
template-এর মূল নকশায় কেউ ভাবেননি এটি দিয়ে “প্রোগ্রাম” চালানো যাবে — যাকে আজ বলে template metaprogramming. কিন্তু দুটি বৈশিষ্ট্য মিলে সেটি সম্ভব করে দিল: template নিজেকে নিজে ব্যবহার করতে পারে (recursion), আর specialization দিয়ে “থামার শর্ত” লেখা যায়। যেমন Factorial<N>-এর মান N * Factorial<N - 1>, আর Factorial<0>-এর একটি full specialization যার মান 1. compiler instantiation করতে করতেই উত্তর বের করে ফেলে — প্রোগ্রাম চলার আগেই। ১৯৯৪ সালে Erwin Unruh কমিটির সভায় এর চরম রূপ দেখান: একটি প্রোগ্রাম, যার error message-এ মৌলিক সংখ্যা ছাপা হয়। পরে প্রমাণ হলো, template-এর এই ব্যবস্থা তাত্ত্বিকভাবে Turing-complete — অর্থাৎ যেকোনো হিসাব এতে লেখা সম্ভব।
Todd Veldhuizen ১৯৯৫ সালে “template metaprogram” নিয়ে লেখেন, আর expression templates নামে একটি কৌশল দেখান, যা দিয়ে গাণিতিক লাইব্রেরি (তাঁর Blitz++) হাতে-লেখা Fortran-এর কাছাকাছি গতি পেত। ১৯৯৮ সালে Boost প্রতিষ্ঠিত হয় — standard-এ যেতে পারে এমন মানের, peer-review করা লাইব্রেরির একটি সংগ্রহ হিসেবে। Boost-এর অনেক লাইব্রেরি template-এর সীমা ঠেলে দেয়, আর তাদের বহু অংশ পরে standard-এ আসে: smart pointer, type traits, tuple, function, regex, thread, optional, variant.
২০০১ সালে Andrei Alexandrescu প্রকাশ করেন “Modern C++ Design”. বইটি দেখায়, template দিয়ে নকশার সিদ্ধান্তগুলো (যেমন thread-safety, মেমরি নেওয়ার নিয়ম) “policy” হিসেবে compile-এর সময় জুড়ে দেওয়া যায় (policy-based design), আর টাইপের তালিকা (typelist) নিয়ে compile-এর সময় হিসাব চালানো যায়। এর সাথের Loki লাইব্রেরি আর বইটি একটি প্রজন্মের C++ প্রোগ্রামারের চিন্তা বদলে দেয়। ২০০৪ সালে David Abrahams আর Aleksey Gurtovoy-এর “C++ Template Metaprogramming” বই Boost.MPL লাইব্রেরি ঘিরে এই কৌশলগুলো আরও সুশৃঙ্খল করে।
কিন্তু দাম ছিল চড়া: কোড পড়া প্রায় অসম্ভব, error message পৃষ্ঠার পর পৃষ্ঠা, compile সময় দীর্ঘ। তাই পরের standard-গুলো একই ক্ষমতা সহজ ভাষায় দিতে শুরু করে: C++11-এর constexpr আর variadic template, C++17-এর if constexpr আর fold expression, C++20-এর concepts. আজ বেশিরভাগ compile-time হিসাব সাধারণ ফাংশনের মতো দেখতে constexpr কোডে লেখা হয় (পর্ব ৩৯)।
মূল উপলব্ধি: একটি ভাষার বৈশিষ্ট্য প্রায়ই তার নকশাকারদের কল্পনার বাইরে ব্যবহার হয়। template metaprogramming দেখিয়েছিল কী সম্ভব; পরের দুই দশক C++ সেই সম্ভাবনাকে সাধারণ প্রোগ্রামারের হাতে সহজ রূপে তুলে দেওয়ার চেষ্টা করেছে।
প্র ০৩template-কে বলা হয় “চলার সময়ে বিনা খরচে” — তাহলে খরচটা কোথায় যায়? compile time, প্রোগ্রামের আকার আর চলার গতির মধ্যে আসল লেনদেন কী?
চলার সময়ের হিসাবটা সত্যিই চমৎকার। Stack<int>-এর কোড ঠিক ততটাই দ্রুত, যতটা আপনি হাতে IntStack লিখলে হতো। কারণ instantiation-এর পর কোনো টাইপ-অনিশ্চয়তা থাকে না: compiler জানে উপাদান int, তাই সরাসরি ৪ বাইট কপি করে, ছোট ফাংশন inline করে, লুপ অপ্টিমাইজ করে। তুলনা করুন C-এর qsort-এর সাথে: এটি প্রতিটি তুলনার জন্য একটি function pointer ডাকে, যা compiler সাধারণত inline করতে পারে না। std::sort template বলে তুলনাটি inline হয়, আর এ কারণেই বাস্তবে এটি প্রায়ই qsort-এর চেয়ে লক্ষণীয়ভাবে দ্রুত।
খরচ যায় তিন জায়গায়। এক, compile time. template-এর পুরো সংজ্ঞা header-এ থাকে, তাই যে .cpp ফাইল সেটি include করে, প্রত্যেককে পুরো কোড আবার পড়তে আর instantiate করতে হয়। একশো ফাইলে std::vector<int> ব্যবহার হলে একই কাজ একশো বার হয় (linker শেষে অতিরিক্ত কপিগুলো ফেলে দেয়, ODR-এর নিয়মে)। template-ভারী বড় প্রজেক্টে compile সময় কয়েক গুণ বেড়ে যেতে পারে।
দুই, প্রোগ্রামের আকার। প্রতিটি আলাদা টাইপের জন্য আলাদা কোড — Stack<int>, Stack<long>, Stack<double>-এর মেশিন কোড হয়তো প্রায় একই, তবু তিনটি কপি। একে বলে code bloat. বেশি কোড মানে CPU-র instruction cache-এ চাপ, যা কখনো উল্টো গতি কমায়। linker-এর কিছু অপ্টিমাইজেশন হুবহু একই মেশিন কোড একত্র করতে পারে, আর লাইব্রেরি-লেখকেরা কখনো টাইপ-নির্ভর নয় এমন অংশ একটি সাধারণ, non-template base-এ সরিয়ে রাখেন।
তিন, মানুষের সময়। দীর্ঘ error message, কঠিন debugging, আর নতুন সদস্যের জন্য কোড বোঝার কষ্ট। এর প্রতিকার হিসেবে C++ ধীরে ধীরে যন্ত্র যোগ করেছে: C++11-এর extern template (অন্য ফাইলকে বলা “এই instantiation এখানে কোরো না, অন্যত্র আছে”), C++20-এর concepts (ছোট error), আর C++20-এর modules (header বারবার পড়ার কাজ কমানো)।
মূল উপলব্ধি: template চলার সময়ের খরচকে compile-এর সময় আর প্রোগ্রামের আকারে সরিয়ে দেয়। বেশিরভাগ প্রোগ্রামে এটি লাভজনক লেনদেন — কিন্তু “বিনা খরচে” কথাটির পুরো অর্থ হলো “ব্যবহারকারীর কম্পিউটারে বিনা খরচে”, ডেভেলপারের কম্পিউটারে নয়।
প্র ০৪function template-এ টাইপ বুঝে নেওয়ার সুবিধা C++-এর শুরু থেকেই ছিল, অথচ class template-এ CTAD এল ২০১৭ সালে। এত দেরি কেন — সমস্যাটা কোথায় ছিল?
দেরির একটি বড় কারণ, মনে হয় যতটা সহজ, আসলে তা নয়। ফাংশনের ক্ষেত্রে argument সরাসরি ফাংশনের প্যারামিটারের সাথে মেলে — max(T a, T b)-এ argument int হলে T = int. কিন্তু class-এর template parameter আর তার constructor-এর প্যারামিটার আলাদা জিনিস। একটি class-এর অনেকগুলো constructor থাকতে পারে, আর তাদের প্যারামিটার template parameter-এর সাথে সরাসরি যুক্ত না-ও হতে পারে। যেমন std::vector(first, last) constructor নেয় দুটি iterator — উপাদানের টাইপ সেখানে লেখা নেই, iterator-এর ভেতর থেকে বের করতে হয়। কোন নিয়মে compiler এটি বুঝবে, তা ঠিক করা দরকার ছিল।
দীর্ঘদিন এই ফাঁক ভরানো হয়েছে “make” ফাংশন দিয়ে। C++98-এ std::make_pair, C++11-এ std::make_tuple আর std::make_shared, C++14-এ std::make_unique. এগুলো function template, তাই deduction পায়, আর ভেতরে সঠিক class বানিয়ে ফেরত দেয়। কাজ চলত, কিন্তু প্রতিটি নতুন class-এর জন্য একটি সহায়ক ফাংশন লিখতে হতো, আর শিক্ষার্থীদের কাছে ব্যাখ্যা করতে হতো, std::pair p(1, 2.5) কেন চলে না।
অবশেষে Mike Spertus, Faisal Vali আর Richard Smith-এর প্রস্তাব (P0091) C++17-এ গৃহীত হয়। মূল কৌশল: প্রতিটি constructor থেকে compiler কল্পনায় একটি function template বানায়, আর সাধারণ deduction চালায়। যেখানে এটি যথেষ্ট নয়, সেখানে লাইব্রেরি-লেখক deduction guide লিখে নিয়ম শিখিয়ে দেন। কিন্তু কঠিন প্রশ্নগুলো থেকেই গেল। std::vector v{v2}; — এটি কি v2-এর কপি (vector<int>), নাকি একটি উপাদানওয়ালা vector<vector<int>>? নিয়ম বলে: কপি — যা বেশিরভাগ মানুষ আশা করেন, কিন্তু সবাই নয়। আরও বিতর্ক হয়েছিল, CTAD পুরোনো কোডের অর্থ বদলে দেবে কি না, আর ভুল টাইপ বুঝে নেওয়ার ঝুঁকি কতটা।
কাজটি পরেও চলেছে: C++20-এ aggregate (constructor ছাড়া struct) আর alias template-এর জন্যও CTAD যোগ হয়। অর্থাৎ এটি একবারে শেষ হওয়া কাজ নয়, ধাপে ধাপে বাড়ানো একটি সুবিধা।
মূল উপলব্ধি: একটি “ছোট সুবিধা” ভাষায় যোগ করতেও কয়েক দশকের পুরোনো কোড, ভাষার প্রতিটি কোণের নিয়ম আর অস্পষ্ট ক্ষেত্রগুলো মাথায় রাখতে হয়। তাই C++ প্রায়ই আগে লাইব্রেরি দিয়ে সমস্যা সামলায়, পরে ভাষায় স্থায়ী সমাধান আনে।
প্র ০৫C++11-এর variadic template কেন এত বড় পরিবর্তন? এর আগে std::tuple বা make_unique-এর মতো জিনিস কীভাবে লেখা হতো, আর এখন কী বদলেছে?
C++11-এর আগে template-এর parameter-এর সংখ্যা নির্দিষ্ট থাকতেই হতো। তাহলে “যেকোনো সংখ্যক টাইপ” নেওয়া একটি tuple কীভাবে বানাবেন? উত্তর ছিল কৌশল আর ধৈর্য: একটি সর্বোচ্চ সংখ্যা ঠিক করা (Boost-এর tuple-এ এটি ছিল ১০), প্রতিটি parameter-এর default দেওয়া একটি বিশেষ “খালি” টাইপ, আর প্রতিটি সংখ্যার জন্য আলাদা overload — শূন্য argument, এক argument, দুই argument… দশ পর্যন্ত। এই একঘেয়ে কোড হাতে না লিখে Boost.Preprocessor-এর macro দিয়ে তৈরি করানো হতো। ফলাফল কাজ করত, কিন্তু header বিশাল, compile ধীর, error message দুর্বোধ্য, আর ১১তম argument দিলে সবকিছু ভেঙে পড়ত।
Douglas Gregor আর Jaakko Järvi ২০০০-এর দশকের মাঝামাঝি variadic template-এর প্রস্তাব দেন, যা C++11-এ গৃহীত হয়। parameter pack একটি সরল ধারণা — “শূন্য বা তার বেশি জিনিসের ব্যাগ” — কিন্তু এটি একসাথে অনেক সমস্যা মিটিয়ে দেয়। std::tuple এখন সত্যিকার অর্থে যেকোনো সংখ্যক টাইপ নেয়। std::function যেকোনো স্বাক্ষরের ফাংশন রাখতে পারে। আর সবচেয়ে বেশি ব্যবহৃত ফল: rvalue reference-এর সাথে মিলে perfect forwarding — emplace_back(args...) বা make_shared<T>(args...) যতগুলো argument পায়, ঠিক তাদের মূল রূপে (কপি না করে) T-এর constructor-এ পৌঁছে দেয়।
একটি ছোট মজার ঘটনা: std::make_shared C++11-এ এলেও std::make_unique আসে C++14-এ। Herb Sutter পরে একে একটি নিছক অবহেলা (oversight) বলেছিলেন — variadic template থাকায় এটি লেখা মাত্র কয়েক লাইনের কাজ ছিল, আর অনেকে নিজেরাই লিখে নিতেন।
তারপর C++17-এর fold expression এল (Andrew Sutton আর Richard Smith-এর প্রস্তাব), যা ব্যাগ খোলার কাজ আরও সহজ করল — আগে প্রতিটি ছোট কাজের জন্য recursion দিয়ে একটি “প্রথমটি নাও, বাকিগুলোর ওপর আবার ডাকো” ফাংশন লিখতে হতো। আজ std::format আর std::print-ও variadic template — আপনি যতগুলো মান দেন, প্রতিটির টাইপ compile-এর সময় যাচাই হয়, যা C-এর printf-এর বড় দুর্বলতা দূর করে।
মূল উপলব্ধি: একটি সাধারণীকরণ — “সংখ্যা নির্দিষ্ট নয়” — হাজার হাজার লাইনের macro-কৌশলকে কয়েক লাইনের পরিষ্কার কোডে নামিয়ে আনল। আধুনিক C++-এর অনেক “সহজ” সুবিধা, যা আপনি প্রতিদিন ব্যবহার করেন, এই ভিত্তির ওপর দাঁড়িয়ে।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
আউটপুট অনুমান:
template <typename T> struct Counter { inline static int created = 0; Counter() { ++created; } };. এবারCounter<int> a, b, c; Counter<double> d; Counter<int> e = a;—Counter<int>::createdআরCounter<double>::createdকত?Counter<int>::created= 3,Counter<double>::created= 1. দুটি instantiation আলাদা class, তাই তাদের static member-ও আলাদা। আর ফাঁদটি হলোe = a: এটি default constructor নয়, compiler-এর তৈরি copy constructor চলে, যা++createdকরে না — তাই int-এর গণনা ৪ নয়, ৩। ঠিকভাবে গুনতে চাইলে copy constructor-এও গুনতে হতো। -
বাগ শিকার: একটি AI তিনটি ফাইল দিয়েছে।
box.h-এtemplate <typename T> class Box { T v_; public: void set(T v); T get() const; };,box.cpp-এ দুটি ফাংশনের সংজ্ঞা, আরmain.cpp-এBox<int> b; b.set(5);. দুটি ফাইল আলাদা compile হয়, কিন্তু জোড়া লাগাতে ব্যর্থ। কেন, আর দুটি সমাধান কী?linker error:
undefined reference to Box<int>::set(int). box.cpp compile হওয়ার সময় সংজ্ঞা ছিল, কিন্তু কেউBox<int>চায়নি, তাই কোনো কোড তৈরি হয়নি; main.cpp-এ চাওয়া হয়েছে, কিন্তু সংজ্ঞা দেখা যায়নি। সমাধান ১: দুটি সংজ্ঞাই box.h-এ সরিয়ে আনুন (সবচেয়ে প্রচলিত)। সমাধান ২: box.cpp-এর শেষেtemplate class Box<int>;লিখে explicit instantiation — তখন শুধু int-এর জন্য চলবে,Box<double>আবার একই error দেবে। -
নকশা ভাবুন: একটি বাস-টিকিটের অ্যাপে কোনগুলো class template হওয়া উচিত, কোনগুলো নয়? (ক) নির্দিষ্ট মাপের সিটের গ্রিড, যার আকার প্রতিটি বাসের মডেলে আলাদা কিন্তু compile-এর সময় জানা (খ) যাত্রীর তথ্য রাখার
Passengerclass (গ) একটি “শেষ ২০টি সার্চ” মনে রাখার তালিকা, যা শহরের নাম ও রুট দুটোর জন্যই লাগে (ঘ) bKash দিয়ে টাকা নেওয়ারPaymentclass.(ক) হ্যাঁ —
SeatGrid<Rows, Cols>-এর মতো non-type parameter যুক্তিসংগত, কারণ মাপ compile-এর সময় জানা আর ভুল মাপ মেশানো ঠেকানো যায়। তবে বাসের মডেল যদি ডেটাবেস থেকে চলার সময় আসে, তাহলেstd::vector-ভিত্তিক সাধারণ class. (খ) না — একটিই টাইপ, template-এর কোনো লাভ নেই। (গ) হ্যাঁ —RingBuffer<T, 20>বাRecent<T>; একই আচরণ দুটি ভিন্ন টাইপে, ঠিক template-এর কাজ। (ঘ) না — বিভিন্ন পেমেন্ট পদ্ধতি চলার সময় বেছে নেওয়া হয়, এটি পর্ব ২৫-এর virtual-এর কাজ। নিয়মটি: “একই আচরণ, compile-এর সময় জানা একাধিক টাইপ” হলে template. -
নিজে লিখুন (কোড):
print_all-এর মতো করে একটিsum_all(args...)লিখুন, যা fold expression দিয়ে সব argument-এর যোগফল ফেরত দেয়।sum_all(1, 2, 3.5)কী দেবে, আর ফেরত-টাইপ কী হওয়া উচিত? বোনাস:fixed_stack.cpp-এরpush-এ ভরা stack-এর পরীক্ষা যোগ করুন।template <typename... Args> auto sum_all(const Args&... args) { return (args + ...); }. ফল 6.5 —1 + (2 + 3.5), যেখানে int আর double যোগে double হয়, তাইautoফেরত-টাইপ double. কোনো argument না দিলে(args + ...)compile error, কারণ খালি যোগফলের মান কী হবে compiler জানে না; সমাধান(args + ... + 0)— একটি শুরুর মানসহ fold. বোনাসের জন্য push বদলে লিখুন:bool push(const T& v) { if (size_ == N) return false; data_[size_++] = v; return true; }— এখন invariantsize_ <= Nকখনো ভাঙে না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমC++-এ Stack<int> a; a.push(1); লেখা থাকলে compiler-এর কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- template-এর সংজ্ঞা পড়ে নকশাটি মনে রাখে, কোনো মেশিন কোড তৈরি করে না
- template argument ঠিক করে — লেখা মান, default বা CTAD থেকে
- মেলে এমন specialization আছে কি না দেখে, সবচেয়ে নির্দিষ্টটি বেছে নেয়
- T-এর জায়গায় int বসিয়ে class-টি তৈরি করে (instantiation)
- যে member function সত্যিই ডাকা হয়েছে, শুধু তার কোড তৈরি করে
C++-এ Stack<int> আর Stack<double> — একই class template-এর দুটি instantiation-এর মধ্যে সম্পর্ক কী?
- Stack<double> হলো Stack<int>-এর derived class
- দুটি একই টাইপ, শুধু ভেতরের মান আলাদা
- দুটি সম্পূর্ণ আলাদা, সম্পর্কহীন class
- একই প্রোগ্রামে দুটি একসাথে রাখা যায় না
C++17 বা পরের standard-এ std::pair p{1, 2.5}; লিখলে p-এর টাইপ কী?
- std::pair<int, int>
- std::pair<int, double>
- std::pair<double, double>
- compile error — template argument লিখতেই হবে
C++-এ template <typename T> class Stack-এর pop() class-এর বাইরে সংজ্ঞায়িত করার সঠিক রূপ কোনটি?
template <typename T> T Stack<T>::pop() { ... }T Stack::pop() { ... }- template <typename T> T Stack::pop() { ... }
- T Stack<T>::pop() { ... }
C++-এ একটি class template-এর member function-এর সংজ্ঞা শুধু stack.cpp-এ রেখে main.cpp থেকে Stack<int> ব্যবহার করলে সাধারণত কী হয়?
- সব ঠিকঠাক চলে, compiler নিজে stack.cpp খুঁজে নেয়
- main.cpp compile-এর সময়েই error
- প্রোগ্রাম চলার সময় crash করে
- linker error — undefined reference
C++-এ template <typename T> struct S { };, template <typename T> struct S<T*> { }; আর template <> struct S<int> { }; আছে। S<int*> কোন সংস্করণ ব্যবহার করে?
- primary template S<T>
- partial specialization S<T*>, T = int
- full specialization S<int>
- compile error — ambiguous
C++-এর একটি template-এর ভেতরে typename C::value_type x; লেখায় typename কেন দরকার?
- value_type নামে একটি নতুন টাইপ তৈরি করতে
- চলার সময় C-এর টাইপ যাচাই করতে
- C-এর ওপর নির্ভরশীল নামটি যে একটি টাইপ, তা compiler-কে জানাতে
- শুধু পড়ার সুবিধার জন্য, না লিখলেও চলে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- class template একটি ছাঁচ: প্রতিটি argument-এর জন্য compiler একটি আলাদা, সম্পর্কহীন class তৈরি করে; শুধু ডাকা member function-এর কোডই তৈরি হয়।
- বাইরের সংজ্ঞায়
template <...>লাইন আরName<T>::লাগে; পুরো সংজ্ঞা থাকে header-এ, নইলে linker error. - CTAD (C++17) constructor থেকে টাইপ বোঝে, deduction guide নিয়ম শেখায়; default argument আর non-type parameter (
RingBuffer<T, N>) নকশা সহজ করে। - specialization: full একটি টাইপের জন্য, partial একটি ধাঁচের জন্য, সবচেয়ে নির্দিষ্টটি জেতে — কিন্তু interface আর প্রতিশ্রুতি এক রাখুন (
std::vector<bool>-এর শিক্ষা)। - বাকি যন্ত্র:
usingদিয়ে alias template; static member প্রতিটি instantiation-এ আলাদা; Rule of Zero; variadic template ও fold expression; dependent name-এর আগেtypename.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩০ · sequence container — vector, deque, listপরবর্তী পর্বstandard library-র class template-গুলো ভেতর থেকে — মেমরি, গতি আর বেছে নেওয়ার নিয়ম।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ২৮ · function templateআবার দেখুনtemplate-এর ভিত্তি, deduction, instantiation আর non-type parameter — এই পর্বের পূর্বশর্ত।