সরাসরি পর্বে যান
পর্ব ৪১ · ৫৮-এর মধ্যে · মডিউল ৬

উন্নত template — variadic, fold expression, CRTP আর SFINAE

Advanced Templates — Variadic Packs, Fold Expressions, CRTP, SFINAE and Their Modern Replacements

Taught in Bangla, this lesson goes deeper into C++ templates: variadic templates, parameter packs and pack expansion, the four kinds of fold expressions, perfect forwarding with std::index_sequence and std::apply, CRTP for static polymorphism and mixins, C++23 deducing this, and type traits, SFINAE and enable_if compared with if constexpr and concepts.

এই পর্বে যা শিখবেন

  • parameter pack, pack expansion-এর “ছাঁচ” আর sizeof... — recursion বনাম fold expression
  • চার রকম fold, খালি pack-এর নিয়ম, perfect forwarding দিয়ে factory, std::index_sequence আর std::apply
  • CRTP দিয়ে static interface আর mixin, virtual-এর সাথে খরচের তুলনা, আর C++23-এর deducing this
  • type trait, SFINAE আর enable_if — একই overload set-এর SFINAE, if constexpr আর concept রূপ পাশাপাশি
  • template-এর error কেন লম্বা, compile-সময় আর code bloat, কখন এই কৌশল এড়াবেন, আর AI-এর লেখা template-কোড যাচাই

১ · বড় ছবি — template একটি compile-time কোড-কারখানা

পর্ব ২৮ আর ২৯-এ দেখেছেন, একটি template নিজে কোনো ফাংশন বা class নয়, বরং একটি ছাঁচ। আপনি largest(3, 7) ডাকলে compiler ছাঁচে T = int বসিয়ে একটি আসল ফাংশন বানায় — একে বলে instantiation. পর্ব ৩৯-এ দেখেছেন, compiler compile-এর সময়েই হিসাবও করতে পারে। আর পর্ব ৪০-এ concept দিয়ে এই ছাঁচের ওপর শর্ত বসাতে শিখেছেন।

এই তিনটি ধারণা মেলালে দাঁড়ায়: template আসলে একটি কোড-কারখানা, যা compile-এর সময় চলে। কাঁচামাল টাইপ আর compile-time মান, উৎপাদন আসল C++ কোড; চলার সময় কারখানার কোনো চিহ্ন থাকে না।

নীলক্ষেতের একটি ছাপাখানার কথা ভাবুন। বিয়ের কার্ডের একটি নকশা (ছাঁচ) তৈরি, শুধু নাম আর তারিখের জায়গা ফাঁকা। একশো পরিবার এলে একশোটি আলাদা কার্ড ছাপা হয়, কিন্তু নকশা একটিই। সাধারণ template এটুকুই করে। এবার ধরুন একটি পরিবার বলল, “আমাদের কার্ডে তিনজন অভিভাবকের নাম যাবে”, আরেকটি বলল, “আমাদের সাতজনের।” ভালো ছাপাখানা প্রতিটি সংখ্যার জন্য আলাদা নকশা বানায় না — নকশাটিই “যতজন আসবে, ততটি লাইন” ধরে রাখে। variadic template ঠিক এই নমনীয় নকশা। আর ছাপার আগেই যদি মেশিন যাচাই করে নেয় নামের ঘরে সংখ্যা বসানো হয়নি — সেটি concept বা SFINAE-এর কাজ।

এই পর্বের কৌশলগুলো কারখানাটিকে চারটি নতুন ক্ষমতা দেয়:

যত খুশি টাইপ

একটি সংজ্ঞা শূন্য, এক বা দশটি টাইপ নেয় — log("অর্ডার", id, "টাকা", amount). এটি variadic template.

variadic

টাইপের তালিকা নিয়ে হিসাব

সব argument-এর যোগফল, সব শর্ত সত্য কি না, কয়টি টাইপ — লুপ ছাড়াই, compile-এর সময় খুলে লেখা।

fold

virtual ছাড়া polymorphism

base class derived-এর ফাংশন ডাকে, কিন্তু কোন ফাংশন তা compile-এর সময়েই ঠিক — vtable লাগে না।

CRTP

ক্ষমতা দেখে overload বাছাই

“যে টাইপ সংখ্যা, তার জন্য এটি; যে টাইপ লেখা, তার জন্য ওটি।” পুরোনো পথ SFINAE, নতুন পথ concept.

SFINAE → concept

এগুলো শুধু তাত্ত্বিক খেলা নয়। প্রতিদিন যে standard library ব্যবহার করেন, তার বড় অংশ এই কৌশলেই লেখা: std::tuple আর std::variant যত খুশি টাইপ রাখে variadic template দিয়ে; std::make_unique আপনার argument-গুলো constructor-এ পৌঁছে দেয় perfect forwarding দিয়ে; std::format compile-এর সময়েই যাচাই করে format string-এর প্রতিটি {}-এর সাথে argument-এর টাইপ মেলে কি না; আর std::enable_shared_from_this একটি CRTP base. ভেতরটা বুঝলে library-র error পড়তে পারবেন।

মূল ধারণা

উন্নত template-কৌশল মানে compile-এর সময় কোড তৈরি করানো, যাতে চলার সময়ে কোনো বাড়তি খরচ না থাকে। দাম দিতে হয় compile-এর সময়ে আর পাঠযোগ্যতায়। তাই প্রতিটি কৌশলের সাথে এই পর্বে দেখবেন তার সহজ বিকল্প কী, আর কখন সেটিই যথেষ্ট।

C জানলে

C-তে “যত খুশি argument” মানে variadic function: printf-এর মতো ... আর <stdarg.h>-এর va_arg. সেখানে argument-এর টাইপ হারিয়ে যায় — ভুল %d দিলে compiler বড়জোর warning দেয়, চলার সময় আবর্জনা ছাপা হয়। C++-এর variadic template-এ প্রতিটি argument-এর টাইপ compile-এর সময় জানা থাকে, তাই ভুল টাইপে compile error. C-রীতি গভীরে দেখতে C কোর্সের কমান্ড-লাইন আর্গুমেন্ট ও variadic ফাংশন পর্ব পড়ুন।

২ · variadic template গভীরে — pack, expansion-এর ছাঁচ আর sizeof...

পর্ব ২৯-এ variadic template-এর সাথে প্রথম দেখা হয়েছিল। মনে করিয়ে দিই: typename... Ts হলো টাইপের একটি parameter pack — শূন্য বা তার বেশি টাইপের ব্যাগ। const Ts&... xs হলো মানের ব্যাগ। আর sizeof...(xs) বলে ব্যাগে কয়টি জিনিস — compile-এর সময়ের একটি সংখ্যা, চলার সময়ে গোনা নয়।

এবার আসল যন্ত্রটি বুঝুন: pack expansion. ব্যাগ নিজে কোনো কাজে লাগে না; কাজে লাগে যখন আপনি তাকে “খুলে দেন”। নিয়ম হলো: ...-এর বাঁ দিকে যে expression থাকে, সেটি একটি ছাঁচ। compiler ব্যাগের প্রতিটি জিনিসের জন্য ছাঁচটি একবার করে বসায়, আর মাঝে কমা দিয়ে জোড়ে। ধরুন xs ব্যাগে তিনটি মান a, b, c:

লেখাখুলে দাঁড়ায়ছাঁচটি কী
g(xs...)g(a, b, c)xs — শুধু নাম
g(f(xs)...)g(f(a), f(b), f(c))f(xs) — প্রতিটির ওপর f
g(f(xs...))g(f(a, b, c))ভেতরে খোলা, f একবারই
g(xs * 2 ...)g(a * 2, b * 2, c * 2)xs * 2
std::forward<Ts>(xs)...forward<A>(a), forward<B>(b), forward<C>(c)দুটি pack একসাথে, জোড়ায় জোড়ায়

শেষ সারিটি খেয়াল করুন: একটি ছাঁচে দুটি pack (Ts আর xs) থাকলে দুটি একসাথে, পাশাপাশি খোলে — প্রথম টাইপের সাথে প্রথম মান, দ্বিতীয়র সাথে দ্বিতীয়। তাই দুটির আকার সমান হতেই হবে। আর ... কোথায় বসছে, তাতে অর্থ পুরো বদলে যায় — দ্বিতীয় আর তৃতীয় সারি তুলনা করুন। AI-এর লেখা কোডে এই একটি বিন্দুর জায়গা ভুল হওয়া খুব সাধারণ।

C++17-এর আগে: recursion দিয়ে ব্যাগ খোলা

fold expression আসার আগে ব্যাগের প্রতিটি জিনিসের ওপর কাজ করার প্রচলিত উপায় ছিল recursion: “প্রথমটি নাও, কাজ করো, বাকি ব্যাগ দিয়ে নিজেকে আবার ডাকো।” ব্যাগ খালি হলে থামাতে একটি আলাদা “base case” লাগে। compiler একটি ডাককে কীভাবে কয়েকটি আলাদা ফাংশনে ভেঙে ফেলে, ধাপে ধাপে দেখুন:

1void print() { std::println(""); }          // base case2template <typename T, typename... Rest>3void print(const T& first, const Rest&... rest) {4    std::print("{} ", first);5    print(rest...);                         // pack expansion6}7print(1, "দুই", 3.0);
compiler-এর খাতা
এখনকার ডাক
T
Rest · sizeof...
তৈরি হওয়া ফাংশন
  1. প্রথম ডাক, প্রথম instantiation. print(1, "দুই", 3.0) দেখে compiler deduce করে: প্রথম argument থেকে T = int, বাকি দুটি থেকে Rest = {const char*, double}. এই টাইপগুলোর জন্য একটি আসল ফাংশন তৈরি হয়। লক্ষ করুন, template-এর প্রথম parameter আলাদা (T first) — এটিই “প্রথমটি নাও” অংশ।
  2. প্রথম জিনিসের কাজ। লাইন ৪ first ছাপে: 1. এখানে কোনো লুপ নেই; প্রতিটি সংস্করণ শুধু নিজের প্রথম জিনিসটি সামলায়।
  3. বাকি ব্যাগ দিয়ে নিজেকে ডাকা। লাইন ৫-এ print(rest...) খুলে দাঁড়ায় print("দুই", 3.0). এটি আগের ফাংশন নয় — compiler নতুন করে deduce করে T = const char*, Rest = {double}, আর দ্বিতীয় একটি ফাংশন বানায়। এটি ছাপে দুই.
  4. আরও এক ধাপ। এবার print(3.0): T = double, Rest খালি, sizeof...(Rest) = 0. তৃতীয় ফাংশন তৈরি হয়, ছাপে 3 (std::print 3.0-কে সবচেয়ে ছোট রূপে লেখে)।
  5. খালি ব্যাগ — base case. শেষ সংস্করণের লাইন ৫ খুলে দাঁড়ায় print() — কোনো argument নেই। template-টি অন্তত একটি argument চায়, তাই সে প্রার্থী নয়; ডাকটি যায় লাইন ১-এর সাধারণ ফাংশনে, যে শুধু নতুন লাইন দেয়। base case না থাকলে, বা template-এর পরে ঘোষিত হলে, error: no matching function for call to 'print()'.
  6. হিসাব। একটি ডাক থেকে তিনটি আলাদা template-ফাংশন, সাথে একটি base case. চলার সময় এটি আসলে একটি ছোট ডাকের শিকল, আর optimizer সাধারণত সবগুলো inline করে একটানা তিনটি print-এ নামিয়ে আনে। কিন্তু compile-এর সময় প্রতিটি ধাপ একটি আলাদা instantiation — বিশটি argument মানে বিশটি ফাংশন। fold expression ঠিক এই খরচ আর ঝামেলা কমায়।

