এই পর্বে যা শিখবেন
- একই কাজের ফাংশন টাইপ বদলে বারবার লেখার সমস্যা, আর macro দিয়ে তার পুরোনো, বিপজ্জনক সমাধান
template <typename T>লেখার নিয়ম, template argument deduction আর explicit argument- instantiation — compiler কীভাবে টাইপ-প্রতি একটি করে ফাংশন বানায়, আর কেন template header-এ থাকে
- একাধিক type parameter,
autoreturn, C++20-এর abbreviated template আর non-type parameter - overloading বনাম specialization, T-এর অলিখিত শর্ত ও
requires, template বনাম virtual — আর AI-এর template-কোড যাচাই
১ · সমস্যাটা আগে — একই ফাংশন, তিনবার লেখা
ধরুন আপনি একটি অনলাইন বাজারের অ্যাপ লিখছেন। এক জায়গায় দুটি পণ্যের দামের মধ্যে বড়টি চাই, আরেক জায়গায় দুটি দোকানের রেটিংয়ের মধ্যে বড়টি, আর আরেক জায়গায় দুটি নামের মধ্যে বর্ণানুক্রমে পরেরটি। পর্ব ৯-এর function overloading দিয়ে আপনি এভাবে লিখতে পারেন:
int largest(int a, int b) { return (a < b) ? b : a; }
double largest(double a, double b) { return (a < b) ? b : a; }
std::string largest(std::string a, std::string b) { return (a < b) ? b : a; }
// কাল long-এর জন্য লাগবে, পরশু Money টাইপের জন্য...তিনটি ফাংশনের শরীর হুবহু এক: (a < b) ? b : a. পার্থক্য শুধু টাইপের নামে। এটি একটি কপি-পেস্টের কারখানা। নতুন টাইপ এলে আবার একটি কপি। আর কোনোদিন যুক্তিতে একটি ভুল ধরা পড়লে — ধরুন সমান হলে কোনটি ফেরত দেবেন, সেই নিয়ম বদলাতে হলো — তখন সবগুলো কপি খুঁজে খুঁজে বদলাতে হবে। একটি বাদ পড়লেই দুই টাইপে দুই রকম আচরণ, আর কেউ টের পাবে না।
পিঠা বানানোর কাঠের ছাঁচের কথা ভাবুন। ছাঁচে নকশা খোদাই করা থাকে একবারই। সেই ছাঁচে চালের গুঁড়ার মণ্ড চাপলে চালের পিঠা, আটার মণ্ড চাপলে আটার পিঠা, খেজুর গুড় মেশানো মণ্ড চাপলে গুড়ের পিঠা — নকশা প্রতিবার হুবহু এক। প্রতিটি উপাদানের জন্য আলাদা ছাঁচ খোদাই করতে হয় না। Template হলো সেই ছাঁচ, আর টাইপ হলো মণ্ড। আপনি নকশা (যুক্তি) লেখেন একবার; compiler প্রতিটি টাইপের “মণ্ড” দিয়ে আলাদা আসল ফাংশন বানিয়ে নেয়।
C++-এ এই ছাঁচের নাম template. যে template একটি ফাংশন বানানোর নিয়ম দেয়, তাকে বলে function template. আর টাইপের নাম না বেঁধে একবার যুক্তি লেখার এই চিন্তাধারাকে বলে generic programming — এই মডিউলের পুরো বিষয়। পর্ব ১৩ থেকে আপনি যে std::vector<int> ব্যবহার করে আসছেন, সেটিও একটি template থেকে বানানো; পরের পর্বে সেই class template-এর কথা আসবে। আজ শুরু করছি সবচেয়ে সরল রূপ দিয়ে: ফাংশন।
Template হলো compile-এর সময়ের কোড-তৈরি (code generation), চলার সময়ের কোনো জাদু নয়। template নিজে কোনো মেশিন কোড নয়। আপনি যখন একটি নির্দিষ্ট টাইপ দিয়ে ডাকেন, তখন compiler ছাঁচে টাইপ বসিয়ে একটি সাধারণ ফাংশন লিখে নেয় — যেন আপনি নিজেই কপি-পেস্ট করেছেন, কিন্তু নির্ভুলভাবে। প্রোগ্রাম যখন চলে, তখন template বলে আর কিছু থাকে না; থাকে শুধু সেই তৈরি করা ফাংশনগুলো।
২ · পুরোনো পথ: macro — আর তার লুকানো ফাঁদ
template আসার আগে, আর C-তে আজও, “যেকোনো টাইপে চলবে” এমন কোডের প্রচলিত উপায় ছিল macro:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int m1 = MAX(3, 7); // 7
double m2 = MAX(2.5, 1.5); // 2.5 — "যেকোনো টাইপে চলে"!দেখতে চমৎকার। কিন্তু macro কোনো ফাংশন নয়। এটি preprocessor-এর একটি লেখা-বদলের নিয়ম: compile শুরুর আগেই MAX(x, y) লেখাটি অক্ষরে অক্ষরে বদলে বসানো হয়। preprocessor C++ বোঝে না — সে টাইপ চেনে না, scope চেনে না, শুধু লেখা চেনে। ফলে তিনটি সমস্যা:
- টাইপ যাচাই নেই।
MAX(3, "hello")লিখলে macro আপত্তি করে না; compiler পরে বদলানো লেখায় অদ্ভুত error দেয়, macro-র নাম পর্যন্ত বলে না। - নাম সবখানে ঢুকে পড়ে। macro namespace মানে না। অন্য কোনো লাইব্রেরিতে
maxনামে ফাংশন থাকলে এবং কেউmaxনামে macro বানালে, সেই ফাংশনের ঘোষণাও বদলে যায়। Windows-এরwindows.h-এরmin/maxmacro এই কারণে বহু বছর ধরে C++ প্রোগ্রামারদের ভোগাচ্ছে। - argument একাধিকবার হিসাব হতে পারে। এটিই সবচেয়ে নীরব ফাঁদ, যাকে বলে double evaluation. নিচে ধাপে ধাপে দেখুন।
1int i = 6;2int m = MAX(i++, 5);3// বদলে হলো: ((i++) > (5) ? (i++) : (5))45int m2 = largest(i++, 5); // template, i = 6 থেকে
| i | |
| এই মুহূর্তের কাজ | |
| ফল |
- শুরু।
i-এর মান 6. আমরা চাই: i আর 5-এর মধ্যে বড়টি নেওয়া হোক, তারপর i একবার বাড়ুক। সাধারণ ফাংশনে ঠিক এটাই হতো। - preprocessor-এর লেখা-বদল। compile শুরুর আগে
MAX(i++, 5)অক্ষরে অক্ষরে বদলে যায়। যেখানে যেখানেaছিল, সেখানে বসলi++— আর macro-র ভেতরেaআছে দুবার। কোনো ফাংশন-ডাক নেই, কোনো parameter নেই। - প্রথম
i++: শর্ত। শর্তের অংশে(i++) > (5)হিসাব হলো: পুরোনো মান 6, 6 > 5 সত্য। এর side effect হিসেবে i হলো 7. - দ্বিতীয়
i++: ফল। শর্ত সত্য, তাই বেছে নেওয়া হলো(i++)অংশ — আবার। এবার মান 7, আর i হলো 8. m = 7, অথচ i আর 5-এর বড়টি ছিল 6! এখানে undefined behavior নেই, ক্রমটি নির্ধারিত — কিন্তু ফল আপনার উদ্দেশ্যের সাথে মেলে না, আর কোনো warning-ও আসে না। - template-এ একই ডাক।
largest(i++, 5)একটি আসল ফাংশন-ডাক। argument একবারই হিসাব হয়ে parametera-তে কপি হয়: a = 6, i = 7. ফাংশনের ভেতরেaযতবারই লেখা থাকুক, সেটি একটি ভ্যারিয়েবল, আবার হিসাব নয়। ফল 6, ঠিক যা চেয়েছিলেন।
| প্রশ্ন | macro (#define) | function template |
|---|---|---|
| কে প্রক্রিয়া করে | preprocessor, লেখা হিসেবে | compiler, C++ কোড হিসেবে |
| টাইপ যাচাই | নেই | আছে, প্রতিটি টাইপের জন্য |
| argument হিসাব | যতবার লেখা, ততবার | ঠিক একবার |
| namespace ও scope | মানে না | মানে |
| debugger-এ ধাপে ধাপে চালানো | প্রায় অসম্ভব | সাধারণ ফাংশনের মতো |
| চলার সময়ের খরচ | নেই (লেখা বসে যায়) | নেই বললেই চলে (inline হতে পারে) |
C-তে template নেই। “যেকোনো টাইপ” চাইলে সেখানে দুটি পথ: macro, অথবা void* নিয়ে কাজ করা ফাংশন — যেমন qsort, যা টাইপের মাপ আর তুলনার ফাংশন pointer আলাদা করে নেয়, আর ভুল টাইপ দিলে compiler কিছুই ধরে না। C11-এর _Generic টাইপ দেখে আগে থেকে লেখা ফাংশন বেছে দিতে পারে, কিন্তু নতুন ফাংশন বানায় না। C++-এর template দুটির ভালো দিক মেলায়: macro-র মতো খরচহীন, ফাংশনের মতো টাইপ-নিরাপদ। macro-র সব ফাঁদ বিস্তারিত দেখতে C কোর্সের Preprocessor ও Macro পর্ব পড়ুন।
৩ · প্রথম function template — template <typename T>
তিনটি largest-কে এক করার জন্য টাইপের নামের জায়গায় একটি “ফাঁকা ঘর” রাখুন, আর তার নাম দিন T:
template <typename T> // "T নামে একটি টাইপ আসবে"
T largest(T a, T b) { // যেখানে টাইপ ছিল, সেখানে T
return (a < b) ? b : a;
}প্রথম লাইনটি template-এর ঘোষণা। কোণ বন্ধনীর ভেতরের typename T হলো একটি template parameter — ফাংশনের parameter যেমন একটি মানের জন্য জায়গা রাখে, এটি তেমনি একটি টাইপের জন্য জায়গা রাখে। T নামটি শুধু প্রচলিত রীতি; Item বা Number লিখলেও চলে। typename-এর বদলে class লেখাও এখানে হুবহু একই অর্থ বহন করে (template <class T>) — পুরোনো কোডে প্রায়ই দেখবেন। T-এর জায়গায় যে আসল টাইপ বসে (int, double…), তাকে বলে template argument.
এবার এই একটি ছাঁচ দিয়ে দামের তুলনা, নামের তুলনা — সব করা যাক। প্রথমবারের মতো চালিয়ে দেখুন:
#include <print>
#include <string>
template <typename T>
T largest(T a, T b) {
return (a < b) ? b : a;
}
int main() {
std::println("{}", largest(3, 7)); // T = int
std::println("{}", largest(120.5, 99.75)); // T = double (price)
std::string x = "Rahim", y = "Karim";
std::println("{}", largest(x, y)); // T = std::string
std::println("{}", largest<double>(3, 2.5)); // T given explicitly
// largest(3, 2.5); // error: is T int or double?
}আউটপুট চার লাইন: 7, 120.5, Rahim আর 3. প্রথম তিনটি ডাকে আপনি কোথাও T কী, তা বলেননি — compiler argument দেখে নিজেই বুঝে নিয়েছে: int, double, std::string. string-এর < বর্ণানুক্রমে তুলনা করে, আর R আসে K-এর পরে, তাই Rahim. চতুর্থ ডাকে কোণ বন্ধনীতে <double> লিখে T নিজে বলে দেওয়া হয়েছে; 3 তখন 3.0 হয়ে যায়, আর বড়টি 3.0 — println এটি 3 হিসেবে দেখায়। শেষের মন্তব্য করা লাইনটি খুলে দিলে compile error আসবে; কেন, তা ৪ নম্বর অংশে। খেয়াল করুন, string by value নেওয়ায় কপি হয় — বড় টাইপের জন্য const T& নেওয়াই ভালো, যেমন standard library-র std::max নেয়।
একটি জিনিস আলাদা করে মনে রাখুন: largest নিজে কোনো ফাংশন নয় — এটি একটি ফাংশন-পরিবারের নিয়ম। largest<int> একটি আসল ফাংশন, largest<std::string> আরেকটি। দুটির ঠিকানা আলাদা, মেশিন কোড আলাদা। কোনো টাইপে কখনো ডাকা না হলে সেই টাইপের ফাংশন কখনো তৈরিই হয় না।
৪ · Deduction — compiler কীভাবে T খুঁজে পায়
আপনি largest(3, 7) লিখলেন, কিন্তু T কী, তা বলেননি। compiler প্রতিটি argument-এর টাইপকে parameter-এর ছাঁচের সাথে মিলিয়ে T বের করে। এই প্রক্রিয়ার নাম template argument deduction — পর্ব ৫-এর auto-র type deduction-এর মতোই, কারণ auto-র নিয়ম আসলে template-এর নিয়ম থেকেই নেওয়া। ধাপে ধাপে দেখুন, একটি ডাক থেকে আসল ফাংশন পর্যন্ত:
1template <typename T>2T largest(T a, T b) { return (a < b) ? b : a; }34int r = largest(3, 7);
| argument-এর টাইপ | |
| T | |
| তৈরি হওয়া ফাংশন | |
| কখন |
- ডাকটি চোখে পড়ল। compiler
largest(3, 7)দেখল। এই নামে একটি function template আছে, কিন্তু T এখনো অজানা। হাতে আছে দুটি argument:3আর7, দুটিই int টাইপের literal. - Deduction. parameter
a-এর টাইপ T, আর তাতে যাচ্ছে int — তাই T = int. parameterb-এর টাইপও T, যাচ্ছে int — আবার T = int. দুই দিক থেকে একই উত্তর, তাই কোনো দ্বন্দ্ব নেই: T = int নিশ্চিত। - Instantiation. compiler ছাঁচের প্রতিটি T-এর জায়গায় int বসিয়ে একটি আসল ফাংশন লিখে নিল:
int largest<int>(int a, int b). এটিই instantiation. তারপর সেটিকে সাধারণ ফাংশনের মতোই compile করল। একই প্রোগ্রামে পরে আবারlargest(10, 20)এলে নতুন কিছু বানাবে না, এই ফাংশনটিই ব্যবহার করবে। - চলার সময়: সাধারণ ডাক। প্রোগ্রাম চলার সময় template বা deduction বলে কিছু নেই — আছে একটি সাধারণ int ফাংশন। a = 3, b = 7, শর্ত সত্য, ফেরত 7. কোনো বাড়তি খরচ নেই; ছোট বলে compiler এটি inline করেও ফেলতে পারে।
যখন দুই দিক থেকে দুই উত্তর আসে
এবার সেই মন্তব্য করা লাইনটি: largest(3, 2.5). এখানেই নতুনরা সবচেয়ে বেশি হোঁচট খান, কারণ সাধারণ ফাংশনে int থেকে double-এ রূপান্তর নিজে থেকেই হয়ে যায়। deduction-এ তা হয় না:
template <typename T> T largest(T a, T b); largest(3, 2.5); // 3 হলো int, 2.5 হলো double
a ← 3 থেকে: T = int b ← 2.5 থেকে: T = double T একটাই হতে পারে → দ্বন্দ্ব! (deduction-এর সময় int → double রূপান্তর ধরা হয় না)
error: no matching function for call to
'largest(int, double)'
note: candidate: 'template<class T> T largest(T, T)'
note: template argument deduction/substitution failed:
note: deduced conflicting types for parameter 'T'
('int' and 'double')
largest<double>(3, 2.5); // ১. T নিজে বলে দিন: 3 → 3.0
largest(3.0, 2.5); // ২. argument-এর টাইপ মেলান
template <typename A, typename B> // ৩. দুটি আলাদা টাইপ মেনে নিন
auto largest2(A a, B b) { return (a < b) ? b : a; }
largest2(3, 2.5); // ফল double: 3.0
- ডাকটি নিরীহ দেখায়। মানুষ হিসেবে আপনি জানেন 3 আর 2.5-এর মধ্যে বড়টি 3. কিন্তু compiler-কে আগে ঠিক করতে হবে T কী, তারপরই ফাংশন বানানো যাবে।
- দুই দিক থেকে দুই উত্তর। প্রথম argument বলছে T = int, দ্বিতীয়টি বলছে T = double. compiler কোনো পক্ষ নেয় না — “int-কে double বানিয়ে নিই” এমন অনুমান deduction-এর নিয়মে নেই। কারণ আছে: কোন দিকে রূপান্তর হবে, সেটি অনুমান করতে গেলে ভুল টাইপ বেছে নেওয়ার ঝুঁকি থাকত।
- error বার্তাটি পড়তে শিখুন। g++ বলছে: উপযুক্ত কোনো ফাংশন পাইনি; একটিমাত্র প্রার্থী ছিল, এই template; তার deduction ব্যর্থ, কারণ T-এর জন্য দুটি পরস্পরবিরোধী টাইপ পাওয়া গেছে — int আর double. “deduced conflicting types” কথাটি দেখলেই বুঝবেন সমস্যা কোথায়। standard library-র
std::max(3, 2.5)-ও ঠিক একই কারণে compile হয় না। - সমাধান উদ্দেশ্যের ওপর নির্ভর করে। T নিজে বলে দেওয়া — explicit template argument — সবচেয়ে সরল: তখন deduction হয় না, আর 3 সাধারণ নিয়মে 3.0 হয়ে যায়। অথবা argument-এর টাইপই মিলিয়ে দিন। অথবা মেনে নিন যে দুটি টাইপ আলাদা হতে পারে: দুটি type parameter আর
autoreturn. তৃতীয়টি নমনীয়, কিন্তু signed আর unsigned মেশালে তুলনা বিপজ্জনক হতে পারে, তাই অকারণে নমনীয়তা দেবেন না।
deduction-এর আরও কিছু নিয়ম জেনে রাখুন। by value parameter (T a) হলে argument-এর const আর reference বাদ পড়ে, আর array হয়ে যায় pointer — auto-র মতোই। const T& parameter হলে array থাকে array. আর return টাইপ থেকে কখনো T বের করা হয় না: template <typename T> T make();-কে ডাকতে হলে make<int>() লিখতেই হবে, কারণ কোনো argument নেই যা দেখে compiler অনুমান করবে।
৫ · Instantiation — ছাঁচ থেকে আসল ফাংশন, আর কেন template header-এ থাকে
একই template যখন দুটি ভিন্ন টাইপে ডাকা হয়, তখন compiler দুটি সম্পূর্ণ আলাদা ফাংশন বানায়। প্রতিটির নিজের মেশিন কোড, নিজের symbol. ঘটনাটি ধাপে ধাপে দেখুন:
- template
largest<T>শুধু নিয়ম - T = int
largest<int>প্রথম ফাংশন - T = string
largest<string>দ্বিতীয় ফাংশন - object file
২টি symbollinker দেখে
template <typename T>
T largest(T a, T b) { return (a < b) ? b : a; }
// কেউ ডাকেনি → compiler শুধু ব্যাকরণ দেখে রাখল,
// কোনো ফাংশন বানাল না।
int largest<int>(int a, int b) {
return (a < b) ? b : a; // int-এর <: একটি CPU নির্দেশ
}
std::string largest<std::string>(std::string a, std::string b) {
return (a < b) ? b : a; // string-এর operator<:
} // অক্ষর ধরে ধরে তুলনা, একটি লুপ
int largest<int>(int, int) std::string largest<std::string>(std::string, std::string) // দুটি আলাদা symbol, দুটি আলাদা মেশিন কোড। // largest<double> নেই — কেউ double দিয়ে ডাকেনি।
- শুধু নিয়ম। template লিখে রাখলেই কোনো মেশিন কোড তৈরি হয় না। compiler শুধু ব্যাকরণ ঠিক আছে কি না আর T-এর ওপর নির্ভর করে না এমন অংশগুলো দেখে রাখে। ছাঁচ তাকে রাখা আছে, পিঠা এখনো হয়নি।
- প্রথম টাইপ: int. int দিয়ে প্রথম ডাক আসতেই compiler T = int বসিয়ে একটি ফাংশন লিখে নিল। এখানে
<মানে দুটি পূর্ণসংখ্যার তুলনা — মেশিন কোডে প্রায় একটি নির্দেশ। - দ্বিতীয় টাইপ: std::string. একই লেখা, কিন্তু এবার
<মানে std::string-এর operator overloading — অক্ষর ধরে ধরে তুলনার একটি পুরো ফাংশন। উৎস কোড এক, তৈরি হওয়া মেশিন কোড সম্পূর্ণ ভিন্ন। এজন্যই template এত দ্রুত: প্রতিটি টাইপ নিজের সেরা কোড পায়। - object file-এ দুটি symbol.
nm -Cদিয়ে object file দেখলে দুটি আলাদা ফাংশন পাবেন, নামে টাইপসহ (পর্ব ৩-এর name mangling এখানেই কাজে লাগে)। double-এর কোনো সংস্করণ নেই। নিয়মটি সরল: যে টাইপে ডাকবেন, শুধু সেই টাইপের ফাংশন তৈরি হবে — প্রতিটি টাইপের জন্য একবার।
কেন template-এর পুরো সংজ্ঞা header-এ থাকে
পর্ব ১১-এ শিখেছিলেন: সাধারণ ফাংশনের ঘোষণা header file-এ, সংজ্ঞা .cpp-তে। template-এ এই অভ্যাস ভেঙে যায়। কারণ লুকিয়ে আছে compile হওয়ার ধরনে। প্রতিটি .cpp ফাইল আলাদাভাবে compile হয় — একেকটি translation unit. ধরুন main.cpp largest(3, 7) ডাকল, কিন্তু সে শুধু header-এর ঘোষণাটুকু দেখে:
// largest.h — শুধু ঘোষণা (ভুল পথ)
template <typename T> T largest(T a, T b);
// largest.cpp — সংজ্ঞা এখানে
template <typename T> T largest(T a, T b) { return (a < b) ? b : a; }
// main.cpp
#include "largest.h"
int main() { return largest(3, 7); }
// linker: undefined reference to 'int largest<int>(int, int)'main.cpp compile করার সময় compiler-এর T = int সংস্করণ বানানো দরকার, কিন্তু ছাঁচের শরীর তার সামনে নেই। সে ধরে নেয়, অন্য কোথাও নিশ্চয় তৈরি আছে, আর একটি অপূর্ণ ডাক রেখে যায়। ওদিকে largest.cpp compile করার সময় compiler-এর কাছে শরীর আছে, কিন্তু সেই ফাইলে কেউ int দিয়ে ডাকেনি — তাই সে কিছুই বানায় না। শেষে linker খুঁজে পায় না: একটি linker error. দুজনের কাছে আধখানা করে তথ্য, কারও কাছে পুরোটা নেই।
সমাধান: template-এর পুরো সংজ্ঞা header-এ রাখুন। তখন যে ফাইল যে টাইপে ডাকে, সেই ফাইলেই compiler ছাঁচটি পুরো দেখে সংস্করণ বানাতে পারে। দুটি ফাইল যদি একই largest<int> বানায়? সমস্যা নেই — template থেকে তৈরি ফাংশন ODR-এর নিয়মে inline ফাংশনের মতো ছাড় পায়: একাধিক ফাইলে একই সংজ্ঞা থাকতে পারে, linker একটি রেখে বাকিগুলো ফেলে দেয়। এজন্যই standard library-র বেশিরভাগ template-কোড header-এর ভেতরে, আর অনেক জনপ্রিয় লাইব্রেরি পুরোপুরি header-only library.
আগে থেকে জানা অল্প কয়েকটি টাইপের জন্য explicit instantiation করা যায়: largest.cpp-এর শেষে template int largest<int>(int, int); লিখলে compiler সেখানেই int সংস্করণ বানিয়ে রাখে। তখন শুধু সেই টাইপগুলোই চলবে, অন্য টাইপে আবার linker error. C++11-এর extern template উল্টো কাজ করে — বলে “এখানে বানিও না, অন্য ফাইলে বানানো আছে” — বড় প্রজেক্টে compile-এর সময় কমাতে। আর C++20-এর modules (পর্ব ৪২) এই পুরো সমস্যাকে নতুনভাবে সমাধানের চেষ্টা করছে।
Code bloat — টাইপ-প্রতি একটি কপির দাম
প্রতিটি টাইপের জন্য আলাদা ফাংশন মানে প্রোগ্রামের আকার বাড়তে পারে। দশটি টাইপে ডাকা একটি বড় template থেকে দশটি বড় ফাংশন তৈরি হয়। এর নাম code bloat. ছোট ফাংশনে এটি নগণ্য — compiler প্রায়ই সেগুলো inline করে ফেলে, আর কিছু linker হুবহু একই মেশিন কোডের ফাংশন একটিতে মিলিয়েও দেয়। কিন্তু অনেক টাইপে ব্যবহৃত বিশাল template-ভারী কোডে executable-এর আকার আর compile-এর সময় — দুটিই চোখে পড়ার মতো বাড়ে। ভারসাম্যের একটি পুরোনো কৌশল: টাইপ-নির্ভর নয় এমন বড় অংশটি একটি সাধারণ ফাংশনে সরিয়ে রাখা, আর template-কে শুধু পাতলা মোড়ক রাখা।
৬ · একাধিক টাইপ, auto return, abbreviated template আর non-type parameter
একাধিক type parameter আর auto return
৪ নম্বর অংশের তৃতীয় সমাধানে দুটি type parameter দেখেছেন: template <typename A, typename B>. তখন প্রশ্ন ওঠে — ফেরত টাইপ কী লিখবেন? A, না B? C++14 থেকে উত্তর সহজ: auto লিখুন, compiler return-এর হিসাব দেখে টাইপ ঠিক করবে (return type deduction). (a < b) ? b : a-তে একটি int আর একটি double থাকলে ফল double, তাই largest2(3, 2.5) ফেরত দেয় 3.0. আগে থেকে টাইপটি নাম ধরে চাইলে standard library-র std::common_type_t<A, B> লেখা যায়, যা দুই টাইপের “মিলনের” টাইপ বলে দেয়।
Abbreviated function template — C++20-এর সংক্ষিপ্ত রূপ
C++20 থেকে parameter-এর টাইপের জায়গায় সরাসরি auto লিখলেই ফাংশনটি template হয়ে যায়। একে বলে abbreviated function template:
auto twice(auto x) { return x + x; }
// হুবহু সমান:
template <typename T> auto twice(T x) { return x + x; }
auto bigger(auto a, auto b) { return (a < b) ? b : a; }
// সাবধান: এটি template <typename A, typename B> — দুটি আলাদা টাইপ!লিখতে ছোট, পড়তে সহজ। কিন্তু একটি সূক্ষ্ম পার্থক্য মনে রাখুন: প্রতিটি auto parameter একটি আলাদা template parameter. “দুটি argument-এর টাইপ এক হতেই হবে” — এই শর্ত বোঝাতে চাইলে পুরোনো template <typename T> রূপটিই লিখতে হবে। ছোট সহায়ক ফাংশনে auto রূপ চমৎকার; যেখানে টাইপগুলোর সম্পর্ক গুরুত্বপূর্ণ, সেখানে নাম দেওয়া T.
Non-type template parameter — টাইপ নয়, একটি মান
template parameter সবসময় টাইপ হতে হবে, এমন নয়। একটি compile-time ধ্রুবক মানও হতে পারে: template <int N>. একে বলে non-type template parameter. পর্ব ১৩-এর std::array<int, 5>-এর 5 ঠিক এমনই একটি parameter. মানটি compile-এর সময় জানা থাকতে হয় — একটি literal, constexpr ভ্যারিয়েবল ইত্যাদি; ব্যবহারকারীর ইনপুট থেকে পড়া মান চলবে না। C++20 থেকে double আর কিছু সরল class টাইপও এখানে চলে, তবে সাধারণ ব্যবহার পূর্ণসংখ্যা।
এবার তিনটি বাস্তব কাজ একসাথে: যেকোনো সংখ্যার vector-এর যোগফল (sum_all), একটি sensor-এর গড়বড়ে মানকে সীমার ভেতরে আনা (clamp_to_range), আর নির্দিষ্ট হারে ভ্যাট যোগ — হারটি non-type parameter হিসেবে:
#include <print>
#include <vector>
template <typename T>
T sum_all(const std::vector<T>& v) {
T total{}; // 0 for numbers
for (const T& x : v) total += x;
return total;
}
template <typename T>
T clamp_to_range(T value, T lo, T hi) { return value < lo ? lo : (hi < value ? hi : value); }
template <int Percent> // non-type parameter
int add_vat(int taka) { return taka + taka * Percent / 100; }
int main() {
std::vector<int> sales{120, 80, 200};
std::vector<double> temps{30.5, 31.25, 29.75};
std::println("{} {}", sum_all(sales), sum_all(temps));
std::println("{} {}", clamp_to_range(1023, 0, 1000), clamp_to_range(-4.5, 0.0, 50.0));
std::println("{}", add_vat<15>(200));
}আউটপুট তিন লাইন: 400 91.5, 1000 0 আর 230. sum_all একবার int-এর জন্য, একবার double-এর জন্য instantiate হয়েছে — T deduce হয়েছে std::vector<T> ছাঁচ থেকে, vector-এর ভেতরের টাইপ দেখে। T total{} হলো value-initialization: সংখ্যার জন্য 0, আর std::string হলে খালি string — তাই এই ফাংশন string-এর vector জুড়েও দেবে। clamp_to_range-এ sensor 1023 পাঠালে সেটি 1000-এ নামে, ঋণাত্মক তাপমাত্রা -4.5 উঠে আসে 0-তে (double 0.0 দেখায় 0). খেয়াল করুন, এখানে clamp_to_range(-4.5, 0, 50) লিখলে আবার deduction-এর দ্বন্দ্ব হতো। শেষে add_vat<15>-এ 15 compile-এর সময়েই কোডে বসে গেছে; add_vat<5> লিখলে সম্পূর্ণ আলাদা আরেকটি ফাংশন তৈরি হতো। (standard library-তে C++17 থেকে তৈরি std::clamp আছে — বাস্তব কোডে সেটিই ব্যবহার করুন।)
৭ · Overloading আর specialization — কোনো এক টাইপের জন্য আলাদা নিয়ম
কখনো কখনো ছাঁচটি কোনো একটি টাইপে ভুল কাজ করে। সবচেয়ে বিখ্যাত উদাহরণ: C-ধাঁচের string literal.
auto w = largest("apple", "mango");
// "apple" আর "mango" by value-তে pointer হয়ে যায়: T = const char*
// a < b তুলনা করে দুটি ঠিকানা, লেখা নয়!
// কোনটি বড় হবে, তা নির্ভর করে লেখা দুটি মেমরিতে কোথায় বসেছেএটি compile হয়, চলেও — কিন্তু ফল লেখার বর্ণানুক্রমের সাথে সম্পর্কহীন। দুটি আলাদা object-এর ঠিকানা < দিয়ে তুলনার ফল C++-এ unspecified. এমন টাইপের জন্য আলাদা নিয়ম দেওয়ার দুটি পথ আছে।
// পথ ১ (পছন্দের): একটি সাধারণ, non-template overload
const char* largest(const char* a, const char* b) {
return std::strcmp(a, b) < 0 ? b : a; // অক্ষর ধরে তুলনা, <cstring>
}
// পথ ২: full specialization — "T = const char* হলে এই শরীর"
template <>
const char* largest<const char*>(const char* a, const char* b) {
return std::strcmp(a, b) < 0 ? b : a;
}প্রথম পথে আপনি একই নামে একটি সাধারণ ফাংশন যোগ করছেন; overload resolution তখন template আর সাধারণ ফাংশনকে পাশাপাশি বিচার করে। একটি নিয়ম এখানে গুরুত্বপূর্ণ: দুজন সমান ভালো মিললে সাধারণ (non-template) ফাংশন জেতে। তাই largest("apple", "mango") এখন সাধারণ ফাংশনটি ডাকে। দ্বিতীয় পথটি template specialization: template <> দিয়ে বলা হয়, “এই template-এর T = const char* সংস্করণ আমি নিজে লিখে দিচ্ছি।”
দুটিই এখানে কাজ করে। তবু অভিজ্ঞ C++ প্রোগ্রামারদের পরামর্শ: function template-এর জন্য overloading বেছে নিন, specialization নয়। কারণ specialization নিজে overload resolution-এ অংশ নেয় না। compiler আগে শুধু “মূল” template আর সাধারণ ফাংশনগুলোর মধ্যে বিচার করে; মূল template জিতলে তবেই দেখে, এর কোনো specialization আছে কি না। একাধিক overload করা template থাকলে কোন specialization কোন মূল template-এর অংশ, তা নিয়ে এমন চমক তৈরি হয় যে অভিজ্ঞরাও ভুল অনুমান করেন। Herb Sutter-এর একটি বহুল পঠিত প্রবন্ধের শিরোনামই ছিল “Why Not Specialize Function Templates?”. তা ছাড়া function template-এর আংশিক (partial) specialization ভাষাতেই নেই — class template-এ আছে, পরের পর্বে দেখবেন। সবচেয়ে সরল সমাধান অবশ্য তৃতীয়টি: C-ধাঁচের string-ই এড়িয়ে চলুন। ডাকার সময় T হিসেবে std::string_view বলে দিলে তুলনা হয় লেখা ধরে, ঠিকানা ধরে নয়।
৮ · T-এর অলিখিত শর্ত, ভয়ংকর error, আর requires-এর এক ঝলক
largest-এর কোথাও লেখা নেই যে T-কে < দিয়ে তুলনা করা যেতে হবে। কিন্তু শরীরে a < b আছে, তাই শর্তটি লুকিয়ে আছে। একইভাবে sum_all নীরবে চায়: T-এর T{} বানানো যায় আর += চলে। এই অলিখিত চাহিদাগুলোকে অনেকে বলেন T-এর implicit interface — virtual ফাংশনের base class-এর মতো কোনো লিখিত চুক্তি নেই, শুধু “যা ব্যবহার করা হয়েছে, তা থাকতে হবে”। শর্ত না মিললে কী হয়?
struct Point { int x, y; }; // < সংজ্ঞায়িত নেই
largest(Point{1, 2}, Point{3, 4});In instantiation of 'T largest(T, T) [with T = Point]':
required from here
error: no match for 'operator<' (operand types are 'Point' and 'Point')
| return (a < b) ? b : a;
| ~~^~~
note: candidate: 'operator<(int, int)' (built-in) ...
note: no known conversion for argument 1 from 'Point' to 'int'
... (আরও বহু লাইন: std::string, std::vector, std::pair-এর operator< কেন মেলেনি)
খেয়াল করুন, error আসছে template-এর ভেতর থেকে — যে লাইন আপনি লেখেননি, এমনকি হয়তো কখনো দেখেননি। এখানে template ছোট বলে বার্তাটি সহনীয়। কিন্তু STL-এর গভীর template-এ (যেমন std::sort-কে ভুল টাইপ দিলে) একই ভুলে কয়েকশো লাইনের বার্তা আসতে পারে, যার প্রথম আর শেষ কয়েক লাইন ছাড়া প্রায় সবই compiler-এর ভেতরের চিন্তাভাবনা। template-এর দুর্নামের বড় অংশ এই error message.
C++20 শর্তটি মুখে বলার উপায় দিয়েছে। template-এর মাথায় একটি requires clause বা সরাসরি একটি concept-এর নাম:
#include <concepts>
template <std::totally_ordered T> // শর্ত: T-তে < > <= >= == != চলে
T largest(T a, T b) { return (a < b) ? b : a; }
// একই কথা অন্যভাবে:
template <typename T>
requires std::totally_ordered<T>
T smallest(T a, T b) { return (b < a) ? b : a; }
// largest(Point{1, 2}, Point{3, 4}) এখন:
// error: no matching function ... constraints not satisfied
// note: the required expression 'a < b' is invalid ...std::totally_ordered হলো standard library-র একটি তৈরি concept (header <concepts>), যার অর্থ: এই টাইপের দুটি মানকে সব রকম তুলনা অপারেটরে সংগতভাবে তুলনা করা যায়। এখন ভুল টাইপ দিলে error আসে ডাকার জায়গায়, template-এর ভেতরে ঢোকার আগেই, আর বার্তা সরাসরি বলে কোন শর্ত মেলেনি। শর্তটি ফাংশনের ঘোষণাতেই লেখা, তাই পাঠকও দেখেই বোঝেন কোন টাইপ চলবে। concept নিজে লেখা, নানা শর্ত মেলানো আর concept দিয়ে overload বাছাই — পুরো বিষয়টি পর্ব ৪০-এ। আপাতত অভ্যাসটুকু নিন: নতুন template লিখলে T-এর শর্তটি নাম ধরে লিখে দিন।
৯ · Template বনাম virtual — compile-time বনাম run-time polymorphism
পর্ব ২৫-এ virtual ফাংশন দিয়ে “একই ডাক, ভিন্ন কাজ” দেখেছেন। template-ও একই জিনিস দেয়: একই largest ডাক int-এ এক কাজ করে, string-এ আরেক কাজ। পার্থক্য হলো সিদ্ধান্তটি কখন হয়। virtual-এ চলার সময়, vtable দেখে (dynamic binding). template-এ compile-এর সময়, টাইপ দেখে — একে বলে static polymorphism.
| প্রশ্ন | Template | Virtual ফাংশন |
|---|---|---|
| কখন সিদ্ধান্ত | compile-এর সময় | চলার সময় |
| টাইপদের সম্পর্ক | দরকার নেই; শুধু ব্যবহৃত কাজগুলো থাকলেই হলো | একটি সাধারণ base class থেকে আসতে হয় |
| ডাকের খরচ | সরাসরি ডাক, প্রায়ই inline | একটি পরোক্ষ ডাক, সাধারণত inline নয় |
| এক container-এ নানা টাইপ | সরাসরি নয় (প্রতিটি instantiation আলাদা টাইপ) | হ্যাঁ, base pointer দিয়ে |
| নতুন টাইপ যোগ | ডাকার কোড আবার compile করতে হয় | আগের compile করা কোড না ছুঁয়েই যায় |
| দাম | code bloat, compile-এর সময়, লম্বা error | vptr, পরোক্ষ ডাক, heap-এ object |
একটি সরল প্রশ্নে বাছাই করুন: টাইপটি কে জানে, আর কখন? compile-এর সময়েই সব টাইপ জানা, আর গতি গুরুত্বপূর্ণ — template. টাইপ ঠিক হয় চলার সময় (ব্যবহারকারী মেনু থেকে বেছে নেন, ফাইল থেকে পড়া হয়), বা নতুন টাইপ বাইরের কেউ পরে যোগ করবে — virtual. standard library নিজেই দুটি মেশায়: std::sort পুরোপুরি template, তাই হাতে-লেখা লুপের মতো দ্রুত; আবার std::function ভেতরে চলার সময়ের dispatch ব্যবহার করে যেকোনো callable ধরে রাখে।
Template-এর ধারণা নতুন ছিল না — Ada আর CLU-র মতো ভাষায় “generic” আগেই ছিল। C++-এ Bjarne Stroustrup ১৯৮৮ সালে “Parameterized Types for C++” নামের গবেষণাপত্রে template-এর নকশা প্রকাশ করেন, আর ১৯৯০ সালের The Annotated C++ Reference Manual (ARM)-এ এটি ভাষার অংশ হিসেবে বর্ণিত হয়। তার আগে C++ প্রোগ্রামাররা generic container বানাতেন macro দিয়েই। template-এর পুরো শক্তি দেখা দেয় Alexander Stepanov-এর STL-এ, যা ১৯৯৪ সালে standard-এ গৃহীত হয়ে C++98-এর অংশ হলো।
- macro আর function template-এর পার্থক্য কী? — macro preprocessor-এর লেখা-বদল, টাইপ যাচাই নেই, argument একাধিকবার হিসাব হতে পারে; template compiler বোঝে, টাইপ-নিরাপদ, argument একবার হিসাব হয়।
- template instantiation কী, আর কখন হয়? — নির্দিষ্ট template argument বসিয়ে compiler-এর আসল ফাংশন বানানো; compile-এর সময়, প্রতিটি ব্যবহৃত টাইপের জন্য একবার।
- template-এর সংজ্ঞা header-এ কেন রাখা হয়? — instantiation-এর জন্য compiler-কে ডাকার ফাইলেই পুরো শরীর দেখতে হয়; শুধু ঘোষণা থাকলে linker error হয়।
template <class T>আরtemplate <typename T>-এ পার্থক্য আছে? — type parameter ঘোষণায় নেই, দুটি হুবহু এক।- একই নামের template আর সাধারণ ফাংশন সমান ভালো মিললে কোনটি ডাকা হয়? — সাধারণ (non-template) ফাংশন।
১০ · AI-এর যুগে — template-কোড যাচাই
AI টুল template লিখতে খুব আগ্রহী — “generic” কোড দেখতে পেশাদার লাগে। কিন্তু আসল প্রশ্ন সবসময় আগে: এখানে কি আদৌ template দরকার? যে ফাংশন জীবনে শুধু int নিয়েই কাজ করবে, তাকে template বানালে কোনো লাভ নেই, আছে শুধু লম্বা error আর header-এ ঠেলে দেওয়া কোড। আরেকটি প্রবণতা: শর্তহীন template, যা ভুল টাইপ পেলে compiler-এর ভেতর থেকে কয়েকশো লাইনের error দেয়; আর specialization দিয়ে এমন কাজ করা, যা একটি সাধারণ overload-ই করত। পুরোনো অনলাইন কোড থেকে শেখা বলে AI মাঝে মাঝে macro-র MAX-ও লিখে দেয়। সাধারণ ভুলগুলো:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
| একটিমাত্র টাইপে ব্যবহৃত ফাংশনকেও template বানায় | অকারণ জটিলতা, লম্বা error, header-এ কোড | সাধারণ ফাংশন; দ্বিতীয় টাইপ এলে তখন template |
শর্তহীন typename T | ভুল টাইপে template-এর ভেতর থেকে দুর্বোধ্য error | requires বা concept দিয়ে শর্ত লেখা |
একটি টাইপের জন্য template <> specialization | overload resolution-এ অংশ নেয় না; অপ্রত্যাশিত বাছাই | সাধারণ non-template overload |
template-এর সংজ্ঞা .cpp-তে, header-এ শুধু ঘোষণা | linker error: undefined reference | পুরো সংজ্ঞা header-এ |
#define MAX(a,b) বা windows.h-এর max-এর ওপর নির্ভর | double evaluation, টাইপ যাচাই নেই | std::max বা নিজের template |
বড় টাইপ by value (T a) | প্রতি ডাকে অকারণ কপি | const T& |
AI-কে template লিখতে বলার আগে নিজের সিদ্ধান্তটি দিয়ে দিন, আর তার কারণ জানতে চান:
AI-এর template-কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১C++-এর প্রথম দিকে template ছিল না। Stroustrup কেন template যোগ করলেন, আর এত দেরি হলো কেন?
১৯৭৯ সালে “C with Classes” শুরুর সময় Bjarne Stroustrup-এর মূল লক্ষ্য ছিল class, inheritance আর virtual ফাংশন — Simula-র সংগঠন, C-এর গতিতে। কিন্তু খুব দ্রুতই একটি সমস্যা চোখে পড়ল: একটি ভালো container class — ধরুন একটি তালিকা বা একটি vector — লিখতে গেলে তার উপাদানের টাইপ বেঁধে দিতে হয়। int-এর তালিকা, Employee-এর তালিকা, Shape*-এর তালিকা — প্রতিটির জন্য আলাদা class. প্রথম দিকের C++-এ এর সমাধান ছিল preprocessor: একটি header-এ (generic.h নামে পরিচিত) macro দিয়ে class-এর পুরো সংজ্ঞা লেখা থাকত, আর টাইপের নাম দিয়ে সেই macro “চালিয়ে” নতুন class বানানো হতো। কাজ চলত, কিন্তু macro-র সব দুর্বলতা নিয়ে: টাইপ যাচাই নেই, error message বোঝা যায় না, debugger-এ কিছু দেখা যায় না।
Stroustrup নিজে পরে লিখেছেন, তিনি চেয়েছিলেন template আরও আগে আসুক, কিন্তু নকশাটি ঠিক করতে সময় লেগেছিল। প্রশ্নগুলো কঠিন ছিল: টাইপ parameter-এর ওপর শর্ত কীভাবে বলা হবে? প্রতিটি টাইপের জন্য আলাদা কোড বানানো হবে, না একটি সাধারণ কোড সবাই ভাগ করবে? compile আর link-এর মডেলের সাথে কীভাবে মেলানো হবে? ১৯৮৮ সালে USENIX-এর একটি C++ সম্মেলনে তিনি “Parameterized Types for C++” গবেষণাপত্রে নকশাটি তুলে ধরেন। ১৯৯০ সালে Margaret Ellis-এর সাথে লেখা The Annotated C++ Reference Manual (ARM)-এ template আর exception দুটিই ভাষার অংশ হিসেবে বর্ণিত হলো, আর এই বইটিই ANSI/ISO standard তৈরির ভিত্তি-দলিল হয়। AT&T-র Cfront-এ template এল ১৯৯১ সালের দিকে, ৩.০ সংস্করণে।
নকশার সবচেয়ে প্রভাবশালী সিদ্ধান্ত ছিল দুটি: প্রতিটি টাইপের জন্য আলাদা কোড (instantiation), যাতে কোনো চলার-সময়ের খরচ না থাকে; আর T-এর শর্ত লিখে না দিয়ে “ব্যবহার থেকে বোঝা” — যাতে নকশা নমনীয় থাকে। প্রথমটি template-কে দ্রুততম করেছে, দ্বিতীয়টি দিয়েছে বিস্ময়কর নমনীয়তা — আর সেই কুখ্যাত error message. শর্ত লেখার সঠিক উপায় খুঁজতে C++-এর আরও প্রায় তিন দশক লেগেছে: C++20-এর concepts.
মূল উপলব্ধি: template এসেছে macro দিয়ে generic কোড লেখার যন্ত্রণা থেকে। এর নকশা সচেতনভাবে গতি আর নমনীয়তাকে বেছে নিয়েছিল, আর তার দাম — দুর্বোধ্য error — মেটাতে ভাষাকে বহু বছর অপেক্ষা করতে হয়েছে।
প্র ০২Alexander Stepanov-এর “generic programming” আর STL কীভাবে template-কে একটি সুবিধা থেকে C++-এর কেন্দ্রে নিয়ে এল?
Alexander Stepanov গণিতের মানুষ। তাঁর মূল প্রশ্ন ছিল: একটি algorithm-কে কি তার সবচেয়ে সাধারণ রূপে লেখা যায়, কোনো দক্ষতা না হারিয়ে? যেমন “সবচেয়ে বড়টি খোঁজা” কাজটির জন্য আসলে কী লাগে? array লাগে না, int লাগে না — লাগে শুধু উপাদানগুলোর ওপর দিয়ে একে একে হাঁটার উপায় আর দুটি উপাদান তুলনার উপায়। গণিতে বীজগণিতের কাঠামো (group, ring) যেমন নির্দিষ্ট সংখ্যা না ভেবে নিয়মের ওপর দাঁড়ায়, তিনি algorithm-কেও তেমনি টাইপের “ধারণা” (concept) আর তার শর্তের ওপর দাঁড় করাতে চেয়েছিলেন। এই চিন্তাধারাকেই তিনি আর David Musser নাম দেন generic programming.
তাঁরা প্রথমে Scheme আর Ada-তে এমন লাইব্রেরি বানানোর চেষ্টা করেন। Ada-র generic ছিল, কিন্তু Stepanov-এর অভিজ্ঞতায় কোনো ভাষাই তাঁকে একই সাথে পুরো সাধারণতা আর হাতে-লেখা কোডের সমান গতি দিচ্ছিল না। C++-এর template — বিশেষ করে টাইপ-প্রতি আলাদা instantiation, যার ফলে সব inline হয়ে যায় — প্রথমবার দুটিই একসাথে সম্ভব করল। HP Labs-এ Meng Lee-এর সাথে তিনি যে লাইব্রেরি বানালেন, সেটিই Standard Template Library. Andrew Koenig-এর আমন্ত্রণে ১৯৯৩ সালের শেষে তিনি C++ standard কমিটির সামনে এটি উপস্থাপন করেন; প্রস্তাবটি ছিল বিশাল আর standard-এর কাজ তখন প্রায় শেষের দিকে, তবু ১৯৯৪ সালে কমিটি এটি গ্রহণ করে। HP সেই বছরই কোডটি বিনা মূল্যে প্রকাশ করে।
STL-এর মূল কৌশল হলো তিনটি স্বাধীন অংশ: container (ডেটা রাখে), iterator (ডেটার ওপর দিয়ে হাঁটে) আর algorithm (কাজ করে)। algorithm শুধু iterator চেনে, container চেনে না। ফলে M-টি container আর N-টি algorithm-এর জন্য M×N-টি ফাংশন লিখতে হয় না, লাগে M+N. আর সবকিছু template বলে std::sort একটি int-এর vector-এ হাতে-লেখা লুপের মতোই দ্রুত — C-এর qsort-এর চেয়ে প্রায়ই দ্রুত, কারণ তুলনার ফাংশন inline হয়ে যায়। পর্ব ৩০ থেকে ৩৫ জুড়ে আপনি এই নকশাই খুঁটিয়ে দেখবেন।
লক্ষ্য করার মতো বিষয়: Stepanov শুরু থেকেই “concept” শব্দটি ব্যবহার করতেন — iterator-এর শর্ত, তুলনার শর্ত — কিন্তু তা লেখা থাকত শুধু documentation-এ, কোডে নয়। C++20-এর concepts সেই কাগুজে শর্তগুলোকে অবশেষে compiler-এর যাচাইযোগ্য কোড বানিয়েছে।
মূল উপলব্ধি: template একটি ভাষার সুবিধা; generic programming একটি চিন্তার পদ্ধতি। STL দেখাল যে সঠিক বিমূর্ততা (abstraction) দক্ষতা কমায় না — আর সেই প্রমাণই template-কে C++-এর কেন্দ্রে বসিয়েছে।
প্র ০৩১৯৯৪ সালে একটি প্রোগ্রাম compile হয়নি, কিন্তু তার error message-এ ছিল মৌলিক সংখ্যার তালিকা। template কি নিজেই একটি প্রোগ্রামিং ভাষা?
ঘটনাটি C++-এর লোককথার অংশ। ১৯৯৪ সালে C++ standard কমিটির একটি সভায় Erwin Unruh একটি ছোট প্রোগ্রাম দেখান, যা ইচ্ছা করেই compile হয় না। কিন্তু compiler যে error message-গুলো দেয়, সেগুলোতে ধারাবাহিকভাবে মৌলিক সংখ্যাগুলো ফুটে ওঠে। অর্থাৎ হিসাবটি প্রোগ্রাম চলার সময় হয়নি — প্রোগ্রাম তো চলেইনি — হয়েছে compiler-এর ভেতরে, template instantiation-এর সময়। Unruh দেখালেন, template-এর নিয়মগুলো (recursion-এর মতো একটি template নিজেকে অন্য argument দিয়ে instantiate করতে পারে, আর specialization দিয়ে থামার শর্ত বলা যায়) মিলে একটি পূর্ণাঙ্গ হিসাবের যন্ত্র তৈরি করে।
পরে Todd Veldhuizen-সহ কয়েকজন দেখান যে C++ template আসলে Turing-complete — অর্থাৎ নীতিগতভাবে যেকোনো গণনাযোগ্য হিসাব compile-এর সময়েই করা সম্ভব (compiler-এর নিজের সীমা, যেমন instantiation-এর গভীরতার সীমা, বাদ দিলে)। এই আবিষ্কার জন্ম দেয় template metaprogramming-এর: compile-এর সময় চলা প্রোগ্রাম, যা টাইপ আর ধ্রুবক নিয়ে কাজ করে। এর বাস্তব ফলও বড়। Boost-এর মতো লাইব্রেরি এই কৌশলে টাইপের তালিকা নিয়ে কাজ করার যন্ত্র বানায়; Eigen-এর মতো গাণিতিক লাইব্রেরি “expression templates” দিয়ে a + b + c-এর মতো matrix হিসাবকে compile-এর সময়েই একটি লুপে মিলিয়ে দেয়, মাঝের অস্থায়ী matrix ছাড়াই।
কিন্তু একটি দুর্ঘটনাক্রমে আবিষ্কৃত ভাষার দামও আছে। এর ব্যাকরণ হিসাবের জন্য বানানো হয়নি: লুপ নেই, শুধু recursion; ভ্যারিয়েবল নেই, শুধু টাইপ; আর ভুল হলে সেই কুখ্যাত error. compile-এর সময় বেড়ে যায় অনেক। তাই পরবর্তী C++ ধাপে ধাপে সহজ পথ দিয়েছে: C++11-এর constexpr ফাংশন (পর্ব ৩৯) দিয়ে সাধারণ C++ কোডই compile-এর সময় চালানো যায়, C++17-এর if constexpr শাখা বাছাই সহজ করেছে, আর C++20-এর concepts শর্ত লেখার জন্য পুরোনো কৌশলের (যেমন SFINAE) জটিলতা অনেকটাই সরিয়েছে।
মূল উপলব্ধি: template-এর হিসাবের ক্ষমতা পরিকল্পনা করে আনা হয়নি, আবিষ্কৃত হয়েছিল। এটি শক্তিশালী, কিন্তু আধুনিক C++-এ compile-time হিসাবের জন্য আগে constexpr ভাবুন; template metaprogramming রাখুন শুধু টাইপ নিয়ে কাজের জন্য।
প্র ০৪C++98-এ “export template” নামে একটি সুবিধা ছিল, যা template-কে .cpp-তে রাখতে দিত। সেটি কেন C++11-এ মুছে ফেলা হলো?
template-এর সংজ্ঞা header-এ রাখার বাধ্যবাধকতা শুরু থেকেই অনেকের অপছন্দ ছিল। এর ফলে প্রতিটি ফাইলে পুরো template-কোড ঢোকে, compile ধীর হয়, আর লাইব্রেরি-লেখক তাঁর বাস্তবায়ন লুকাতে পারেন না। তাই C++98 standard-এ যোগ হয় export শব্দ: export template <typename T> T largest(T, T); — অর্থ, “এই template-এর সংজ্ঞা অন্য একটি .cpp-তে আছে; দরকার হলে compiler সেখান থেকে খুঁজে নিয়ে instantiate করবে।” কাগজে এটি সাধারণ ফাংশনের মতোই সুন্দর আলাদা-compile ব্যবস্থা।
সমস্যা হলো বাস্তবায়নে। instantiation-এর জন্য compiler-কে এক ফাইল compile করার সময় অন্য ফাইলের template-এর পুরো প্রেক্ষাপট জানতে হয় — কোন নাম কোথায় ঘোষিত, কোন overload দেখা যায়। দুটি ফাইলের নামের জগৎ মেলানোর এই কাজ অবিশ্বাস্য জটিল হয়ে দাঁড়ায়। বহু বছরে মাত্র একটি দল এটি পুরোপুরি বানাতে পেরেছিল: Edison Design Group (EDG), যাদের C++ front end অনেক compiler-এর ভেতরে ব্যবহৃত হয়; Comeau C++-এর মতো কয়েকটি compiler তাদের মাধ্যমে export পেয়েছিল। EDG-র প্রকৌশলীরা পরে জানান, কাজটিতে কয়েক বছরের শ্রম লেগেছে, আর ফল হিসেবে compile-এর সময় বা কোড লুকানো — কোনোটিতেই প্রত্যাশিত লাভ মেলেনি। GCC, MSVC বা Clang কখনো এটি বানায়নি।
একটি সুবিধা যা প্রায় কেউ বানাতে পারে না, আর যারা বানিয়েছে তারাও ব্যবহারের পরামর্শ দেয় না — তা standard-এ রেখে লাভ নেই। তাই C++11-এ export template সরিয়ে দেওয়া হয়, তবে export শব্দটি ভবিষ্যতের জন্য সংরক্ষিত রাখা হয়। সেই শব্দটি ফিরে আসে C++20-এর modules-এ, সম্পূর্ণ ভিন্ন অর্থে: একটি module কোন নামগুলো বাইরে দেখাবে। modules-এ compiler প্রতিটি module একবার প্রক্রিয়া করে একটি কাঠামোবদ্ধ রূপে রাখে, তাই template-ভারী কোডের compile-সময়ের সমস্যা অনেকটাই কমে — export template যে স্বপ্ন দেখিয়েছিল, তার কাছাকাছি, কিন্তু ভিন্ন পথে।
আজকের ব্যবহারিক পথ তাই তিনটি: পুরো template header-এ (সবচেয়ে প্রচলিত); অল্প কয়েকটি জানা টাইপ হলে .cpp-তে explicit instantiation আর header-এ extern template; আর নতুন প্রজেক্টে modules, যেখানে compiler আর build system সমর্থন দেয়।
মূল উপলব্ধি: standard-এ লেখা থাকলেই একটি সুবিধা বাস্তব হয় না। export template শিখিয়েছে, ভাষার নকশায় বাস্তবায়নের খরচও একটি নকশা-প্রশ্ন — আর “header-এ template” নিয়মটি অলসতা নয়, compile মডেলের স্বাভাবিক ফল।
প্র ০৫Java আর C#-এর “generics” দেখতে C++-এর template-এর মতো। ভেতরে তারা কীভাবে আলাদা, আর প্রতিটির দাম কী?
তিনটি ভাষায় List<Integer> বা vector<int> লেখা প্রায় এক রকম, কিন্তু ভেতরের ব্যবস্থা তিন রকম। C++ করে instantiation: প্রতিটি টাইপের জন্য compile-এর সময় আলাদা কোড। vector<int> আর vector<double> মেশিন কোডে দুটি আলাদা জিনিস। লাভ — সর্বোচ্চ গতি, int সরাসরি int হিসেবেই থাকে, সব inline হতে পারে। দাম — code bloat, ধীর compile, template header-এ রাখা, আর চলার সময় নতুন টাইপের জন্য কোড বানানো যায় না।
Java generics যোগ করে ২০০৪ সালে, Java 5-এ — ভাষা চালু হওয়ার প্রায় এক দশক পরে। তখন কোটি কোটি লাইনের পুরোনো কোড আর লাখো JVM চালু ছিল, তাই নকশাকারদের প্রধান শর্ত ছিল পুরোনো কোডের সাথে সামঞ্জস্য। সমাধান হলো type erasure: compiler টাইপ যাচাই করে, তারপর টাইপের তথ্য মুছে ফেলে। চলার সময় List<String> আর List<Integer> একই List, ভেতরে Object-এর তালিকা। ফলে একটিই কোড সবার জন্য, কোনো bloat নেই। কিন্তু দামও আছে: int সরাসরি রাখা যায় না, Integer object-এ মোড়াতে (boxing) হয়, যা ধীর আর মেমরি খায়; generic কোডের ভেতরে new T() লেখা যায় না, কারণ চলার সময় T জানা নেই।
C# generics আসে ২০০৫ সালে, .NET Framework 2.0-তে, আর এখানে নকশাকাররা runtime নিজেই বদলে নেন। টাইপের তথ্য চলার সময়েও থাকে (reified generics). runtime-এর JIT compiler দরকার হলে টাইপ-প্রতি কোড বানায়: int বা double-এর মতো value type-এর জন্য আলাদা, দ্রুত কোড — অনেকটা C++-এর মতো; আর reference type-গুলোর জন্য একটি ভাগ করা কোড — অনেকটা Java-র মতো। মাঝামাঝি একটি পথ, যা সম্ভব হয়েছে কারণ C# একটি JIT-যুক্ত runtime-এর ওপর চলে।
আরেকটি গুরুত্বপূর্ণ পার্থক্য শর্তে। Java আর C#-এর generics-এ শর্ত শুরু থেকেই লিখিত (T extends Comparable, where T : IComparable) — তাই generic কোডের ভেতরে শুধু সেই কাজই করা যায়, আর error আসে ডাকার জায়গায়। C++ বহু বছর শর্ত ছাড়াই চলেছে, যার ফল নমনীয়তা আর দুর্বোধ্য error; C++20-এর concepts সেই ফাঁক পূরণ করছে, তবে inheritance না চেয়েই। নতুন ভাষা Rust-ও C++-এর মতো টাইপ-প্রতি কোড বানায় (monomorphization), কিন্তু শর্ত বাধ্যতামূলকভাবে লেখে।
মূল উপলব্ধি: generics-এর প্রতিটি নকশা একটি আপস: C++ গতি বেছে নিয়েছে, Java সামঞ্জস্য, C# একটি নতুন runtime-এর সুযোগ। কোন ভাষায় কী সম্ভব — new T(), primitive টাইপ সরাসরি রাখা, চলার সময় টাইপ জানা — তা এই এক সিদ্ধান্ত থেকেই আসে।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
আউটপুট অনুমান:
largesttemplate-টি নিন (T largest(T a, T b)).largest('a', 'b'),largest<int>('a', 'b')আরlargest(10u, 3u)— প্রতিটিতে T কী, আরprintln("{}", ...)দিয়ে কী দেখাবে?প্রথমটিতে T = char, ফল
b(অক্ষর হিসেবে দেখায়)। দ্বিতীয়টিতে T নিজে int বলে দেওয়া, তাই 'a' আর 'b' int-এ রূপান্তরিত হয় (ASCII 97 আর 98), ফল98. তৃতীয়টিতে দুটিই unsigned int literal, তাই T = unsigned int, ফল10. একই ছাঁচ থেকে তিনটি আলাদা ফাংশন তৈরি হলো: char, int আর unsigned int সংস্করণ। -
কোন টাইপ চলবে: শর্তহীন
largest-এ নিচের কোন টাইপগুলো compile হবে? (ক)long(খ)std::string(গ)std::vector<int>(ঘ)struct Point { int x, y; };(ঙ)std::complex<double>.(ক), (খ) আর (গ) চলবে, কারণ তিনটিতেই
<সংজ্ঞায়িত — vector-এর<উপাদানগুলো বর্ণানুক্রমের মতো (lexicographic) তুলনা করে। (ঘ) চলবে না, Point-এর<নেই, যদি না আপনি নিজে লেখেন (পর্ব ২৩)। (ঙ) চলবে না — জটিল সংখ্যার কোনো স্বাভাবিক “বড়-ছোট” নেই, তাই standard library ইচ্ছা করেই<দেয়নি। শিক্ষা: template-এর শর্ত শরীর থেকেই আসে;std::totally_orderedলিখলে এই তালিকা ঘোষণাতেই স্পষ্ট হতো। -
কতগুলো ফাংশন তৈরি হবে: একটি প্রোগ্রামে এই ডাকগুলো আছে —
largest(1, 2),largest(5, 9),largest(1.5, 2.5),largest<double>(1, 2),largest(s1, s2)(দুটিই std::string) আরlargest<long>(1, 2). template থেকে কয়টি আলাদা ফাংশন instantiate হবে?চারটি:
largest<int>(প্রথম দুটি ডাক মিলে একটি),largest<double>(তৃতীয় আর চতুর্থ ডাক — একটি deduce করা, একটি নিজে বলা, কিন্তু একই T),largest<std::string>আরlargest<long>. ডাকের সংখ্যা নয়, আলাদা T-এর সংখ্যাই ঠিক করে কয়টি ফাংশন তৈরি হবে। -
নিজে লিখুন (কোড): একটি function template
smallest_inলিখুন, যা যেকোনোstd::vector<T>-এর সবচেয়ে ছোট উপাদান ফেরত দেয় (vector খালি নয় ধরে নিন)। T-এর শর্তstd::totally_orderedদিয়ে লিখুন, আর int ও std::string — দুই রকম vector দিয়ে পরীক্ষা করুন।একটি সমাধান:
template <std::totally_ordered T>, তারপরT smallest_in(const std::vector<T>& v), ভেতরেT best = v[0];, একটি range-for-এif (x < best) best = x;, শেষেreturn best;.{40, 15, 70}দিলে 15, আর{"Mitu", "Anik", "Rafi"}দিলে Anik. vector খালি হলেv[0]undefined behavior — বাস্তব কোডে আগে পরীক্ষা করুন, বাstd::ranges::minব্যবহার করুন। ভুল টাইপ দিয়ে ডেকে দেখুন, error কত ছোট আসে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমC++-এ largest(3, 7) লেখা থেকে ফল পাওয়া পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- compiler ডাকটি দেখে, আর একই নামের function template খুঁজে পায়
- argument-এর টাইপ দেখে T deduce করে (T = int)
- ছাঁচে T-এর জায়গায় int বসিয়ে আসল ফাংশন instantiate করে
- তৈরি ফাংশনটি সাধারণ ফাংশনের মতো compile আর link হয়
- প্রোগ্রাম চলার সময় সাধারণ ডাক হিসেবে ফাংশনটি চলে আর 7 ফেরত দেয়
C++-এ template <typename T> T f(T a, T b); ঘোষিত আছে। f(3, 2.5) লিখলে কী হয়?
- T = double ধরে 3 কে 3.0 বানিয়ে ডাকা হয়
- T = int ধরে 2.5 কে 2 বানিয়ে ডাকা হয়
- Compile error — T-এর জন্য দুটি ভিন্ন টাইপ পাওয়া গেছে
- চলার সময় undefined behavior
C++ function template থেকে আসল ফাংশন কখন আর কতবার তৈরি হয়?
- প্রোগ্রাম চলার সময়, প্রতিটি ডাকে একবার
- compile-এর সময়, যে যে টাইপে ব্যবহৃত হয় তার প্রতিটির জন্য একটি
- compile-এর সময়, সব সম্ভাব্য টাইপের জন্য আগেভাগে
- একবারই, আর সব টাইপ সেই একটি ফাংশন ভাগ করে
C++-এ function template-এর পুরো সংজ্ঞা সাধারণত header ফাইলে রাখা হয় কেন?
- যে ফাইলে ডাকা হয়, সেখানেই compiler-কে পুরো শরীর দেখে নির্দিষ্ট টাইপের ফাংশন বানাতে হয়
- header-এ রাখলে প্রোগ্রাম চলার সময় দ্রুত হয়
- C++ standard .cpp ফাইলে template লেখা নিষিদ্ধ করেছে
- preprocessor শুধু header-এর template বোঝে
C/C++-এ #define MAX(a, b) ((a) > (b) ? (a) : (b)) আছে। int i = 6; int m = MAX(i++, 5);-এর পরে i-এর মান কত?
- 6
- 7
- 9
- 8
C++-এ কোনটি compile-time (static) polymorphism-এর উদাহরণ?
- base class-এর reference দিয়ে virtual ফাংশন ডাকা
- একটি function template-কে int আর std::string দিয়ে ডাকা
- dynamic_cast দিয়ে base pointer-কে derived-এ রূপান্তর
- std::unique_ptr-এর vector-এ নানা derived object রেখে ডাকা
C++-এ একই নামের একটি function template আর একটি সাধারণ (non-template) ফাংশন কোনো ডাকের সাথে সমান ভালোভাবে মেলে। কোনটি ডাকা হয়?
- function template-টি, কারণ সেটি বেশি সাধারণ
- Compile error — ambiguous call
- সাধারণ (non-template) ফাংশনটি
- যেটি ফাইলে আগে ঘোষিত হয়েছে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Template = ছাঁচ:
template <typename T>দিয়ে যুক্তি একবার লিখুন; compiler প্রতিটি ব্যবহৃত টাইপের জন্য আলাদা আসল ফাংশন বানায় — সব compile-এর সময়। - Deduction: compiler argument দেখে T খোঁজে; দুই argument দুই টাইপ বললে error —
largest<double>(3, 2.5)লিখে বা দুটি type parameter দিয়ে সমাধান। - macro নয়: macro টাইপ দেখে না আর argument দুবার হিসাব করতে পারে; template নিরাপদ আর সমান দ্রুত।
- header-এ পুরো সংজ্ঞা; টাইপ-প্রতি কপি মানে দ্রুত কোড, কিন্তু code bloat আর ধীর compile-এর ঝুঁকি।
- বিশেষ টাইপে overload, specialization নয়; T-এর শর্ত
requiresবা concept দিয়ে লিখুন; টাইপ compile-এর সময় জানা থাকলে template, চলার সময় ঠিক হলে virtual.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৯ · class templateপরবর্তী পর্বফাংশনের পরে class-এর ছাঁচ — নিজের generic container বানানো।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C প্রোগ্রামিং · Preprocessor ও MacroC কোর্সmacro-র পুরোনো পথ আর তার সব ফাঁদ — template কেন এল, তা আরও গভীরে বুঝতে।