recursion-এর ধাঁচ চিনে রাখুন, কারণ পুরোনো library-র error-এ এই শিকলটিই দেখবেন — “required from print<double>, required from print<const char*, double> …”. তবে শুধু “সবার ওপর একই কাজ” করতে হলে আধুনিক উত্তর একটিই: fold expression.

৩ · fold expression — চার রকম fold, এক লাইনে লুপ

C++17-এর fold expression একটি operator দিয়ে পুরো ব্যাগকে এক expression-এ “ভাঁজ” করে। ব্যাকরণে চারটি রূপ, আর বন্ধনী বাধ্যতামূলক:

নামলেখাxs = 1, 2, 3 হলে খুলে দাঁড়ায়
unary left fold(... + xs)((1 + 2) + 3)
unary right fold(xs + ...)(1 + (2 + 3))
binary left fold(init + ... + xs)(((init + 1) + 2) + 3)
binary right fold(xs + ... + init)(1 + (2 + (3 + init)))

মনে রাখার সহজ উপায়: ... যেদিকে, ভাঁজ শুরু হয় সেদিক থেকে। ... বাঁয়ে থাকলে বাঁ দিকের জোড়াটি আগে হিসাব হয় (left fold). unary fold-এ শুধু ব্যাগ থাকে; binary fold-এ সাথে একটি শুরুর মান (init) — যেমন (0 + ... + xs) বা (std::cout << ... << xs). প্রায় সব binary operator চলে: + - * / && || , আর <<, তুলনা, assignment ইত্যাদি। (... + xs)-এর ভেতরে ১, ২, ৩, ৪ কীভাবে হিসাব হয়, আর বাম-ডানের পার্থক্য কখন গুরুত্বপূর্ণ, দেখুন:

1template <typename... Ts>2auto sum(Ts... xs) { return (... + xs); }34std::println("{}", sum(1, 2, 3, 4));5// (... - xs) বনাম (xs - ...) ?
compiler যা লেখে
খুলে লেখা
এখন হিসাব
ফল
  1. compile-এর সময় খোলা। sum(1, 2, 3, 4) দেখে Ts = {int, int, int, int}. ... বাঁ দিকে, তাই compiler লেখে ((1 + 2) + 3) + 4 — বাঁ দিক থেকে বন্ধনী। কোনো লুপ বা recursion তৈরি হয় না; এটি একটিমাত্র expression.
  2. প্রথম জোড়া। সবচেয়ে ভেতরের বন্ধনী আগে: 1 + 2 = 3. left fold মানে ফলটি জমতে থাকে বাঁ দিকে — ঠিক যেমন খাতায় আপনি যোগ করেন।
  3. পরের জিনিস যোগ। জমা ৩-এর সাথে তৃতীয় মান: 3 + 3 = 6.
  4. শেষ জিনিস। 6 + 4 = 10. আউটপুট 10. চলার সময় এটি হুবহু তিনটি যোগের সমান কোড; optimizer চাইলে পুরোটা compile-এর সময়েই ১০ বানিয়ে ফেলতে পারে।
  5. দিক কখন গুরুত্বপূর্ণ। যোগ আর গুণে বন্ধনীর জায়গা বদলালে ফল বদলায় না, তাই left আর right fold একই। কিন্তু বিয়োগে: (... - xs) = ((1 − 2) − 3) − 4 = −8, আর (xs - ...) = 1 − (2 − (3 − 4)) = −2. ভাগ, string জোড়া বা <<-এর মতো operator-এ দিক বেছে নিন অর্থ বুঝে।

খালি ব্যাগ আর comma fold

ব্যাগ খালি হলে unary fold কী দেবে? মাত্র তিনটি operator-এর জন্য উত্তর নির্দিষ্ট: && দেয় true, || দেয় false, আর কমা দেয় “কিছুই না” (void())। বাকি সব — যেমন খালি ব্যাগে (... + xs) — compile error, কারণ শূন্য জিনিসের যোগফল int-এর ০ নাকি string-এর "", compiler জানে না। সমাধান binary fold-এ একটি init দেওয়া, অথবা অন্তত একটি argument বাধ্যতামূলক করা।

সবচেয়ে কাজের fold সম্ভবত comma operator-এর fold: (f(xs), ...). এর মানে “প্রতিটি জিনিসের ওপর f চালাও, বাঁ থেকে ডানে” — অর্থাৎ লুপ। comma operator ক্রমের নিশ্চয়তা দেয়, তাই ছাপা হয় ঠিক যে ক্রমে argument দেওয়া হয়েছে। আর ((xs > 0) && ...) লিখলে “সবাই ধনাত্মক কি না”, শর্টসার্কিটসহ — প্রথম মিথ্যাতেই থামে।

এবার দুটি বাস্তব কাজ একসাথে। একটি type-safe logger, যা যত খুশি আর যেকোনো টাইপের মান নেয়; আর একটি sum_all, যা int, double, এমনকি নিজের বানানো Taka টাইপের জন্যও চলে:

#include <print>

struct Taka {                                  // amount in paisa
    long long paisa;
    Taka operator+(Taka o) const { return {paisa + o.paisa}; }
};

template <typename... Ts>
void log_line(const Ts&... xs) {
    std::print("[LOG]");
    (std::print(" {}", xs), ...);              // comma fold
    std::println(" ({} fields)", sizeof...(xs));
}
template <typename T, typename... Ts>
auto sum_all(const T& first, const Ts&... rest) {
    return (first + ... + rest);               // binary left fold
}
int main() {
    log_line("order", 1042, "taka", 350.5);
    Taka bill = sum_all(Taka{105000}, Taka{2550}, Taka{75});
    log_line(sum_all(1, 2, 3, 4), sum_all(0.5, 1.25), bill.paisa);
}

আউটপুট দুই লাইন: [LOG] order 1042 taka 350.5 (4 fields) আর [LOG] 10 1.75 107625 (3 fields). log_line-এর comma fold প্রতিটি মান ক্রমানুসারে ছাপে, আর sizeof...(xs) compile-এর সময়েই জানে কয়টি মান এসেছে। প্রতিটি মানের টাইপ আলাদা (const char*, int, double), তবু প্রতিটির জন্য ঠিক format-নিয়ম বেছে নেওয়া হয়েছে compile-এর সময় — C-র printf-এর মতো %d ভুল হওয়ার সুযোগই নেই। sum_all একটি কৌশল দেখায়: প্রথম argument-কে আলাদা নিয়ে তাকেই binary fold-এর init বানানো হয়েছে — (first + ... + rest). এতে খালি ব্যাগের সমস্যা নেই (অন্তত একটি মান লাগবেই), আর init-এর জন্য 0 লিখতে হয়নি, যা Taka-র সাথে যোগ করা যেত না। Taka-র শুধু একটি operator+ আছে (পর্ব ২৩) — fold-এর এটুকুই দরকার। ১০৫০ + ২৫.৫০ + ০.৭৫ টাকা = ১০৭৬.২৫ টাকা, অর্থাৎ 107625 পয়সা। টাকা-পয়সা পূর্ণসংখ্যায় রাখা হয়েছে, কারণ double-এ পয়সার হিসাব গোলমেলে হয়।

নামের ফাঁদ: ফাংশনটির নাম log না রেখে log_line রাখা হয়েছে। <cmath>-এর গাণিতিক log(double) অনেক header-এর ভেতর দিয়ে global namespace-এ চলে আসে; একই নামের template থাকলে overload resolution অপ্রত্যাশিত ফাংশন বেছে নিতে পারে, বা ambiguous error দিতে পারে। নিজের কোডে খুব সাধারণ নাম এড়ান, অথবা নিজের namespace-এ রাখুন (পর্ব ১১)।

৪ · perfect forwarding, index_sequence আর একটি ছোট tuple

factory: যেমন এসেছে, তেমন পাঠাও

পর্ব ৩৬-এ perfect forwarding দেখেছেন: template-এর T&& parameter একটি forwarding reference, আর std::forward argument-কে তার মূল পরিচয়ে (lvalue হলে lvalue, temporary হলে rvalue) পাঠিয়ে দেয়। variadic template-এর সাথে মিলে এটিই make_unique-এর মতো সব factory-র হৃদয়:

template <typename T, typename... Args>
std::unique_ptr<T> make_xxx(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

std::string name = "Karim";
auto o = make_xxx<Order>(name, 3);   // name কপি হয়, 3 সরাসরি যায়; name অক্ষত

std::forward<Args>(args)... হলো ২ নম্বর অংশের টেবিলের শেষ সারি: দুটি pack জোড়ায় জোড়ায় খোলে। name একটি lvalue, তাই Args-এর প্রথম টাইপ std::string&, আর forward তাকে lvalue হিসেবেই পাঠায় — Order-এর constructor কপি নেয়, caller-এর name অক্ষত থাকে।

এখানে দুটি ভুল খুব সাধারণ। প্রথমটি forward ভুলে যাওয়া: new T(args...) লিখলে সব argument lvalue হয়ে যায় (নাম থাকলেই lvalue), ফলে temporary-ও কপি হয়, আর move-only টাইপে compile error. দ্বিতীয়টি আরও বিপজ্জনক: একই argument দুবার forward করা। temporary string দিলে প্রথম forward-এই string-টি move হয়ে খালি হয়, দ্বিতীয়বার যায় একটি moved-from খালি string. gcc-head-এ এমন একটি কোড চালিয়ে আমরা দেখেছি: কোনো warning নেই, আর দ্বিতীয় object-এর নাম ফাঁকা — [Rahim] []. নিয়ম: প্রতিটি argument একবারই forward করুন, আর সেটি হোক শেষ ব্যবহার।

std::index_sequence আর std::apply — tuple-এর প্রতিটি ঘরে কাজ

একটি std::tuple<int, const char*, double>-এর প্রতিটি ঘর ছাপাতে চান। tuple-এর ঘরে ঢোকার উপায় std::get<0>(t), std::get<1>(t) — সংখ্যাটি compile-time-এ জানা থাকতে হবে, লুপের i চলবে না। তাহলে লুপ ছাড়া ০, ১, ২ কোথা থেকে আসবে? উত্তর: সংখ্যার একটি parameter pack. std::index_sequence<0, 1, 2> একটি খালি টাইপ, যার একমাত্র কাজ ০, ১, ২ সংখ্যাগুলো টাইপের ভেতরে বয়ে নেওয়া। ফাংশন সেটি parameter হিসেবে নিলে compiler সংখ্যাগুলো একটি pack-এ deduce করে, আর তখন pack expansion চলে:

template <typename Tuple, std::size_t... I>
void print_tuple_impl(const Tuple& t, std::index_sequence<I...>) {
    ((std::print("{}{}", I == 0 ? "" : ", ", std::get<I>(t))), ...);
    std::println("");
}
template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
    print_tuple_impl(t, std::index_sequence_for<Ts...>{});   // <0, 1, 2>
}

print_tuple(std::make_tuple(1042, "Rahim", 350.5));   // 1042, Rahim, 350.5

নিজের কোডে এর দরকার খুব কম, কারণ standard library কাজটি একটি তৈরি ফাংশনে ভরে দিয়েছে: C++17-এর std::apply. std::apply(f, t) tuple-এর ঘরগুলো খুলে f-কে আলাদা আলাদা argument হিসেবে দেয় — ভেতরে ঠিক উপরের index_sequence-কৌশল। একটি generic lambda (পর্ব ৩৪) আর একটি fold মিলিয়ে উপরের পুরো কাজ এক লাইনে:

std::apply([](const auto&... xs) { (std::print("{} ", xs), ...); }, t);

variadic class template — একটি ছোট tuple-এর ভাবনা

std::tuple নিজে কীভাবে লেখা? একটি সরল ধারণা (আসল বাস্তবায়ন আরও জটিল): খালি ব্যাগের জন্য একটি খালি struct, আর “প্রথম টাইপ + বাকি ব্যাগ”-এর জন্য একটি struct, যা বাকি ব্যাগের tuple থেকে inherit করে আর নিজে প্রথম মানটি রাখে:

template <typename... Ts> struct TinyTuple;          // শুধু ঘোষণা
template <> struct TinyTuple<> {};                     // খালি ব্যাগ: থামো

template <typename Head, typename... Tail>
struct TinyTuple<Head, Tail...> : TinyTuple<Tail...> {
    Head value;
    TinyTuple(Head h, Tail... t) : TinyTuple<Tail...>(t...), value(h) {}
};
// TinyTuple<int, double, char>  →  ভেতরে int, তার base-এ double, তার base-এ char

এটি ২ নম্বর অংশের recursion-ই, শুধু ফাংশনের বদলে টাইপে: পর্ব ২৯-এর partial specialization দিয়ে “প্রথম আর বাকি” ভাগ, প্রতিটি স্তরে একটি মান।

template template parameter — এক ঝলক

কখনো parameter হিসেবে একটি টাইপ নয়, একটি template দিতে চান — “কোন container, তা caller ঠিক করবে”:

template <template <typename...> class Container, typename T>
Container<T> make_filled(int n, const T& v) { return Container<T>(n, v); }

auto v = make_filled<std::vector>(3, 7);   // std::vector<int>{7, 7, 7}
auto d = make_filled<std::deque>(2, 1.5);  // std::deque<double>

এখানে Container একটি template template parameter: caller লেখে std::vector, std::vector<int> নয় — বাকি টাইপ ফাংশন নিজে বসায়। policy-based design-এ এটি দেখবেন; দৈনন্দিন কোডে কম।

৫ · CRTP — virtual ছাড়া polymorphism

পর্ব ২৫-এ virtual ফাংশন দিয়ে polymorphism দেখেছেন: base class-এর pointer দিয়ে area() ডাকলে চলার সময় vtable দেখে ঠিক derived-এর ফাংশন চলে। এর দাম: প্রতিটি object-এ একটি লুকানো vptr, প্রতিটি ডাকে একটি পরোক্ষ লাফ, আর সাধারণত inline হয় না। কিন্তু অনেক ক্ষেত্রে compile-এর সময়েই জানা থাকে কোন টাইপ — তাহলে চলার সময় খোঁজাখুঁজি কেন?

CRTP (Curiously Recurring Template Pattern — “অদ্ভুতভাবে ফিরে আসা template-এর ধাঁচ”) এই প্রশ্নের একটি উত্তর। কৌশলটি প্রথম দেখায় অদ্ভুত: derived class নিজেকেই base-এর template argument হিসেবে দেয় — struct Circle : Shape<Circle>. তাতে base জানে তার derived কে, আর static_cast দিয়ে নিজেকে derived-এ রূপান্তর করে সরাসরি derived-এর ফাংশন ডাকতে পারে:

#include <print>

template <typename Derived>
struct Shape {                                    // CRTP base
    double area() const {                         // static dispatch
        return static_cast<const Derived&>(*this).area_impl();
    }
};
struct Circle : Shape<Circle> {
    double r;
    explicit Circle(double radius) : r(radius) {}
    double area_impl() const { return 3.14159 * r * r; }
};
struct Rect : Shape<Rect> {
    double w, h;
    Rect(double width, double height) : w(width), h(height) {}
    double area_impl() const { return w * h; }
};
template <typename D>
void show(const char* label, const Shape<D>& s) { std::println("{}: {:.2f}", label, s.area()); }
static_assert(sizeof(Circle) == sizeof(double));  // no vptr inside
int main() { show("circle", Circle{2.0}); show("rect", Rect{3.0, 4.5}); }

আউটপুট দুই লাইন: circle: 12.57 আর rect: 13.50. Shape<Circle>::area() জানে, এই object আসলে একটি Circle — কারণ Circle নিজেই সেটা template argument-এ বলে দিয়েছে। তাই static_cast<const Circle&>(*this) নিরাপদ, আর area_impl()-এর ডাক একটি সাধারণ, সরাসরি ডাক, যা optimizer inline করতে পারে। show একটি template, যা যেকোনো Shape<D> নেয় — এটিই “static interface”: সব shape-এর মুখ একই (area()), কিন্তু কোনটি চলবে তা compile-এর সময় ঠিক। আর static_assert লাইনটি প্রমাণ দেয়: Circle-এর আকার শুধু একটি double-এর সমান, ভেতরে কোনো vptr নেই। virtual দিয়ে লিখলে সাধারণ 64-bit সিস্টেমে আকার হতো ১৬ byte.

compiler এই ডাক কীভাবে সমাধান করে, ধাপে ধাপে:

  1. ডাকshow(c)D = Circle
  2. baseShape<Circle>::areainstantiate
  3. caststatic_castcompile-time
  4. derivedCircle::area_implসরাসরি ডাক
deductionconst Shape<D>& ← Circle
show("circle", Circle{2.0});

Circle-এর base হলো Shape<Circle>
→ const Shape<D>& মেলাতে D = Circle
→ show<Circle> তৈরি হলো
base-এর কোড তৈরিDerived = Circle
double Shape<Circle>::area() const {
    return static_cast<const Circle&>(*this).area_impl();
}
// Derived শব্দটির জায়গায় এখন আসল নাম Circle
castচলার সময় কোনো যাচাই নেই
*this-এর টাইপ: const Shape<Circle>&
static_cast → const Circle&

base থেকে derived-এ static_cast:
টাইপ compile-এর সময় জানা, কোনো খরচ নেই।
(object সত্যিই Circle না হলে UB — তাই নিয়ম মানা জরুরি)
সরাসরি ডাকinline হতে পারে
Circle::area_impl() → 3.14159 * r * r

optimizer প্রায়ই পুরো শিকলটি গলিয়ে দেয়:
show → area → area_impl  ⇒  একটি গুণ
আউটপুট: circle: 12.57
virtual হলেতুলনা
s.area()  →  object-এর vptr পড়ো
          →  vtable থেকে area-র ঠিকানা
          →  পরোক্ষ ডাক (সাধারণত inline নয়)

CRTP:  কোনো vptr নেই, কোনো খোঁজ নেই
দাম:   প্রতিটি D-এর জন্য আলাদা show<D>;
       Circle আর Rect এক vector-এ রাখা যায় না
  1. deduction. show-এর parameter const Shape<D>&. Circle-এর base class Shape<Circle>, তাই compiler মিলিয়ে নেয় D = Circle. পর্ব ২৮-এর deduction নিয়ম derived থেকে base template-এও কাজ করে।
  2. base-এর কোড তৈরি। Shape<Circle>-এর area() instantiate হয়। Derived-এর জায়গায় এখন Circle, তাই শরীরের area_impl() কোন ফাংশন, compiler এখনই জানে। লক্ষ করুন, Shape লেখার সময় Circle-এর অস্তিত্বই ছিল না — template-এর শরীর instantiate হয় ব্যবহারের সময়, তাই এটি সম্ভব।
  3. compile-time cast. static_cast base-এর reference-কে derived-এর reference বানায়। এর জন্য চলার সময় কোনো কাজ হয় না। কিন্তু দায়িত্ব আপনার: কেউ struct Rect : Shape<Circle> লিখে ভুল নাম দিলে cast মিথ্যা বলবে, আর ফল undefined behavior.
  4. সরাসরি ডাক। Circle::area_impl() একটি সাধারণ member function-এর ডাক। কোনো পরোক্ষতা না থাকায় optimizer পুরো শিকল — show, area, area_impl — একসাথে inline করে একটি গুণে নামিয়ে আনতে পারে। এটিই static polymorphism.
  5. virtual-এর সাথে তুলনা। virtual-এ প্রতিটি ডাকে vptr পড়া, vtable থেকে ঠিকানা আনা আর পরোক্ষ ডাক। CRTP-তে সেসব নেই। দামটি অন্য জায়গায়: প্রতিটি টাইপের জন্য আলাদা ফাংশন তৈরি হয়, আর Shape<Circle> ও Shape<Rect> সম্পূর্ণ আলাদা টাইপ — কোনো সাধারণ base নেই, তাই এক তালিকায় রাখা যায় না।

mixin — একবার লিখুন, অনেক class-এ মেশান

CRTP-র দ্বিতীয় বড় ব্যবহার mixin: base class এমন সুবিধা দেয়, যা derived-এর কোনো কাজের ওপর ভিত্তি করে তৈরি। দুটি পরিচিত উদাহরণ:

// ১. Comparable: শুধু < দিন, বাকি তুলনা বিনামূল্যে
template <typename D>
struct Comparable {
    friend bool operator>(const D& a, const D& b)  { return b < a; }
    friend bool operator<=(const D& a, const D& b) { return !(b < a); }
    friend bool operator>=(const D& a, const D& b) { return !(a < b); }
};
struct Marks : Comparable<Marks> {
    int v;
    friend bool operator<(const Marks& a, const Marks& b) { return a.v < b.v; }
};

// ২. প্রতিটি টাইপের জন্য আলাদা গণনা: কয়টি object এখন বেঁচে আছে
template <typename T>
struct Counted {
    static inline int alive = 0;
    Counted() { ++alive; }
    Counted(const Counted&) { ++alive; }
    ~Counted() { --alive; }
};
struct Order : Counted<Order> {};
struct Rider : Counted<Rider> {};
// Order a, b; Rider r;  →  Counted<Order>::alive == 2, Counted<Rider>::alive == 1

দ্বিতীয় উদাহরণে CRTP-র “জাদু”টি একটু অন্য রকম: base-এর ভেতরে T ব্যবহারই হয়নি! তবু Counted<Order> আর Counted<Rider> দুটি আলাদা টাইপ, তাই প্রতিটির নিজস্ব alive — পর্ব ২৯-এ দেখেছেন, প্রতিটি instantiation-এর static member আলাদা। একটি সাধারণ Counted base থাকলে সব টাইপ মিলিয়ে একটিই গণনা হতো। (copy constructor-এ ++alive না দিলে কপি করা object গোনা হতো না, অথচ destructor-এ কমত — গণনা ঋণাত্মক হয়ে যেত। mixin-এও Rule of 0/3/5 ভুলবেন না।)

প্রথম উদাহরণটি C++20-এর আগে খুব প্রচলিত ছিল — Boost.Operators নামে এর একটি পূর্ণ library-ও আছে। আজ পর্ব ২৩-এর operator<=> একটি লাইনে ছয়টি তুলনাই দেয়, তাই নতুন কোডে এর দরকার নেই — কিন্তু ধারণাটি থেকে যায়: base class derived-এর সামর্থ্যের ওপর নতুন সামর্থ্য গড়ে, virtual ছাড়াই। C++20-এর std::ranges::view_interface<D> ঠিক এভাবে আপনার view-কে empty(), front() ইত্যাদি দেয়।

প্রশ্নCRTPvirtual
কোন ফাংশন চলবে, কখন ঠিক হয়compile-এর সময়চলার সময়
object-এ বাড়তি জিনিসকিছু নেইএকটি vptr
ডাকের খরচসরাসরি, প্রায়ই inlineপরোক্ষ, সাধারণত inline নয়
নানা টাইপ এক container-এনা (সাধারণ base নেই)হ্যাঁ, base pointer দিয়ে
নতুন টাইপ চলার সময় যোগ (plugin)নাহ্যাঁ
কোডের আকারটাইপ-প্রতি আলাদা কপিএকটি কপি
পড়তে কেমনঅচেনাদের কাছে অদ্ভুতসবাই চেনে

একটি সতর্কতা: “CRTP দ্রুত” — এই দাবি সব জায়গায় সত্য নয়। আধুনিক compiler প্রায়ই virtual ডাকও সরাসরি করে ফেলে, যখন টাইপ নিশ্চিত জানা থাকে (devirtualization). CRTP-র আসল জায়গা হলো খুব ছোট, লাখো বার ডাকা ফাংশন (গণিত library, game engine-এর ভেতরের লুপ), বা mixin. মাপুন আগে, তারপর বদলান (পর্ব ৫২)।

৬ · deducing this — C++23-এর সহজ CRTP

CRTP কাজ করে, কিন্তু তার ব্যাকরণ ভারী: base-কে template বানাও, derived-কে নিজের নাম পাঠাতে মনে রাখো, প্রতিটি জায়গায় static_cast<const Derived&>(*this) লেখো। C++23 এর একটি সহজ বিকল্প দিয়েছে: deducing this, আনুষ্ঠানিক নাম explicit object parameter. member function-এর প্রথম parameter হিসেবে this লিখে object-টিকে একটি সাধারণ, deduce করা parameter হিসেবে নেওয়া যায়:

struct ShapeBase {                         // কোনো template নেই!
    void report(this const auto& self) {   // self-এর টাইপ deduce হয়
        std::println("area {:.1f}", self.area_impl());
    }
};
struct Square : ShapeBase {
    double s;
    double area_impl() const { return s * s; }
};

Square q; q.s = 3;
q.report();          // self = const Square&  →  area 9.0

q.report() ডাকলে self deduce হয় const Square& — base-এর ফাংশন হলেও, আসল object-এর টাইপে। তাই self.area_impl() সরাসরি Square-এর ফাংশন ডাকে। CRTP-র মতোই static dispatch, কিন্তু base template নয়, derived-কে নিজের নাম পাঠাতে হয় না, cast নেই — ভুল নাম দিয়ে UB ঘটানোর সুযোগও নেই। আমরা gcc-head-এ -std=c++2b দিয়ে কোডটি চালিয়ে দেখেছি; GCC 14, Clang 18 আর MSVC-র সাম্প্রতিক সংস্করণ এটি সমর্থন করে।

এর আরেকটি লাভ: const আর non-const দুটি সংস্করণ একবার this auto&& self লিখেই সামলানো যায়। তবে সব CRTP এতে বদলায় না: Counted-এর মতো mixin, যেখানে প্রতিটি টাইপের জন্য আলাদা static data চাই, এখনো CRTP দিয়েই লিখতে হয় — deducing this ফাংশনকে template বানায়, class-কে নয়।

৭ · type trait আর SFINAE — concept-এর আগের যুগে overload বাছাই

type trait: টাইপ সম্পর্কে compile-time প্রশ্ন

<type_traits> header-এ শতাধিক ছোট template আছে, যাদের বলে type trait. প্রতিটি একটি টাইপ সম্পর্কে একটি প্রশ্নের উত্তর দেয়, বা একটি টাইপ থেকে আরেকটি টাইপ বানায় — সব compile-এর সময়:

traitকী দেয়উদাহরণ
std::is_integral_v<T>bool: T পূর্ণসংখ্যার টাইপ কি নাis_integral_v<long> = true, <double> = false
std::is_arithmetic_v<T>bool: পূর্ণসংখ্যা বা দশমিক<float> = true
std::is_same_v<A, B>bool: দুটি হুবহু একই টাইপ কি না<int, int32_t> সাধারণত true
std::conditional_t<B, X, Y>টাইপ: B সত্য হলে X, না হলে Yconditional_t<(N < 256), uint8_t, uint32_t>
std::remove_cvref_t<T>টাইপ: const আর reference ছেঁটেconst int& → int
std::enable_if_t<B, T>টাইপ: B সত্য হলে T; মিথ্যা হলে কোনো টাইপই নেইenable_if_t<true, int> = int

_v আর _t শেষাংশ C++14/17-এর সংক্ষেপ: std::is_integral_v<T> মানে std::is_integral<T>::value, আর std::conditional_t<...> মানে typename std::conditional<...>::type. ভেতরে এরা পর্ব ২৯-এর specialization দিয়ে লেখা — যেমন primary template-এ value = false, আর int, long ইত্যাদির specialization-এ value = true. শেষ সারির enable_if অদ্ভুত: শর্ত মিথ্যা হলে সে ইচ্ছা করে “ভাঙা” — তার ভেতরে type নামে কিছুই থাকে না। এই অদ্ভুততাই পুরো একটি যুগের কৌশল।

SFINAE: substitution ব্যর্থ হলে error নয়

overload resolution-এর সময় compiler প্রতিটি template প্রার্থীর ঘোষণায় deduce করা টাইপ বসিয়ে দেখে (substitution). বসাতে গিয়ে যদি একটি অবৈধ টাইপ তৈরি হয় — যেমন enable_if<false, int>::type, যা নেই — তাহলে সেটি error নয়; প্রার্থীটি শুধু তালিকা থেকে চুপচাপ বাদ পড়ে। এই নিয়মের নাম SFINAE — Substitution Failure Is Not An Error. নিয়মটি কেন এসেছিল, তার ইতিহাস পর্ব ৪০-এর ভাবনার প্রশ্ন ০৩-এ আছে। এখানে দেখুন কৌশলটি কীভাবে কাজ করে। একটি to_string_any, যা সংখ্যাকে std::to_string দিয়ে আর লেখার মতো জিনিসকে সরাসরি string বানায় — C++17-এর SFINAE রীতিতে:

// C++17: SFINAE দিয়ে দুটি overload
template <typename T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
std::string to_string_any(const T& x) { return std::to_string(x); }

template <typename T, std::enable_if_t<std::is_convertible_v<const T&, std::string>, int> = 0>
std::string to_string_any(const T& x) { return std::string(x); }

to_string_any(42);     // "42"  — প্রথমটি টিকে থাকে, দ্বিতীয়টি বাদ
to_string_any("hi");   // "hi"  — প্রথমটি বাদ, দ্বিতীয়টি টিকে থাকে

শর্তটি লুকিয়ে আছে একটি নামহীন template parameter-এ: std::enable_if_t<শর্ত, int> = 0. শর্ত সত্য হলে এটি দাঁড়ায় int = 0 — একটি নিরীহ, অব্যবহৃত parameter. মিথ্যা হলে টাইপটিই নেই, substitution ব্যর্থ, প্রার্থী বাদ। (পর্ব ৪০-এর AI অংশে দেখেছেন, typename = enable_if_t<...> রূপে দুটি overload লিখলে compiler “একই template দুবার” বলে আপত্তি করে; int = 0 রূপটি সেই ফাঁদ এড়ায়।) একটি অসমর্থিত টাইপ — std::vector<int> — দিলে কী হয়, আর একই overload set concept দিয়ে লিখলে কী বদলায়, ধাপে ধাপে:

দুটি প্রার্থীv হলো std::vector<int>
ক: template <T, enable_if_t<is_arithmetic_v<T>, int> = 0>
খ: template <T, enable_if_t<is_convertible_v<const T&, string>, int> = 0>

to_string_any(v);
→ দুজনেরই T = std::vector<int> (deduction সফল)
ক-তে substitutionঘোষণায় T বসানো
is_arithmetic_v<vector<int>>      → false
enable_if_t<false, int>            → enable_if<false,int>::type
                                     ... এমন কিছু নেই!
substitution failure → error নয়
→ ক চুপচাপ বাদ
খ-তে substitutionএকই ঘটনা
is_convertible_v<const vector<int>&, string> → false
enable_if_t<false, int> → টাইপ নেই
→ খ-ও চুপচাপ বাদ

প্রার্থী-তালিকা এখন খালি
SFINAE-র errorgcc-head, ছাঁটা
error: no matching function for call to
       'to_string_any(std::vector<int>&)'
  • there are 2 candidates
    • candidate 1: 'template<class T, typename
      std::enable_if<is_arithmetic_v<T>, int>::type
      <anonymous> > std::string to_string_any(const T&)'
      • template argument deduction/substitution failed:
        • error: no type named 'type' in
          'struct std::enable_if<false, int>'
    • candidate 2: ... (একই কথা)
concept রূপের errorgcc-head, ছাঁটা
template <typename T> requires std::is_arithmetic_v<T>  ...
template <std::convertible_to<std::string> T>          ...

error: no matching function for call to
       'to_string_any(std::vector<int>&)'
    • candidate 1: ... requires is_arithmetic_v<T> ...
      • constraints not satisfied
        • the expression 'is_arithmetic_v<T>
          [with T = std::vector<int>]' evaluated to 'false'
যা SFINAE নয়শরীরের ভেতরের ভুল
template <typename T>
std::string bad(const T& x) { return std::to_string(x); }
//                          ^ শরীরে ভুল — ঘোষণায় নয়

bad(v);   // প্রার্থী বাদ পড়ে না!
          // শরীর instantiate হয় → hard error,
          // to_string-এর সব overload-এর লম্বা তালিকা
  1. দুটি প্রার্থী, deduction সফল। দুটি template-এরই parameter const T&, তাই দুটিতেই T = std::vector<int>. এখন পর্যন্ত কোনো সমস্যা নেই। শর্তগুলো এখনো হিসাব হয়নি — সেগুলো লুকিয়ে আছে দ্বিতীয় template parameter-এর টাইপে।
  2. ক বাদ — চুপচাপ। compiler ক-এর ঘোষণায় T বসায়। is_arithmetic_v মিথ্যা, তাই enable_if_t<false, int> — একটি টাইপ, যা নেই। এটি substitution failure; SFINAE নিয়মে error নয়, ক শুধু প্রার্থী-তালিকা থেকে বাদ পড়ে। to_string_any(42)-এর মতো ডাকে এই নীরবতাই কাম্য — ভুল প্রার্থী সরে যায়, সঠিকটি জেতে।
  3. খ-ও বাদ। vector string-এ রূপান্তর হয় না, তাই খ-এর ঘোষণাও ভাঙে, খ-ও বাদ। এবার তালিকা খালি — আর তখনই error.
  4. SFINAE-র বার্তা। gcc-head (GCC-এর development সংস্করণ, যা নতুন ধাপে ধাপে সাজানো বার্তা দেয়) বলে কোনো মানানসই ফাংশন নেই, তারপর প্রতিটি প্রার্থীর জন্য: “no type named 'type' in 'struct std::enable_if<false, int>'”. কথাটি সত্য, কিন্তু আসল কারণ — “vector সংখ্যা নয়, string-এ রূপান্তরও হয় না” — আপনাকে ঘোষণা পড়ে বের করতে হবে। বাদ পড়ার কারণ লুকিয়ে আছে একটি ভাঙা টাইপের পেছনে।
  5. concept-এর বার্তা। একই দুটি overload concept দিয়ে লিখলে বাছাইয়ের ফল হুবহু এক — দুজনেই বাদ। কিন্তু এবার বার্তা বলে “constraints not satisfied”, আর কোন expression কোন টাইপে মিথ্যা হলো: is_arithmetic_v<T> [with T = std::vector<int>] evaluated to 'false'. কারণটি সরাসরি লেখা। সাথে বাড়তি লাভ: পর্ব ৪০-এর subsumption — বেশি নির্দিষ্ট শর্ত জেতে — যা SFINAE-তে ছিল না।
  6. যা SFINAE নয়। SFINAE শুধু ঘোষণার (এবং template parameter-এর) “তাৎক্ষণিক প্রসঙ্গে” কাজ করে। শর্তহীন একটি template-এর শরীরে std::to_string(x) ভুল হলে প্রার্থী বাদ পড়ে না — compiler তাকে বেছে নেয়, শরীর instantiate করে, আর তখন hard error, সাথে to_string-এর প্রতিটি overload কেন মেলেনি তার লম্বা তালিকা। তাই শর্ত সবসময় ঘোষণায় রাখুন — enable_if দিয়ে হোক বা concept দিয়ে।

৮ · একই কাজ তিনভাবে — SFINAE, if constexpr আর concept

এবার একটি পূর্ণ overload set নিন: to_string_any সংখ্যা, লেখা, আর যেসব object-এর নিজের to_string() member আছে — তিন রকম টাইপের জন্য। একই আচরণ তিনভাবে লেখা, পাশাপাশি:

template <typename T, typename = void>
struct has_to_string : std::false_type {};
template <typename T>
struct has_to_string<T, std::void_t<decltype(std::declval<const T&>().to_string())>>
    : std::true_type {};

template <typename T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
std::string to_string_any(const T& x) { return std::to_string(x); }

template <typename T, std::enable_if_t<std::is_convertible_v<const T&, std::string>, int> = 0>
std::string to_string_any(const T& x) { return std::string(x); }

template <typename T, std::enable_if_t<has_to_string<T>::value, int> = 0>
std::string to_string_any(const T& x) { return x.to_string(); }

“member আছে কি না” জানতে নিজের একটি trait লিখতে হয়েছে, void_t আর declval-এর কৌশল দিয়ে — পাঁচ লাইন, যার অর্থ বুঝতে অভিজ্ঞ প্রোগ্রামারকেও থামতে হয়। আর যদি কোনো টাইপ দুটি শর্তই মানে (যেমন যার to_string() আছে আবার string-এ রূপান্তরও হয়), তাহলে ambiguous — শর্তগুলো হাতে পরস্পরবিরোধী করে লিখতে হয়।

template <typename T>
std::string to_string_any(const T& x) {
    if constexpr (std::is_arithmetic_v<T>)
        return std::to_string(x);
    else if constexpr (std::is_convertible_v<const T&, std::string>)
        return std::string(x);
    else if constexpr (requires { x.to_string(); })      // C++20 requires
        return x.to_string();
    else
        static_assert(false, "to_string_any: unsupported type");   // C++23
}

একটি ফাংশন, উপর থেকে নিচে পড়ার মতো শাখা। পর্ব ৩৯-এ দেখেছেন, বাদ পড়া শাখা instantiate হয় না, তাই int-এর জন্য x.to_string() লেখা থাকলেও error নেই। ক্রমটাই অগ্রাধিকার — ambiguous হওয়ার সুযোগ নেই। শেষ শাখার static_assert(false, ...) বাদ পড়া শাখায় C++23 থেকে বৈধ, আর অসমর্থিত টাইপে ঠিক আপনার লেখা বার্তাটি দেখায়। দুর্বলতা: নতুন টাইপের জন্য নতুন শাখা যোগ করতে এই ফাংশনটিই খুলতে হয়।

template <typename T>
concept HasToString = requires(const T& x) {
    { x.to_string() } -> std::convertible_to<std::string>;
};

template <typename T> requires std::is_arithmetic_v<T>
std::string to_string_any(const T& x) { return std::to_string(x); }

std::string to_string_any(const std::convertible_to<std::string> auto& x) { return std::string(x); }

std::string to_string_any(const HasToString auto& x) { return x.to_string(); }

নামহীন parameter-এর কৌশল নেই, void_t নেই — শর্ত পড়া যায় ইংরেজি বাক্যের মতো। প্রতিটি overload আলাদা, তাই অন্য কেউ নিজের ফাইলে নতুন একটি overload যোগ করতে পারে, পুরোনোটি না ছুঁয়ে। error-এ শর্তের নাম আসে। (দুটি শর্তই মানে এমন টাইপে এখানেও ambiguous হতে পারে — subsumption কাজ করে শুধু তখন, যখন একটি concept আরেকটির ওপর গড়া, পর্ব ৪০ দেখুন।)

কোনটি কখন? একটি সরল নিয়ম: নতুন কোডে SFINAE নয়। শাখাগুলো ছোট আর একই কাজের অংশ হলে if constexpr; আলাদা টাইপের জন্য আলাদা আচরণ, যা ভবিষ্যতে বাড়বে — concept-যুক্ত overload. SFINAE পড়তে পারা দরকার, কারণ C++20-এর আগের বিপুল কোড আর যেসব library এখনো C++17 সমর্থন করে, সেখানে এটিই আছে। কিন্তু লেখার জন্য আজ এটি প্রায় কখনো সেরা উত্তর নয়।

template দিয়ে compile-time হিসাব বনাম constexpr

C++11-এর আগে compile-time-এ হিসাব করার একমাত্র উপায় ছিল template-কে পুনরাবৃত্তি করানো — যাকে বলে template metaprogramming:

// পুরোনো পথ: template-এর recursion
template <unsigned N> struct Factorial { static const unsigned long value = N * Factorial<N - 1>::value; };
template <>           struct Factorial<0> { static const unsigned long value = 1; };
static_assert(Factorial<10>::value == 3628800);

// আধুনিক পথ (পর্ব ৩৯): সাধারণ লুপ, compile-এর সময়েই চলে
constexpr unsigned long factorial(unsigned n) {
    unsigned long r = 1;
    for (unsigned i = 2; i <= n; ++i) r *= i;
    return r;
}
static_assert(factorial(10) == 3628800);

দুটিই compile-এর সময় ৩৬২৮৮০০ বের করে। কিন্তু প্রথমটিতে প্রতিটি N-এর জন্য একটি আলাদা struct instantiate হয় (Factorial<10>, <9>, … <0>), লুপের বদলে recursion, if-এর বদলে specialization — একটি পুরো ভাষা, যা কখনো ভাষা হিসেবে নকশা করা হয়নি। দ্বিতীয়টি সাধারণ C++, যা চলার সময়েও ডাকা যায় আর debug করা যায়। মান নিয়ে হিসাব হলে constexpr; শুধু টাইপ নিয়ে কাজ হলে (যেমন “এই টাইপের তালিকায় int আছে কি?”) template আর trait. এই বিভাজনই আধুনিক নিয়ম।

error কেন লম্বা, আর concept কতটা ছোট করে

template-এর error লম্বা হয় তিন কারণে। প্রথমত, ভুলটি ধরা পড়ে instantiation-এর গভীরে, আর compiler পুরো পথটি দেখায় — “required from … required from …”. দ্বিতীয়ত, overload resolution ব্যর্থ হলে compiler প্রতিটি প্রার্থী কেন মেলেনি, তা আলাদা করে বলে — আর standard library-তে একটি operator-এর প্রার্থী ডজনখানেক। তৃতীয়ত, টাইপের নাম নিজেই বিশাল: std::string আসলে std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >. একটি বাস্তব পরীক্ষা: শর্তহীন sum_all-এ একটি vector দিলে gcc-head কী বলে (ছাঁটা):

std::vector<int> v{1, 2};
auto s = sum_all(1, v, 3);

prog.cc: In instantiation of 'auto sum_all(const T&, const Ts& ...)
  [with T = int; Ts = {std::vector<int, std::allocator<int> >, int}]':
prog.cc:11:21:   required from here
prog.cc:6:19: error: no match for 'operator+' (operand types are
  'const int' and 'const std::vector<int>')
    6 |     return (first + ... + rest);
  • there are 14 candidates
    • candidate 1: 'template<class _Iterator> constexpr
      std::reverse_iterator<_IteratorL> std::operator+(...)'
      • template argument deduction/substitution failed:
        •   'const std::vector<int>' is not derived from
            'const std::reverse_iterator<_IteratorL>'
    • candidate 2: ... move_iterator ...
    • candidate 3: ... basic_string ...
    ... (মোট ১৪টি প্রার্থী, পুরো বার্তা ১৩০ লাইনের বেশি)

error-টি আসে template-এর শরীরের ভেতর থেকে (লাইন ৬), আর বেশিরভাগ লাইন reverse_iterator বা basic_string-এর এমন operator নিয়ে, যার সাথে আপনার কাজের কোনো সম্পর্ক নেই। এবার একই ফাংশনে একটি concept বসান:

template <typename T, typename... Ts>
concept Summable = requires(const T& first, const Ts&... rest) {
    (first + ... + rest);                       // requires expression-এ fold
};
template <typename T, typename... Ts>
    requires Summable<T, Ts...>
auto sum_all(const T& first, const Ts&... rest) { return (first + ... + rest); }
prog.cc:16:21: error: no matching function for call to
  'sum_all(int, std::vector<int>&, int)'
  • there is 1 candidate
    • candidate 1: ... requires Summable<T, Ts ...> ...
      • constraints not satisfied
        • required for the satisfaction of 'Summable<T, Ts ...>'
          [with T = int; Ts = {std::vector<int, std::allocator<int> >, int}]
        • the required expression '...' is invalid
(পুরো বার্তা প্রায় ৪০ লাইন)

error এখন ডাকার লাইনে (লাইন ১৬), একটি প্রার্থী, একটি শর্তের নাম, আর কোন টাইপগুলো দিয়ে শর্ত ভাঙল। শরীরের ভেতরের operator+-এর চৌদ্দজন প্রার্থীর কথা একবারও নেই, কারণ শরীর কখনো instantiate হয়নি। দৈর্ঘ্য প্রায় তিন ভাগের এক ভাগ, আর তথ্য অনেক বেশি কাজের। (নির্দিষ্ট লাইনসংখ্যা compiler-এর সংস্করণে বদলায়; পার্থক্যের ধরনটি বদলায় না।)

compile-সময় আর code bloat

প্রতিটি নতুন টাইপের সমাহারে নতুন instantiation: log_line-কে দশটি আলাদা argument-তালিকায় ডাকলে দশটি আলাদা ফাংশন। এতে compile-সময় বাড়ে (template-ভারী header প্রতিটি .cpp ফাইলে আবার পার্স হয়), আর binary-ও বড় হতে পারে — যাকে বলে code bloat. প্রতিকার: recursion-এর বদলে fold, template-এর শুধু টাইপ-নির্ভর অংশটুকু template রাখা, আর পর্ব ৪৩-এর modules.

কখন এগুলো লিখবেন না

  • একটি-দুটি টাইপের জন্য: দুটি সাধারণ overload variadic-এর চেয়ে পড়তে সহজ।
  • চলার সময়ে টাইপ ঠিক হলে: ব্যবহারকারী মেনু থেকে বেছে নেবেন কোন shape — virtual বা std::variant (পর্ব ৩৮), CRTP নয়।
  • “ভবিষ্যতে লাগতে পারে” ভেবে: আজ যে generic-তা কেউ চায়নি, তার জন্য আজই জটিলতার দাম দেবেন না।
  • দলের দক্ষতা না মিললে: যে কোড দলের বাকিরা ঠিক করতে পারবেন না, তা আপনার ছুটির দিনে ভাঙবে। template-কৌশল library-র ভেতরে লুকিয়ে রাখুন, ব্যবহারকারীর মুখে দিন একটি সরল interface.
  • constexpr বা concept যেখানে যথেষ্ট: মানের হিসাবে constexpr ফাংশন, শর্তে concept, শাখায় if constexpr.
অন্য ভাষায়

Java-র generics compile-এর পরে টাইপ মুছে ফেলে (type erasure): List<String> আর List<Integer> চলার সময় একই class, তাই কোনো code bloat নেই, কিন্তু int-এর মতো primitive টাইপ সরাসরি রাখা যায় না, আর টাইপ-নির্ভর কোড (যেমন C++-এর if constexpr) লেখা যায় না — Java কোর্সের generics — type parameter আর wildcard পর্ব দেখুন। Python-এ টাইপ যাচাই হয় চলার সময়, duck typing-এ: যার area() আছে, সে-ই shape; ভুল হলে প্রোগ্রাম চলতে চলতে থামে (Python কোর্সের ABC, duck typing আর Protocol পর্ব দেখুন)। C++ দুটি পথের মাঝে: টাইপ-প্রতি আসল কোড (দ্রুত, কিন্তু বড়), আর যাচাই compile-এর সময়।

পরীক্ষা ও ইন্টারভিউতে
  • variadic template কী, আর sizeof... কী দেয়? — যে template যত খুশি template argument নেয় (C++11); sizeof...(pack) দেয় pack-এ কয়টি জিনিস, compile-time-এ।
  • (... - xs) আর (xs - ...)-এর পার্থক্য কী? — প্রথমটি left fold, বাঁ দিক থেকে বন্ধনী; দ্বিতীয়টি right fold. বিয়োগ-ভাগে ফল আলাদা হয়, যোগ-গুণে একই।
  • CRTP কী, আর কেন ব্যবহার হয়? — derived class নিজেকে base-এর template argument দেয়; base compile-এর সময় derived-এর ফাংশন ডাকে — vtable ছাড়া static polymorphism আর mixin.
  • SFINAE-র পূর্ণরূপ আর অর্থ কী? — Substitution Failure Is Not An Error: template-এর ঘোষণায় টাইপ বসাতে ব্যর্থ হলে প্রার্থী শুধু বাদ পড়ে; C++20-এ concept এর পরিষ্কার বিকল্প।
  • std::forward আর std::move-এর পার্থক্য কী? — move সবসময় rvalue-তে cast করে; forward শুধু তখনই, যখন মূল argument rvalue ছিল — forwarding reference-এর সাথে ব্যবহার হয়।

৯ · AI-এর যুগে — অতি-চালাক template চেনা

AI টুলের প্রশিক্ষণের ডেটায় পুরোনো Boost-ধাঁচের চতুর কোড প্রচুর, তাই একটি সাধারণ অনুরোধেও এরা প্রায়ই কাজের তুলনায় অনেক জটিল template-কোড দেয়। আপনার কাজ বিচার করা, এর সরল রূপ আছে কি না। সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
enable_if, void_t, decltype-কৌশলের স্তূপপড়া কঠিন, error দুর্বোধ্য, overload-এ ambiguous ফাঁদconcept, বা একটি ফাংশনে if constexpr
recursion দিয়ে pack খোলাবেশি instantiation, বেশি কোডfold expression
Args&&... args নিয়ে f(args...)forward ভুলে যাওয়া — temporary-ও কপি, move-only টাইপে errorf(std::forward<Args>(args)...)
একই args দুবার forwardপ্রথমবারেই move, দ্বিতীয়বার খালি object — warning ছাড়াপ্রতিটি argument একবারই, শেষ ব্যবহারে
মানের হিসাবে template recursion (Factorial<N>)ধীর compile, পড়া কঠিনconstexpr ফাংশন
একটি টাইপের জন্যও CRTP বা variadicঅপ্রয়োজনীয় জটিলতাসাধারণ ফাংশন বা class
খালি pack-এ (... + xs)শূন্য argument দিলে compile errorinit-সহ binary fold, বা অন্তত একটি argument

AI-কে generic কোড লিখতে বলার সময় সরলতাকেই শর্ত বানান, আর প্রতিটি কৌশলের কারণ চান:

আমি C++23 (g++, -std=c++23 -Wall -Wextra) দিয়ে একটি type-safe logger লিখছি: log_line("order", id, "taka", amount) — যত খুশি, যেকোনো ছাপার মতো মান নেবে। নিয়ম: ১. সবচেয়ে সরল সমাধান দাও। enable_if, void_t বা SFINAE ব্যবহার করবে না; শর্ত লাগলে C++20 concept (যেমন std::formattable) নাও। ২. pack খুলতে recursion নয়, fold expression ব্যবহার করো। ৩. কোথাও forwarding reference নিলে প্রতিটি argument ঠিক একবার std::forward করো, আর কেন forward দরকার তা এক লাইনে বলো। ৪. CRTP, template template parameter বা index_sequence ব্যবহার করলে কেন সরল বিকল্প চলত না, তা ব্যাখ্যা করো; না লাগলে ব্যবহার করবে না। ৫. খালি argument-তালিকায় (log_line()) কী হবে, তা বলো আর সেটি সামলাও। ৬. শেষে একটি অসমর্থিত টাইপ দিয়ে ডাক দেখাও, আর g++-এর error-এর প্রথম দুই লাইন কী বলবে তা লেখো।

AI-এর template-কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১১৯৯৪ সালে একটি প্রোগ্রাম compile হতেই পারেনি, অথচ তার error বার্তায় মৌলিক সংখ্যার তালিকা ছাপা হয়েছিল। এই ঘটনা কীভাবে প্রমাণ করল যে C++ template নিজেই একটি পূর্ণ প্রোগ্রামিং ভাষা?

১৯৯৪ সালে C++ standard কমিটির একটি সভায় Siemens-এর Erwin Unruh একটি অদ্ভুত প্রোগ্রাম দেখান। প্রোগ্রামটি compile হতো না — ইচ্ছা করেই। কিন্তু compiler যে error বার্তাগুলো দিত, তার ভেতরে টাইপের নামে লেখা থাকত একের পর এক মৌলিক সংখ্যা: ২, ৩, ৫, ৭, ১১ … কৌশলটি ছিল এমন: template-এর recursion দিয়ে compiler-কে প্রতিটি সংখ্যা মৌলিক কি না হিসাব করানো, আর মৌলিক হলে এমন একটি রূপান্তর চাওয়া, যা অবৈধ। compiler error দিতে গিয়ে টাইপের নামটি ছাপত — যার ভেতরে সংখ্যাটি থাকত। অর্থাৎ হিসাবটি হয়েছিল পুরোপুরি compile-এর সময়ে, আর ফল বেরিয়েছিল error-এর পথ দিয়ে।

এর তাৎপর্য বিশাল ছিল। template নকশা করা হয়েছিল টাইপ-নিরপেক্ষ container আর algorithm লেখার জন্য, হিসাব চালানোর জন্য নয়। কিন্তু Unruh দেখালেন, specialization দিয়ে “যদি-তবে”, recursion দিয়ে “লুপ”, আর non-type parameter দিয়ে “ভ্যারিয়েবল” — একটি প্রোগ্রামিং ভাষার সব মৌলিক উপাদান template-এর ভেতরে লুকিয়ে আছে। পরের বছর Todd Veldhuizen “template metaprogram” নিয়ে লেখা প্রকাশ করেন আর expression templates কৌশল দেখান, যা দিয়ে গাণিতিক library (যেমন Blitz++) Fortran-এর কাছাকাছি গতি পেত। পরে তিনি একটি লেখায় যুক্তি দেন যে C++ template আসলে Turing-complete — অর্থাৎ যেকোনো গণনাযোগ্য জিনিস তা দিয়ে হিসাব করা সম্ভব (Turing completeness).

Turing-complete হওয়ার একটি অস্বস্তিকর ফল আছে: halting problem. compiler সাধারণভাবে জানতে পারে না একটি template-হিসাব কখনো শেষ হবে কি না। তাই standard একটি “implementation limit”-এর সুপারিশ করে — recursive instantiation-এর গভীরতা একটি সীমায় আটকে দেওয়া; GCC-তে এটি -ftemplate-depth দিয়ে বদলানো যায়। অসীম recursion-এ compiler থামে “template instantiation depth exceeds maximum” বলে।

এই আবিষ্কার একটি নতুন ধারা খুলে দিল, কিন্তু একটি সমস্যাও রেখে গেল: এটি দুর্ঘটনাক্রমে পাওয়া ভাষা, যার ব্যাকরণ হিসাবের জন্য নকশা হয়নি। পরের পঁচিশ বছর C++-এর বিবর্তনের বড় অংশ — constexpr (C++11), if constexpr আর fold (C++17), consteval আর concept (C++20) — আসলে এই লুকানো ভাষাকে স্বাভাবিক C++-এর ব্যাকরণে ফিরিয়ে আনার চেষ্টা।

মূল উপলব্ধি: template-এর হিসাব-ক্ষমতা নকশা করা হয়নি, আবিষ্কৃত হয়েছিল। তাই এর ব্যাকরণ অস্বাভাবিক, error ভয়ংকর। আধুনিক C++-এর লক্ষ্য একই ক্ষমতা রেখে তাকে সাধারণ কোডের মতো পড়ার যোগ্য করা — আর আপনার লক্ষ্যও তা-ই হওয়া উচিত।

প্র ০২Andrei Alexandrescu-র ২০০১ সালের বই “Modern C++ Design” আর Boost.MPL template metaprogramming-কে কৌতূহল থেকে প্রকৌশলে তুলেছিল। তারা কী সম্ভব করল, আর কোন দাম রেখে গেল?

২০০১ সালে Andrei Alexandrescu-র “Modern C++ Design: Generic Programming and Design Patterns Applied” প্রকাশিত হয়। বইটি দেখায়, template metaprogramming শুধু মৌলিক সংখ্যার খেলা নয় — বাস্তব নকশার যন্ত্র। দুটি ধারণা বিশেষ প্রভাব ফেলে। প্রথমটি typelist: টাইপের একটি তালিকা, compile-এর সময় যার ওপর লুপ চালানো যায় — দৈর্ঘ্য বের করা, কোনো টাইপ খোঁজা, বাদ দেওয়া। variadic template তখনো ছিল না, তাই তালিকাটি বানানো হতো শিকলের মতো: একটি Head টাইপ আর বাকি তালিকা (Tail), শেষে একটি বিশেষ “NullType”, আর সুবিধার জন্য কয়েক ডজন macro. চেনা লাগছে? ৪ নম্বর অংশের TinyTuple ঠিক এই ধাঁচ, শুধু আজ pack দিয়ে লেখা যায়।

দ্বিতীয় ধারণা policy-based design: একটি class-এর আচরণের প্রতিটি দিক (thread-safety, memory বরাদ্দ, ভুল হলে কী হবে) একটি template parameter হিসেবে নেওয়া, যাতে ব্যবহারকারী ছোট ছোট “policy” মিলিয়ে নিজের class বানান — compile-এর সময়, কোনো virtual খরচ ছাড়া। বইয়ের সাথে আসা Loki library-তে smart pointer, singleton আর object factory এভাবে লেখা ছিল। CRTP আর template template parameter এই নকশার নিয়মিত যন্ত্র।

প্রায় একই সময়ে Aleksey Gurtovoy আর David Abrahams Boost-এ MPL (Metaprogramming Library) তৈরি করেন — compile-time-এর জন্য STL-এর মতো একটি library: টাইপের vector, list, map, আর তাদের ওপর transform, find_if-এর মতো algorithm. ২০০৪ সালে দুজনের বই “C++ Template Metaprogramming” এই পদ্ধতিকে সুসংগঠিত রূপ দেয়।

দাম ছিল তিনটি, আর আজও সেগুলো শিক্ষা। প্রথমত, compile-সময়: MPL-ভারী প্রজেক্টে একটি ফাইল compile হতে মিনিট লাগত। দ্বিতীয়ত, error: একটি ভুলে কয়েকশো লাইন, যার প্রতিটি টাইপের নাম এক অনুচ্ছেদের সমান। তৃতীয়ত, পাঠযোগ্যতা: কোডটি এমন একটি উপভাষায় লেখা, যা দলের অল্প কয়েকজন বুঝতেন — আর সেই কয়েকজন চলে গেলে কোডটি অস্পৃশ্য হয়ে যেত। আর ভাষাটি নিজেই variadic, constexpr, fold, concept দিয়ে সেই পুরোনো কৌশলের বেশিরভাগকে অপ্রয়োজনীয় করেছে।

মূল উপলব্ধি: Alexandrescu আর MPL দেখিয়েছিলেন compile-time-এ কতটা সম্ভব; তাদের দাম দেখিয়েছিল কেন ভাষার নিজস্ব সমাধান দরকার। আজকের variadic, constexpr আর concept সেই পরীক্ষার ফসল — পুরোনো কৌশল জানুন পড়ার জন্য, লিখুন আধুনিক রূপে।

প্র ০৩variadic template-এর নকশায় pack-কে একটি সাধারণ তালিকার মতো index দিয়ে ধরা যেত না — শুধু “ছাঁচ দিয়ে খোলা” যেত। এই নকশা কেন বেছে নেওয়া হলো, আর পরে কী কী যোগ করতে হলো?

পর্ব ২৯-এর ভাবনার প্রশ্নে দেখেছেন, variadic template-এর আগে tuple-এর মতো জিনিস বানানো হতো দশটি পর্যন্ত default parameter আর macro-তৈরি overload দিয়ে। Douglas Gregor আর Jaakko Järvi (সাথে Gary Powell) ২০০৪ সালে প্রথম প্রস্তাবে সমাধানটি দেন, ২০০৭ সালে তা C++0x-এর খসড়ায় ঢোকে, আর Gregor নিজে GCC-তে তার পরীক্ষামূলক বাস্তবায়ন করেন — standard চূড়ান্ত হওয়ার আগেই মানুষ ব্যবহার করে দেখতে পেরেছিল।

নকশার সবচেয়ে চোখে পড়ার মতো সিদ্ধান্ত: pack কোনো তালিকা বা array নয়। আপনি xs[0] লিখতে পারেন না, লুপ চালাতে পারেন না। pack দিয়ে মাত্র দুটি কাজ করা যেত — sizeof... দিয়ে গোনা, আর একটি ছাঁচে বসিয়ে খোলা। কেন এত সীমিত? কারণ pack-এর প্রতিটি জিনিসের টাইপ আলাদা হতে পারে। xs[i]-এর টাইপ কী হবে, যদি i চলার সময়ের মান হয়? C++-এর static টাইপ ব্যবস্থায় এর উত্তর নেই। ছাঁচ-ভিত্তিক expansion এই সমস্যা এড়ায়: প্রতিটি জিনিসের জন্য ছাঁচটি আলাদাভাবে, তার নিজের টাইপে বসে। নকশাটি ছোট, কিন্তু আশ্চর্যরকম শক্তিশালী — function argument, base class-এর তালিকা, initializer, template argument, এমনকি lambda capture-এও pack খোলা যায়।

দাম দিতে হলো সাধারণ কাজে। “সব যোগ করো” লিখতে ২ নম্বর অংশের মতো recursion লাগত, প্রতিটি ধাপে নতুন instantiation. মানুষ নানা কৌশল বের করলেন — যেমন একটি নকল array-এর initializer-এ comma operator দিয়ে pack খোলা, শুধু ক্রমানুসারে কাজ চালানোর জন্য। এই কষ্ট থেকেই এল C++17-এর fold expression (Andrew Sutton আর Richard Smith-এর প্রস্তাব)। তারপর বাকি থাকল “n-তম জিনিস” — সেটি করতে std::tuple_element বা index_sequence-এর ঘোরানো পথ নিতে হতো। অবশেষে C++26-এ যোগ হয়েছে pack indexing: Ts...[0] আর xs...[0], যেখানে index একটি compile-time ধ্রুবক হতে হয় — তাই টাইপের প্রশ্নটি আর ওঠে না। gcc-head-এ -std=c++2c দিয়ে এটি আমরা চালিয়ে দেখেছি।

মূল উপলব্ধি: pack-কে array না বানিয়ে “ছাঁচে খোলা” — এই সিদ্ধান্ত আলাদা আলাদা টাইপকে নিরাপদে সামলানোর জন্য অপরিহার্য ছিল। fold আর pack indexing সেই মূল নকশা না ভেঙে তার কষ্টকর কোণগুলো মসৃণ করেছে।

প্র ০৪২০০৯ সালে C++0x থেকে concepts বাদ পড়ার পর C++20 পর্যন্ত এগারো বছর library-লেখকেরা কীভাবে চালালেন? আর ranges library কেন concepts-কে standard-এ টেনে আনার বড় চালিকাশক্তি হলো?

পর্ব ৪০-এর ভাবনার প্রশ্ন ০১-এ দেখেছেন কমিটির ভেতরের গল্প: ২০০৯ সালের জুলাইয়ে Frankfurt-এর সভায় concepts বাদ, ২০১৩-তে “Concepts Lite”, ২০১৫-তে Concepts TS, ২০১৭-তে C++20-এর খসড়ায়। এখানে দেখুন অন্য দিকটি — যাঁরা প্রতিদিন library লিখতেন, তাঁরা এই এগারো বছর কী করলেন।

উত্তর: SFINAE দিয়ে concepts-এর অনুকরণ। এই পর্বের ৭-৮ নম্বর অংশের সব কৌশল — enable_if, void_t, decltype দিয়ে “এই expression লেখা যায় কি” পরীক্ষা — এই সময়েই পরিণত হয়। কেউ কেউ আরও এগিয়ে গেলেন। Eric Niebler তাঁর range-v3 library-তে macro আর SFINAE দিয়ে একটি পুরো concept-অনুকরণ ব্যবস্থা বানালেন, যাতে library-র ভেতরের কোড concept-এর মতো করে লেখা যায়, আর পরে ভাষায় concept এলে প্রায় সরাসরি বদলে নেওয়া যায়।

ranges কেন চালিকাশক্তি? কারণ range library-র পুরো নকশা concept-এর সিঁড়ির ওপর দাঁড়ানো — input range, forward range, random access range, sortable, indirectly readable… প্রতিটি algorithm-এর জন্য ডজনখানেক শর্ত, আর শর্ত দেখে overload বাছাই। SFINAE দিয়ে এটি সম্ভব, কিন্তু error দুঃস্বপ্ন আর compile ধীর। Niebler, Casey Carter আর Andrew Sutton-এর Ranges TS (২০১৭) সরাসরি Concepts TS-এর ওপর লেখা হলো — অর্থাৎ concepts-এর প্রথম বড় বাস্তব ব্যবহারকারী। ranges দেখাল concept শুধু সুন্দর ধারণা নয়, একটি বড় library-র জন্য অপরিহার্য; আর concepts দেখাল ranges-কে ব্যবহারযোগ্য error দেওয়া সম্ভব। ২০১৮ সালের শেষে ranges C++20-এর খসড়ায় একীভূত হয়, concepts-এর ঠিক পাশে।

আজও অনেক library-তে দুটি পথই দেখবেন — C++20 compiler হলে concept, পুরোনো compiler-এর জন্য macro দিয়ে SFINAE. এ কারণেই AI-এর প্রশিক্ষণ-ডেটায় enable_if এত বেশি, আর এ কারণেই আপনাকে দুটিই পড়তে জানতে হবে।

মূল উপলব্ধি: ভাষার একটি সুবিধা না থাকলে প্রোগ্রামাররা থেমে থাকেন না — অনুকরণ বানান। সেই অনুকরণের অভিজ্ঞতা, বিশেষ করে ranges-এর, প্রমাণ করেছে আসল সুবিধাটি কতটা দরকার, আর তার নকশা কেমন হওয়া উচিত।

প্র ০৫CRTP-র নামে “curiously” শব্দটি কেন? ১৯৯৫ সালে Jim Coplien কী লক্ষ করেছিলেন, আর একটি অদ্ভুত কৌশল কীভাবে standard library-র অংশ হয়ে গেল?

১৯৯৫ সালে Jim Coplien “C++ Report” পত্রিকায় একটি লেখায় এই ধাঁচটির নাম দেন “Curiously Recurring Template Pattern”. “curiously” — কৌতূহলজনকভাবে — কারণ তিনি লক্ষ করেছিলেন, একই অদ্ভুত কৌশল একে অপরকে না চেনা নানা প্রোগ্রামারের কোডে বারবার ফিরে আসছে: একটি class নিজেকেই তার base-এর template argument বানাচ্ছে। প্রথম দেখায় এটি চক্রাকার মনে হয় — Circle সংজ্ঞায়িত হওয়ার আগেই Shape<Circle>? কিন্তু C++-এ এটি বৈধ, কারণ base template-এর শরীর instantiate হয় পরে, যখন Circle সম্পূর্ণ।

ধারণাটির একটি তাত্ত্বিক পূর্বপুরুষও আছে: ১৯৮৯ সালের একটি গবেষণাপত্রে (Peter Canning, William Cook ও সহকর্মীরা) বর্ণিত “F-bounded polymorphism”, যেখানে একটি টাইপের শর্ত নিজেকেই উল্লেখ করে — যেমন “T এমন টাইপ, যা অন্য T-এর সাথে তুলনা করা যায়”। Java-র class Marks implements Comparable<Marks> বা <T extends Comparable<T>> আজও ঠিক এই ধাঁচ।

বাস্তবে CRTP জনপ্রিয় হয় গতির জন্য। ১৯৯০-এর দশকের শেষে Microsoft-এর ATL (Active Template Library) Windows-এর COM component লেখার জন্য CRTP ব্যাপকভাবে ব্যবহার করে, যাতে virtual-এর খরচ ছাড়া base class-এর সুবিধা পাওয়া যায়। গাণিতিক library-গুলোর (যেমন Eigen) expression templates-এর ভিত্তিতেও CRTP. অবশেষে এটি standard-এ ঢোকে: C++11-এর std::enable_shared_from_this<T> (একটি object নিজের shared_ptr বানাতে পারে), আর C++20-এর std::ranges::view_interface<D>.

তারপর এল C++23-এর deducing this — Gašper Ažman, Sy Brand, Ben Deane আর Barry Revzin-এর প্রস্তাব। প্রস্তাবের একটি উদ্দেশ্য ছিল CRTP-র অনেক ব্যবহারকে সরল করা: base-কে template বানাতে হয় না, derived-কে নিজের নাম পাঠাতে হয় না, ভুল নাম দিয়ে UB ঘটানোর সুযোগও নেই। তবে প্রতিটি টাইপের আলাদা static data বা আলাদা base টাইপ দরকার হলে CRTP এখনো প্রয়োজনীয়। প্রায় তিন দশক পরে, একটি “কৌতূহলজনক” কৌশল তার প্রধান কাজের জন্য ভাষার নিজস্ব ব্যাকরণ পেল।

মূল উপলব্ধি: CRTP কোনো একজনের আবিষ্কার নয় — একটি প্রয়োজন (virtual ছাড়া polymorphism) থেকে স্বাধীনভাবে বারবার জন্মানো সমাধান, যাকে Coplien শুধু নাম দিয়েছিলেন। ভাষা যখন সেই প্রয়োজনের সরাসরি উত্তর দেয় (deducing this), পুরোনো কৌশল সংকুচিত হয়ে নিজের আসল জায়গাটুকুতে ফিরে যায়।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. খুলে লিখুন: xs ব্যাগে তিনটি মান a, b, c আর Ts-এ তাদের টাইপ A, B, C. নিচেরগুলো compiler কীভাবে খুলবে? (ক) h(g(xs)...) (খ) h(g(xs...)) (গ) h(g(xs, xs...)...) (ঘ) std::vector<int>{(xs * 10)...}

    (ক) h(g(a), g(b), g(c)) — ছাঁচ g(xs), প্রতিটির ওপর একবার। (খ) h(g(a, b, c)) — ভেতরের xs... আগে খোলে, g একবারই ডাকা হয়। (গ) এটি কৌশলী: ভেতরের xs... পুরো ব্যাগে খোলে, আর বাইরের ছাঁচ g(xs, a, b, c) বাইরের xs-এর প্রতিটির জন্য: h(g(a, a, b, c), g(b, a, b, c), g(c, a, b, c)). (ঘ) {a * 10, b * 10, c * 10}. শিক্ষা: ... সবসময় তার ঠিক বাঁ দিকের পুরো ছাঁচকে খোলে, আর একটির ভেতরে আরেকটি থাকলে ভেতরেরটি আগে।

  2. fold চিনুন: template <typename... Ts> void out(const Ts&... xs) { (std::cout << ... << xs); } — এটি কোন রকম fold? out(1, "-", 2) কী ছাপবে? out() চলবে কি? আর মানগুলোর মাঝে একটি করে ফাঁকা জায়গা দিতে চাইলে কী বদলাবেন?

    এটি binary left fold, init = std::cout: খুলে দাঁড়ায় ((std::cout << 1) << "-") << 2, ছাপে 1-2 — কোনো ফাঁকা নেই। out() compile হয় আর চলে, কারণ binary fold-এ খালি ব্যাগ মানে শুধু init — (std::cout), কিছু ছাপা হয় না। তবে -Wall দিলে g++ এখানে “statement has no effect” warning দেয় — শুধু std::cout লেখা একটি অর্থহীন বাক্য, আর compiler সেটি ধরে ফেলে। ফাঁকা দিতে comma fold-এ বদলান: ((std::cout << xs << ' '), ...); — প্রতিটি মানের পরে একটি ফাঁকা। এখানে দিক গুরুত্বপূর্ণ: <<-এর chain বাঁ থেকে না হলে std::cout শুরুতে থাকত না।

  3. পুরোনো কোড বিচার করুন: AI এই কোড দিয়েছে: template <typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>> T half(T x) { return x / 2; } আর একই নামে template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>> T half(T x) { return x >> 1; }. এটি compile হবে কি? আধুনিক দুই রূপে লিখুন।

    compile হবে না: দুটির পার্থক্য শুধু default template argument-এ, আর default argument signature-এর অংশ নয় — compiler একে “একই function template দুবার সংজ্ঞায়িত” বলে error দেয়। পুরোনো সমাধান ছিল std::enable_if_t<..., int> = 0 রূপ। আধুনিক রূপ এক — concept: template <std::floating_point T> T half(T x) { return x / 2; } আর template <std::integral T> T half(T x) { return x >> 1; }. রূপ দুই — একটি ফাংশন: if constexpr (std::integral<T>) return x >> 1; else return x / 2;. বাড়তি পর্যবেক্ষণ: ঋণাত্মক পূর্ণসংখ্যায় x >> 1 আর x / 2 ভিন্ন ফল দেয় (−৭-এর জন্য −৪ বনাম −৩; C++20 থেকে signed-এর >> নিয়মমতো নিচের দিকে গোল করে) — AI-এর “চালাক” অপ্টিমাইজেশন আচরণও বদলে দিয়েছে। সাধারণ x / 2 রাখুন; compiler নিজেই দ্রুততম নির্দেশ বেছে নেয়।

  4. নিজে লিখুন (কোড): CRTP দিয়ে একটি mixin Printable<D> লিখুন, যার print() ফাংশন derived-এর label() আর value() ডেকে label=value ছাপায়। দুটি টাইপ বানান — Temperature (label "temp", value একটি double) আর Stock (label "stock", value একটি int) — আর দুটির ওপর print() ডাকুন। তারপর একই কাজ deducing this দিয়ে করুন।

    CRTP রূপ: template <typename D> struct Printable { void print() const { const D& d = static_cast<const D&>(*this); std::println("{}={}", d.label(), d.value()); } };, তারপর struct Temperature : Printable<Temperature> { double t = 31.5; const char* label() const { return "temp"; } double value() const { return t; } };, একইভাবে Stock. Temperature{}.print() ছাপে temp=31.5. deducing this রূপ: struct Printable { void print(this const auto& self) { std::println("{}={}", self.label(), self.value()); } }; আর struct Temperature : Printable { ... }; — base-এ template নেই, cast নেই। লক্ষ করুন, value()-এর টাইপ দুই টাইপে আলাদা (double, int), তবু print-এর একটিই লেখা — দুটি রূপেই টাইপ compile-এর সময় জানা থাকায় std::println ঠিক format বেছে নেয়। Temperature-কে ভুল করে Printable<Stock> থেকে inherit করালে CRTP রূপে UB; deducing this রূপে এই ভুলের সুযোগই নেই।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমC++-এ make_unique<Order>(name, 3) ডাকলে (name একটি std::string ভ্যারিয়েবল) যা ঘটে, তা সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. argument দেখে compiler Args deduce করে: std::string& আর int
  2. reference collapsing-এর নিয়মে parameter দুটি হয় std::string& আর int&&
  3. std::forward<Args>(args)... খুলে দুটি আলাদা forward হয়
  4. name lvalue হিসেবে, 3 rvalue হিসেবে Order-এর constructor-এ পৌঁছায়
  5. heap-এ Order তৈরি হয়, caller-এর name অক্ষত থাকে
  6. নতুন object-এর মালিক একটি unique_ptr ফেরত আসে

C++-এ template <typename... Ts> constexpr std::size_t count(const Ts&... xs) { return sizeof...(xs); } সংজ্ঞায়িত। count(1, 2.5, 'c', "dhaka")-এর মান কত?

  • সব argument-এর মোট byte-সংখ্যা
  • 3
  • 4
  • Compile error, কারণ argument-গুলোর টাইপ আলাদা

C++17-এ template <typename... Ts> int f(Ts... xs) { return (... - xs); } আছে। f(10, 3, 2)-এর মান কত?

  • 9
  • 5
  • -15
  • Compile error, কারণ বিয়োগে fold চলে না

C++17-এ একটি unary fold একটি খালি parameter pack-এর ওপর চালানো হলো। নিচের কোন operator-এ এটি compile error দেয়?

  • &&
  • ||
  • কমা (,)
  • +

C++-এ struct Circle : Shape<Circle> ধাঁচের CRTP সম্পর্কে কোনটি সঠিক?

  • base class compile-এর সময় derived-এর ফাংশন ডাকে, vtable ছাড়া
  • base class-এ অন্তত একটি virtual ফাংশন থাকতে হয়
  • Circle আর Rect-কে একটি std::vector<Shape*>-এ রাখা যায়
  • এটি compile হয় না, কারণ Circle তখনো সম্পূর্ণ সংজ্ঞায়িত নয়

C++-এ SFINAE (Substitution Failure Is Not An Error) সম্পর্কে কোনটি সঠিক?

  • template-এর শরীরের যেকোনো ভুলে প্রার্থী চুপচাপ বাদ পড়ে
  • চলার সময় ভুল টাইপ এলে exception ছোড়া হয়
  • ঘোষণায় টাইপ বসাতে ব্যর্থ হলে প্রার্থী বাদ পড়ে, error হয় না
  • C++20-এ SFINAE নিয়মটি ভাষা থেকে বাদ দেওয়া হয়েছে

C++-এ একটি factory template <typename... Args> void build(Args&&... args)-এর ভেতরে A a(std::forward<Args>(args)...); B b(std::forward<Args>(args)...); লেখা। caller একটি temporary std::string পাঠালে কী হতে পারে?

  • Compile error, কারণ forward দুবার ডাকা যায় না
  • A ঠিক string পায়, B পায় একটি moved-from (সাধারণত খালি) string
  • দুটি object-ই string-এর আলাদা কপি পায়
  • প্রোগ্রাম সঙ্গে সঙ্গে crash করে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. template = compile-time কোড-কারখানা: টাইপ ঢোকে, আসল কোড বেরোয়; চলার সময় কোনো বাড়তি খরচ নেই, দাম compile-সময় আর পাঠযোগ্যতায়।
  2. pack খোলে ছাঁচে: ...-এর বাঁ দিকের পুরো expression প্রতিটি জিনিসের জন্য একবার বসে; f(xs)... আর f(xs...) আলাদা। sizeof... compile-time-এ গোনে।
  3. recursion নয়, fold: (... + xs) বাঁ দিক থেকে, (xs + ...) ডান দিক থেকে; খালি pack-এ শুধু &&, || আর কমা চলে। factory-তে প্রতিটি argument ঠিক একবার std::forward.
  4. CRTP = virtual ছাড়া polymorphism আর mixin: vptr নেই, ডাক inline হতে পারে, কিন্তু নানা টাইপ এক তালিকায় নয়। C++23-এর deducing this অনেক CRTP-কে সরল করে।
  5. SFINAE পড়ুন, concept লিখুন: enable_if-এর জায়গায় concept বা if constexpr; মানের হিসাবে constexpr; আর সরল সমাধান চললে কোনো কৌশলই নয়।

পরের পর্বে: template-এর গভীর থেকে উঠে এবার standard library-র দৈনন্দিন যন্ত্রপাতিতে — সময় মাপার chrono, সঠিকভাবে এলোমেলো সংখ্যা বানানোর random, আর ফাইল-ফোল্ডার সামলানোর filesystem.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান