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

constexpr — compile-এর সময়েই হিসাব

constexpr — Computing at Compile Time

Taught in Bangla, this lesson explains compile-time computation: const versus constexpr, functions that run at both compile time and run time, static_assert, C++20 consteval and constinit, if constexpr in templates, compile-time lookup tables, and how constant evaluation rejects undefined behavior.

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

  • কোন হিসাব compile-এর সময়ে সরিয়ে নেওয়া যায়, আর কেন সেটি গতি ও নিরাপত্তা দুটোই দেয়
  • const আর constexpr-এর আসল পার্থক্য; constexpr ভ্যারিয়েবল ও ফাংশন; একই ফাংশন কখন compile-এর সময়, কখন চলার সময় চলে
  • consteval, constinit, static_assert আর if constexpr — কোনটি কোন সমস্যার সমাধান
  • constexpr ফাংশন দিয়ে std::array-এ lookup table বানানো, আর C++11 থেকে C++23 পর্যন্ত constexpr-এ কী কী করা যায় তার বিবর্তন
  • compile-এর সময়ের হিসাব কীভাবে বাগ ধরে, এর খরচ কী, আর AI-এর লেখা compile-time কোড কীভাবে যাচাই করবেন

১ · সমস্যাটা আগে — যে উত্তর কখনো বদলায় না

একটি প্রোগ্রাম চলার সময় অনেক হিসাব করে। কিছু হিসাবের উত্তর নির্ভর করে ব্যবহারকারীর ওপর — কে কত টাকা পাঠালেন, কোন ফাইল খুললেন। কিন্তু কিছু হিসাবের উত্তর পৃথিবীর কোনো ব্যবহারকারী বদলাতে পারেন না। এক দিনে সবসময় ২৪ × ৬০ × ৬০ = ৮৬,৪০০ সেকেন্ড। ১ টাকা সবসময় ১০০ পয়সা। ৫০-এর নিচে মৌলিক সংখ্যা সবসময় ওই ১৫টিই। ফাইল যাচাইয়ে ব্যবহৃত CRC-এর ২৫৬ ঘরের টেবিলও প্রতিবার একই।

এমন উত্তর প্রতিবার প্রোগ্রাম চালানোর সময় নতুন করে হিসাব করা অপচয়। একটি কুইজ অ্যাপের কথা ভাবুন, যা লাখো ফোনে চলে আর প্রতিবার খোলার সময় একটি মৌলিক সংখ্যার টেবিল বানায়। প্রতিটি ফোন একই কাজ করছে, একই ফল পাচ্ছে — বারবার, প্রতিদিন। এর চেয়ে ভালো হতো যদি টেবিলটি একবার বানিয়ে প্রোগ্রামের ভেতরেই রেখে দেওয়া যেত।

পাড়ার চায়ের দোকানের কথা ভাবুন। দোকানি প্রতিটি ক্রেতার জন্য খাতা খুলে হিসাব করেন না যে এক কাপ দুধ-চা আর দুটি বিস্কুটের দাম কত। সকালে দোকান খোলার আগেই দেয়ালে একটি দামের তালিকা লিখে রাখা আছে। ক্রেতা এলে শুধু তালিকা দেখে বলে দেন। হিসাবটা হয়েছে আগে, একবার; দোকান চলার সময় শুধু পড়া। আর তালিকা লেখার সময় কোনো ভুল চোখে পড়লে — যেমন দাম ঋণাত্মক — তখনই মুছে ঠিক করা যায়, ক্রেতার সামনে নয়। C++-এ সেই “দোকান খোলার আগের সময়” হলো compile-এর সময়।

C++ এখানে দুটি লাভ দেয়, আর দ্বিতীয়টি প্রথমটির চেয়েও গুরুত্বপূর্ণ।

লাভ ১ — গতি

যে মান compiler নিজেই বের করে ফেলে, সেটি মেশিন কোডে সরাসরি একটি সংখ্যা হয়ে বসে। চলার সময় কোনো লুপ নেই, কোনো গুণ নেই, কোনো টেবিল বানানো নেই।

কাজ সরে গেল বিল্ডের সময়ে

লাভ ২ — ভুল আগে ধরা

compiler যদি হিসাবটা করতে গিয়ে দেখে উত্তর ভুল, বা নিয়ম ভাঙা হয়েছে, তাহলে প্রোগ্রাম তৈরিই হয় না। ভুল ধরা পড়ে আপনার কম্পিউটারে, ব্যবহারকারীর ফোনে নয়।

বাগ ধরা পড়ল চালানোর আগেই

এই পর্বের সব যন্ত্র — constexpr, consteval, constinit, static_assert, if constexpr — একটিই প্রশ্নের নানা উত্তর: “এই কাজটি কি compile-এর সময়েই করা যায়, আর গেলে compiler কী নিশ্চয়তা দেবে?” যে মান compile-এর সময় হিসাব করা যায়, standard তাকে বলে constant expression, আর compiler যখন সেটি হিসাব করে, সেই প্রক্রিয়াকে বলে constant evaluation.

মূল ধারণা

একটি প্রোগ্রামের জীবনে দুটি আলাদা সময় আছে: compile-এর সময় (একবার, ডেভেলপারের কম্পিউটারে) আর চলার সময় (বারবার, প্রতিটি ব্যবহারকারীর কাছে)। যে হিসাবের সব তথ্য compile-এর সময়েই জানা, তাকে সেখানে সরিয়ে নিলে কাজ একবার হয় — আর ভুল থাকলে সেটিও সেখানেই ধরা পড়ে।

২ · const বনাম constexpr — “বদলাবে না” আর “আগেই জানা”

পর্ব ৫-এ আপনি const দেখেছেন। দুটি শব্দ দেখতে কাছাকাছি, আর AI টুলও প্রায়ই গুলিয়ে ফেলে। কিন্তু এরা দুটি ভিন্ন প্রশ্নের উত্তর দেয়।

int read_price();                       // চলার সময় ফাইল থেকে পড়ে

const int price = read_price();         // ঠিক আছে: মান চলার সময় আসে, তারপর আর বদলাবে না
constexpr int day = 24 * 60 * 60;       // ঠিক আছে: compiler নিজেই 86400 বের করে
constexpr int bad = read_price();       // error: compile-এর সময় এর মান জানা অসম্ভব

int counts[day / 3600];                 // ঠিক: array-র মাপ compile-এর সময় জানা চাই
// price = 50;                          // error: const বদলানো যায় না

const একটি প্রতিশ্রুতি: এই মান আর বদলাবে না। মানটি কোথা থেকে এল — ধ্রুবক থেকে, নাকি চলার সময় ফাইল পড়ে — তাতে const-এর কিছু যায় আসে না। constexpr আরও কড়া দাবি: এই মান compiler-কে এখনই বের করতে হবে। না পারলে compile error. আর যে মান compile-এর সময়েই জানা, সেটি স্বাভাবিকভাবেই বদলানো যায় না — তাই প্রতিটি constexpr ভ্যারিয়েবল নিজে থেকেই const.

প্রশ্নconstconstexpr ভ্যারিয়েবল
মূল অর্থশুধু-পড়া (read-only)compile-এর সময় জানা, এবং শুধু-পড়া
মান কখন ঠিক হয়compile-এর সময় বা চলার সময় — যেকোনোঅবশ্যই compile-এর সময়
array-র মাপ, template argument, static_assert-এ চলবে?শুধু তখন, যখন মানটি সত্যিই ধ্রুবক থেকে এসেছেসবসময়
না পারলেকিছু হয় না, মান চলার সময় বসেcompile error
ফাংশনে বসালেmember function-এ মানে “object বদলাবে না”ফাংশনটি compile-এর সময়েও চলতে পারবে

একটি সূক্ষ্ম পুরোনো নিয়ম জেনে রাখুন। const int k = 10; — integer টাইপের const, যার প্রথম মান একটি ধ্রুবক — C++-এ বহু আগে থেকেই constant expression হিসেবে চলে। এটি C-এর সাথে পুরোনো সামঞ্জস্যের জন্য রাখা। কিন্তু const double বা অন্য টাইপে এই ছাড় নেই, আর পাঠকও দেখে বুঝতে পারেন না মানটি কোথা থেকে এল। তাই নিয়মটা সরল রাখুন: মান compile-এর সময় জানা থাকতেই হবে — constexpr. শুধু “বদলাবে না” বলতে চান — const.

পুরোনো পথ: #define

C আর পুরোনো C++ কোডে ধ্রুবক লেখা হতো preprocessor-এর macro দিয়ে: #define POISHA_PER_TAKA 100. এটি compile-এর আগেই লেখা-বদল করে — নামটি যেখানে আছে, সেখানে অক্ষরে অক্ষরে 100 বসিয়ে দেয়। কোনো টাইপ নেই, namespace মানে না, debugger-এ নাম দেখা যায় না। আর বন্ধনী ভুললে বিপদ: #define DAY 24*60*60 লিখে x / DAY করলে আসলে হয় x / 24 * 60 * 60. constexpr int day = 24 * 60 * 60; একটি সত্যিকারের ভ্যারিয়েবল — টাইপ আছে, scope আছে, ভুল হলে compiler পরিষ্কার error দেয়। কোডে খোলা magic number না লিখে নাম দেওয়া constexpr ধ্রুবক লিখুন।

C জানলে

C-তে ধ্রুবকের প্রধান হাতিয়ার #define আর enum, আর compile-এর সময়ের “ফাংশন” মানে function-like macro — যার argument দুবার হিসাব হওয়ার মতো পুরোনো ফাঁদ আছে। C11-এ _Static_assert এসেছে, আর C23-এ constexpr শব্দটিও এসেছে, তবে শুধু ভ্যারিয়েবলের জন্য — C-তে constexpr ফাংশন নেই। C++-এ ফাংশন, লুপ, এমনকি std::vector-ও compile-এর সময়ে চলতে পারে। macro-র বিস্তারিত দেখতে C কোর্সের প্রিপ্রসেসর ও ম্যাক্রো পর্ব পড়ুন।

৩ · constexpr ফাংশন — একই ফাংশন, দুই সময়ে

ধ্রুবক লেখা সহজ অংশ। আসল শক্তি আসে যখন একটি পুরো ফাংশনকে constexpr বলেন। এর মানে: “এই ফাংশনটি compile-এর সময়েও চালানো যাবে।” লক্ষ করুন — যাবে, যেতে হবে না। একই ফাংশন কোথায় চলবে, তা ঠিক হয় ডাকার জায়গা দেখে:

  • ফলটি যদি এমন জায়গায় লাগে যেখানে compile-এর সময়ের মান বাধ্যতামূলক — constexpr ভ্যারিয়েবল, static_assert, array-র মাপ, template argument — তাহলে compiler ফাংশনটি নিজেই চালায়। সব argument-ও তখন ধ্রুবক হতে হবে।
  • argument যদি চলার সময়ের মান হয় — যেমন ব্যবহারকারীর ইনপুট — তাহলে ফাংশনটি একটি সাধারণ ফাংশনের মতো চলার সময় চলে। কোনো error নেই।

নিচের প্রোগ্রামে একটি টাকা-থেকে-পয়সা রূপান্তর, এক দিনের সেকেন্ড, আর ১ থেকে n পর্যন্ত যোগফলের একটি ফাংশন আছে। দুটি static_assert compiler-কে দিয়ে উত্তর যাচাই করিয়ে নেয় — ভুল হলে প্রোগ্রাম তৈরিই হতো না:

#include <print>

constexpr long long poisha(long long taka) { return taka * 100; }
constexpr int seconds_per_day = 24 * 60 * 60;

constexpr int sum_to(int n) {           // C++14 থেকে লুপ চলে
    int s = 0;
    for (int i = 1; i <= n; ++i) s += i;
    return s;
}

static_assert(seconds_per_day == 86400);
static_assert(sum_to(10) == 55);        // compiler নিজে হিসাব করে মিলিয়ে দেখে

int main() {
    constexpr long long fee = poisha(25);   // compile-এর সময়: 2500
    int n = 4;                              // সাধারণ ভ্যারিয়েবল
    for (int k = 0; k < 3; ++k) n += k;     // n = 7, ধ্রুবক নয়
    std::println("fee = {} poisha", fee);
    std::println("seconds per day = {}", seconds_per_day);
    std::println("sum_to({}) = {}", n, sum_to(n));  // চলার সময়ের ডাক
}

আউটপুট তিন লাইন: fee = 2500 poisha, seconds per day = 86400, sum_to(7) = 28. fee constexpr, তাই poisha(25) compiler নিজে চালিয়েছে — মেশিন কোডে শুধু 2500 সংখ্যাটি আছে। দুটি static_assert চলার সময় কোনো কাজই করে না; এরা compile-এর সময়ের পরীক্ষা। আর শেষ লাইনে sum_to(n)-এ n একটি সাধারণ ভ্যারিয়েবল, তাই একই sum_to এবার চলার সময় সাধারণ ফাংশনের মতো চলেছে। একটি ফাংশন, দুই জায়গায় ব্যবহার — আলাদা “compile-time সংস্করণ” লিখতে হয়নি। (টাকা-পয়সার হিসাব পূর্ণসংখ্যায় রাখা হয়েছে, কারণ double-এ ভগ্নাংশের ছোট ভুল জমে।)

এবার একই poisha ফাংশন চারটি ভিন্ন জায়গায় ডাকলে compiler কী করে, ধাপে ধাপে দেখুন। প্রতিটি ধাপে আগে নিজে অনুমান করুন — compile-এর সময়, চলার সময়, নাকি error?

ধ্রুবক argument, constexpr ফলcompile-এর সময়
constexpr long long fee = poisha(25);

compiler:  25 * 100 = 2500   (নিজেই হিসাব করল)
মেশিন কোডে: fee = 2500       (কোনো ডাক নেই, কোনো গুণ নেই)
static_assert-এর ভেতরেcompile-এর সময়ের পরীক্ষা
static_assert(poisha(2) == 200);    // সত্য: বিল্ড চলবে
static_assert(poisha(2) == 20);     // মিথ্যা:

error: static assertion failed
note: the comparison reduces to '(200 == 20)'
ব্যবহারকারীর ইনপুটচলার সময়
long long t;
std::cin >> t;                      // মান জানা যাবে শুধু চলার সময়
long long p = poisha(t);            // ঠিক আছে: সাধারণ ফাংশন-ডাক

ব্যবহারকারী 40 দিলে  =>  p = 4000  (চলার সময় হিসাব)
চলার সময়ের মান, কিন্তু constexpr দাবিcompile error
long long t;
std::cin >> t;
constexpr long long q = poisha(t);  // compiler-কে এখনই উত্তর দিতে বলা হলো

error: the value of 't' is not usable in a constant expression
  1. ধ্রুবক দিয়ে, ফল constexpr. fee constexpr, তাই এর মান compile-এর সময়েই চাই। argument 25 একটি ধ্রুবক। compiler ফাংশনটি নিজের ভেতরে চালিয়ে 2500 বের করে, আর সেই সংখ্যাটিই প্রোগ্রামে বসায়। চলার সময় poisha একবারও ডাকা হয় না।
  2. পরীক্ষার ভেতরে। static_assert-এর শর্ত compile-এর সময় হিসাব হওয়া বাধ্যতামূলক। তাই এখানেও compiler নিজে চালায়। ফল মিললে কিছু হয় না; না মিললে বিল্ড থামে, আর আধুনিক GCC দেখিয়ে দেয় দুই পাশের মান কত ছিল।
  3. ইনপুট দিয়ে। t-এর মান কেউ জানে না যতক্ষণ না প্রোগ্রাম চলে আর ব্যবহারকারী টাইপ করেন। তাতে সমস্যা নেই — p একটি সাধারণ ভ্যারিয়েবল, তাই poisha(t) চলার সময় সাধারণ ফাংশনের মতো চলে। constexpr ফাংশন মানে “compile-এর সময়ে পারে”, “শুধু compile-এর সময়ে” নয়।
  4. অসম্ভব দাবি। এবার ফলটিকে constexpr বলা হলো, অথচ argument চলার সময়ের। compiler-কে এমন উত্তর এখনই দিতে বলা হচ্ছে, যার তথ্য তার কাছে নেই। ফল compile error. সিদ্ধান্তটা কে নেয়, লক্ষ করুন: ফাংশন নয়, ডাকার জায়গা — ফলটি কোথায় যাচ্ছে।

সাধারণ ভুল ধারণা: “ফাংশনে constexpr লিখলেই সেটি compile-এর সময় চলবে।” না। int x = sum_to(10); — এখানে x সাধারণ ভ্যারিয়েবল, তাই standard অনুযায়ী এটি চলার সময়ের ডাক। optimizer চাইলে আগেই হিসাব করে রাখতে পারে (constexpr ছাড়াও পারত), কিন্তু নিশ্চয়তা নেই। compile-এর সময়ের হিসাব নিশ্চিত করতে ফলটি একটি constexpr ভ্যারিয়েবলে রাখুন, অথবা ফাংশনটি consteval করুন (৫ নম্বর অংশ)।

constexpr ফাংশন সম্পর্কে আরও দুটি কথা। এক, constexpr ফাংশন নিজে থেকেই inline — তাই এর পুরো সংজ্ঞা সাধারণত header-এ থাকে, কারণ compiler-কে ডাকার জায়গাতেই ফাংশনের ভেতরটা দেখতে হয়। দুই, constexpr ফাংশনে যে টাইপ ব্যবহার করা যায়, তাদের বলে literal type — int, double, pointer, আর এমন class যার অন্তত একটি constexpr constructor আছে। নিজের struct Money-কেও constexpr constructor দিয়ে compile-এর সময়ে ব্যবহারযোগ্য করা যায়।

৪ · static_assert — compile-এর সময়ের পাহারাদার

static_assert(শর্ত, "বার্তা"); compiler-কে বলে: “এই শর্তটি compile-এর সময় পরীক্ষা করো; মিথ্যা হলে বার্তাটি দেখিয়ে বিল্ড থামাও।” চলার সময় এর কোনো খরচ নেই — প্রোগ্রামে এর চিহ্নই থাকে না। C++11-এ বার্তা দেওয়া বাধ্যতামূলক ছিল; C++17 থেকে বার্তা ছাড়াও লেখা যায়।

সবচেয়ে বাস্তব ব্যবহারটি নেটওয়ার্ক বা ফাইলের ফরম্যাটে। ধরুন একটি মোবাইল অ্যাপ সার্ভারে একটি ছোট header পাঠায়, যার মাপ প্রোটোকলে ঠিক ৮ বাইট লেখা আছে। কেউ পরে একটি ফিল্ড যোগ করলেন, বা টাইপ বদলালেন, বা compiler padding যোগ করল — তখন অন্য প্রান্ত ভুল বাইট পড়বে, আর বাগটি ধরা পড়বে অনেক পরে, অদ্ভুত আচরণ হিসেবে। static_assert সেই অনুমানটিকে কোডে লিখে রাখে:

#include <cstdint>
#include <type_traits>

struct PacketHeader {
    std::uint16_t type;
    std::uint16_t length;
    std::uint32_t seq;
};

static_assert(sizeof(PacketHeader) == 8, "wire format: header must be 8 bytes");
static_assert(std::is_trivially_copyable_v<PacketHeader>,
              "header is copied byte-by-byte");

// কেউ 'std::uint8_t flags;' যোগ করলে:
// error: static assertion failed: wire format: header must be 8 bytes

এখানে দুই রকম প্রশ্ন করা হয়েছে। sizeof একটি পরিচিত compile-time প্রশ্ন — “এই টাইপ কত বাইট?” আর std::is_trivially_copyable_v হলো একটি type trait — টাইপ সম্পর্কে হ্যাঁ/না প্রশ্ন, যার উত্তর compile-এর সময়ে একটি bool ধ্রুবক। <type_traits> হেডারে এমন অনেক প্রশ্ন তৈরি আছে: std::is_integral_v<T> (T কি পূর্ণসংখ্যার টাইপ?), std::is_floating_point_v<T>, std::is_same_v<A, B> (দুটি টাইপ কি হুবহু এক?), std::is_signed_v<T>. এগুলো static_assert আর if constexpr-এর স্বাভাবিক সঙ্গী।

আরও কয়েকটি জায়গা, যেখানে static_assert অভিজ্ঞ কোডে প্রায়ই দেখা যায়:

  • প্ল্যাটফর্মের অনুমান: static_assert(sizeof(void*) == 8); — কোডটি যদি শুধু ৬৪-বিট সিস্টেমের জন্য লেখা হয়, ৩২-বিটে বিল্ডই হবে না।
  • টেবিল আর enum মেলানো: একটি enum-এ যতগুলো রং, নামের তালিকায় ততগুলো নাম — নতুন রং যোগ করে নাম লিখতে ভুলে গেলে বিল্ড থামবে।
  • template-এর শর্ত: static_assert(std::is_integral_v<T>, "integers only"); — ভুল টাইপ দিলে দীর্ঘ, দুর্বোধ্য error-এর বদলে একটি পরিষ্কার বাক্য। (পরের পর্বের concepts এ কাজ আরও সুন্দরভাবে করে।)
  • constexpr ফাংশনের টেস্ট: static_assert(sum_to(10) == 55); — ছোট unit test, যা প্রতিবার compile-এর সময় চলে।

একটি compile-time-যাচাই করা শতাংশ

একটি ছাড়ের হার কখনো ১০০%-এর বেশি বা ০%-এর কম হতে পারে না। চলার সময় পরীক্ষা করা যায়, কিন্তু হারটি যদি কোডেই লেখা থাকে, তবে ভুলটা চলার সময় পর্যন্ত অপেক্ষা করবে কেন? constexpr constructor-এ একটি পরীক্ষা বসান:

struct Percent {
    int value;
    constexpr explicit Percent(int v) : value(v) {
        if (v < 0 || v > 100) throw "percent must be 0..100";   // compile-এর সময় চললে: error
    }
};

constexpr Percent eid_discount{15};    // ঠিক আছে
constexpr Percent typo{150};           // compile error: throw চলেছে constant evaluation-এ

কৌশলটি হলো: constant evaluation চলার সময় যদি কোনো throw-এ পৌঁছায়, তবে ফলটি আর constant expression নয় — তাই constexpr ভ্যারিয়েবলে সেটি compile error. আর চলার সময় একই constructor ডাকা হলে সত্যিকারের exception ছোড়া হয় (পর্ব ৩৭)। একটি নিয়ম, দুই জায়গায় পাহারা। (বাস্তব কোডে "..."-এর বদলে std::invalid_argument ছুড়ুন; এখানে সংক্ষেপের জন্য লেখা।)

৫ · consteval আর constinit — C++20-এর দুই কড়া নিয়ম

constexpr নমনীয় — compile-এর সময়েও চলে, চলার সময়েও। কখনো কখনো এই নমনীয়তাই সমস্যা। C++20 দুটি নতুন শব্দ এনেছে, যারা দুই ভিন্ন দিক থেকে নিয়ম কড়া করে।

consteval — “শুধু compile-এর সময়”

consteval দিয়ে ঘোষিত ফাংশনকে বলে immediate function. এর প্রতিটি ডাক compile-এর সময়েই হিসাব হতে হবে, নইলে compile error. চলার সময়ের সংস্করণ বলে কিছু নেই।

consteval int checked_percent(int p) {
    if (p < 0 || p > 100) throw "bad percent";
    return p;
}

int vat = checked_percent(15);          // ঠিক: 15 যাচাই হলো compile-এর সময়েই
int oops = checked_percent(115);        // compile error
int user_rate = read_rate();
int r = checked_percent(user_rate);     // compile error: user_rate ধ্রুবক নয়

লক্ষ করুন প্রথম লাইনে vat একটি সাধারণ int, constexpr নয় — তবু ডাকটি compile-এর সময়েই হয়েছে, কারণ ফাংশনটি consteval. constexpr হলে এখানে কোনো নিশ্চয়তা থাকত না, আর 115-এর ভুলটিও চলার সময় পর্যন্ত লুকিয়ে থাকত। consteval কাজে লাগে যখন চলার সময় চলাটাই ভুল — যেমন ফরম্যাট-স্ট্রিং যাচাই। C++20-এর std::format("{} taka", x)-এর ফরম্যাট-স্ট্রিং ঠিক এভাবে compile-এর সময় পরীক্ষা হয়: {}-এর তুলনায় argument কম হলে, বা ফরম্যাটের নির্দেশ argument-এর টাইপের সাথে না মিললে compile error, চলার সময় নয়।

constinit — “শুরুর মান compile-এর সময়ে, পরে বদলানো যাবে”

global আর static ভ্যারিয়েবল (static storage duration) main() শুরুর আগেই তৈরি হয়। তাদের শুরুর মান বসে দুইভাবে। মানটি ধ্রুবক হলে compiler সেটি সরাসরি প্রোগ্রাম ফাইলে লিখে দেয় — একে বলে constant initialization, আর এতে কোনো কোড চলে না। মানটি কোনো ফাংশন-ডাক থেকে এলে main()-এর আগে সেই ফাংশন চালাতে হয় — একে বলে dynamic initialization. সমস্যাটা এখানেই: ভিন্ন translation unit-এর (ভিন্ন .cpp ফাইলের) global-গুলোর dynamic initialization কোন ক্রমে হবে, standard তা ঠিক করে দেয় না।

এই অনিশ্চয়তা থেকে যে কুখ্যাত বাগ জন্মায়, তার নাম static initialization order fiasco. ধাপে ধাপে দেখুন:

  1. config.cppConfig cfg = load();dynamic init
  2. logger.cppLogger lg{cfg.level};cfg-এর ওপর নির্ভর
  3. linker-এর ক্রমlg আগে?অনির্দিষ্ট
  4. constinitবিল্ডেই মানক্রমের প্রশ্ন নেই
config.cppফাংশন-ডাক থেকে মান
Config load();               // ফাইল পড়ে সেটিং আনে
Config cfg = load();         // main()-এর আগে চালাতে হবে
logger.cppঅন্য ফাইলের global ব্যবহার
extern Config cfg;
Logger lg{cfg.level};        // cfg কি তৈরি হয়ে গেছে?
main()-এর আগেক্রম standard-এ বলা নেই
বিল্ড A:  cfg = load()  →  lg{cfg.level}     ঠিকঠাক চলে
বিল্ড B:  lg{cfg.level}  →  cfg = load()     cfg তখনো তৈরি হয়নি!

ফাইলের ক্রম বা linker বদলালেই ফল বদলায়।
constinitconstant initialization বাধ্যতামূলক
constinit int log_level = 2;        // বিল্ডের সময়ই ফাইলে লেখা
constinit int retries = read_env(); // error: compile-এর সময় মান নেই

log_level = 3;                      // ঠিক: constinit মানে const নয়
  1. প্রথম ফাইল। cfg-এর মান আসে load() ফাংশন থেকে, যা ফাইল পড়ে। এটি dynamic initialization — main() শুরুর আগে কোনো এক সময়ে চলবে।
  2. দ্বিতীয় ফাইল। logger তৈরি হওয়ার সময় cfg.level পড়ছে। একই ফাইলের ভেতরে global-গুলো উপর থেকে নিচে তৈরি হয়, কিন্তু এরা দুটি ভিন্ন ফাইলে।
  3. লটারি। ভিন্ন ফাইলের dynamic initialization-এর ক্রম standard ঠিক করে না। এক বিল্ডে cfg আগে, আরেক বিল্ডে lg আগে। ভুল ক্রমে lg এমন একটি object পড়ে, যার constructor তখনো চলেনি। কোড না বদলে শুধু ফাইলের ক্রম বা compiler বদলালে বাগ আসে-যায় — খোঁজা খুব কঠিন।
  4. constinit-এর সমাধান। constinit বলে: “এই ভ্যারিয়েবলের শুরুর মান compile-এর সময়েই জানতে হবে।” তাহলে সেটি constant initialization — প্রোগ্রাম ফাইলেই লেখা, কোনো ক্রমের প্রশ্ন নেই, যেকোনো ফাইল যেকোনো সময় নিরাপদে পড়তে পারে। মান compile-এর সময় না মিললে compile error. আর constexpr-এর মতো এটি const নয় — পরে বদলানো যায়।

constinit তাই একটি নিশ্চয়তা চাওয়া, নতুন কোনো ক্ষমতা নয়। int counter = 0; আজও constant initialization পায়; constinit শুধু নিশ্চিত করে যে কেউ পরে অজান্তে int counter = compute(); লিখে সেটিকে dynamic বানিয়ে ফেললে compiler চেঁচাবে। যে global সত্যিই চলার সময়ের মানের ওপর নির্ভর করে, তার জন্য পুরোনো, পরীক্ষিত সমাধান হলো ফাংশনের ভেতরের static ভ্যারিয়েবল — প্রথমবার ডাকার সময় তৈরি হয়, C++11 থেকে thread-safe ভাবে।

শব্দকোথায় বসেকী নিশ্চিত করেপরে বদলানো যায়?
constexprভ্যারিয়েবল, ফাংশনভ্যারিয়েবল: মান compile-এর সময়ে। ফাংশন: compile-এর সময়ে চলতে পারেনা (ভ্যারিয়েবল const)
consteval (C++20)শুধু ফাংশনপ্রতিটি ডাক compile-এর সময়ে, নইলে errorপ্রযোজ্য নয়
constinit (C++20)static/thread_local ভ্যারিয়েবলশুরুর মান compile-এর সময়েহ্যাঁ
constভ্যারিয়েবল, member functionশুধু-পড়া; মান যেকোনো সময়েনা

৬ · if constexpr — template-এ শাখা ছেঁটে ফেলা

পর্ব ২৮-এ আপনি template দেখেছেন — একই ফাংশন নানা টাইপের জন্য। কিন্তু প্রায়ই টাইপ অনুযায়ী কাজ একটু আলাদা হতে হয়। ধরুন একটি describe ফাংশন: পূর্ণসংখ্যা হলে “এটি জোড় না বিজোড়” বলবে, ভগ্নাংশ হলে দুই ঘর দশমিকে দেখাবে, string হলে দৈর্ঘ্য বলবে। সাধারণ if দিয়ে লিখলে কী হয়?

template <typename T>
std::string describe(const T& v) {
    if constexpr (std::is_integral_v<T>)
        return v % 2 == 0 ? "even" : "odd";          // শুধু integer-এ % চলে
    else if constexpr (std::is_floating_point_v<T>)
        return std::format("{:.2f}", v);
    else
        return "text of " + std::to_string(v.size()); // শুধু string-এ .size()
}

সাধারণ if-এ সব শাখা প্রতিটি টাইপের জন্য compile হয়, শর্ত মিথ্যা হলেও। তখন describe(3.5)-এর জন্য compiler v % 2-ও compile করতে চাইবে, আর double-এ % নেই — error. if constexpr-এর (C++17) শর্ত compile-এর সময় হিসাব হয়, আর template-এর ভেতরে মিথ্যা শাখাটি সেই টাইপের জন্য বাদ পড়ে যায় — একে বলে discarded statement. বাদ পড়া কোড instantiate হয় না, তাই সেখানে এমন কোড থাকতে পারে যা এই টাইপে অর্থহীন। ধাপে ধাপে দেখুন:

1if constexpr (std::is_integral_v<T>)2    return v % 2 == 0 ? "even" : "odd";3else if constexpr (std::is_floating_point_v<T>)4    return std::format("{:.2f}", v);5else6    return "text of " + std::to_string(v.size());
এই instantiation-এ
ডাক
T
রাখা শাখা
বাদ পড়া কোড
  1. T = int. std::is_integral_v<int> সত্য। compiler শুধু প্রথম শাখা রাখে; বাকি দুটি এই instantiation-এ নেই। int-এ .size() নেই — কিছু যায় আসে না, কারণ সেই লাইন compile-ই হচ্ছে না। ফল odd.
  2. T = double. প্রথম শর্ত মিথ্যা, দ্বিতীয়টি সত্য। এবার v % 2 বাদ পড়েছে — double-এ % চলে না, কিন্তু সেই কোড আর নেই। ফল 3.14. লক্ষ করুন, এখানে একটি আলাদা ফাংশন তৈরি হয়েছে; int-এর সংস্করণের সাথে এর কোনো সম্পর্ক নেই।
  3. T = std::string. দুটি শর্তই মিথ্যা, তাই else শাখা থাকে। “Dhaka”-র দৈর্ঘ্য ৫, ফল text of 5. চলার সময় কোনো শর্ত পরীক্ষা হয় না — প্রতিটি সংস্করণে শুধু নিজের শাখাটি আছে, কোনো if-ই নেই।
  4. যদি সাধারণ if হতো। সাধারণ if-এর শর্ত চলার সময় দেখা হয়, তাই compiler-কে প্রতিটি শাখা প্রতিটি টাইপের জন্য compile করতে হয়। double-এর জন্য v % 2 compile করতে গিয়ে error. পুরোনো C++-এ এই সমস্যার সমাধান ছিল overloading বা template specialization-এর জটিল কৌশল — if constexpr সেটিকে একটি পড়ার মতো if-এ নামিয়ে এনেছে।

দুটি সতর্কতা। এক, শাখা বাদ পড়ে শুধু template-এর ভেতরে, যেখানে শর্তটি template parameter-এর ওপর নির্ভর করে। সাধারণ (non-template) ফাংশনে if constexpr-এর মিথ্যা শাখাও পুরোপুরি বৈধ কোড হতে হয়। দুই, বাদ পড়া শাখাও ব্যাকরণগতভাবে ঠিক হতে হয় — বানান ভুল বা বন্ধনী না মেলা চলবে না। আর “বাকি কোনো টাইপ চলবে না” বলতে শেষ else-এ static_assert(false, "...") লেখা C++23 থেকে বৈধ (GCC 13, Clang 17 এবং পরের সংস্করণে কাজ করে); তার আগে sizeof(T) == 0-এর মতো T-নির্ভর একটি শর্ত লিখতে হতো।

ফাংশনটি এখন কোথায় চলছে? — std::is_constant_evaluated আর if consteval

কখনো একটি constexpr ফাংশন চায় দুই জায়গায় দুই রকম পথ নিতে — compile-এর সময় সরল, বহনযোগ্য লুপ; চলার সময় CPU-র বিশেষ নির্দেশনা বা দ্রুত লাইব্রেরি, যা compile-এর সময় চলে না। C++20 এজন্য দিয়েছে std::is_constant_evaluated(), আর C++23 দিয়েছে আরও পরিষ্কার if consteval:

constexpr int popcount32(unsigned x) {
    if consteval {                    // C++23: constant evaluation চলছে
        int c = 0;
        for (; x; x &= x - 1) ++c;    // সরল লুপ, compile-এর সময়ে চলে
        return c;
    } else {
        return std::popcount(x);      // চলার সময় দ্রুত পথ
    }
}
// ফাঁদ: if constexpr (std::is_constant_evaluated()) — সবসময় true!

শেষ লাইনের ফাঁদটি AI-এর কোডে প্রায়ই দেখা যায়। if constexpr-এর শর্ত নিজেই একটি constant evaluation, তাই তার ভেতরে std::is_constant_evaluated() সবসময় true দেয় — চলার সময়ের শাখা কখনো চলে না। GCC এখানে warning দেয়। (উদাহরণটি সরল রাখতে else শাখায় std::popcount রাখা হয়েছে, যদিও সেটি নিজেও constexpr; বাস্তবে সেখানে থাকে compiler-এর বিশেষ ফাংশন বা CPU-নির্দেশনা, যা compile-এর সময় চলে না। দুটি পথের ফল অবশ্যই এক হতে হবে; নইলে একই ফাংশন দুই জায়গায় দুই উত্তর দেবে।)

৭ · compile-time lookup table — একবার বানান, সবসময় পড়ুন

এবার শুরুর কুইজ অ্যাপে ফিরি। অ্যাপটি প্রশ্ন বানায় “এই সংখ্যাটি কি মৌলিক?” — আর উত্তর যাচাইয়ে লাগে ৫০-এর নিচের মৌলিক সংখ্যার একটি টেবিল। এমন আগে থেকে হিসাব করে রাখা উত্তরের তালিকাকে বলে lookup table. constexpr ফাংশন দিয়ে টেবিলটি বানিয়ে std::array-এ রাখলে পুরো কাজ হয় compile-এর সময়ে। পদ্ধতিটি প্রাচীন: Eratosthenes-এর ছাঁকনি — ২ থেকে শুরু করে প্রতিটি মৌলিক সংখ্যার গুণিতক কেটে দেওয়া।

#include <array>
#include <print>

constexpr int N = 50;
constexpr std::array<bool, N> make_sieve() {
    std::array<bool, N> is_prime{};             // সব false
    for (int i = 2; i < N; ++i) is_prime[i] = true;
    for (int i = 2; i * i < N; ++i)
        if (is_prime[i])
            for (int j = i * i; j < N; j += i) is_prime[j] = false;
    return is_prime;
}
constexpr auto prime = make_sieve();            // compiler বানায় টেবিলটি
static_assert(prime[47] && !prime[49]);         // 47 মৌলিক, 49 = 7 x 7

int main() {
    int count = 0;
    for (int i = 0; i < N; ++i)
        if (prime[i]) { std::print("{} ", i); ++count; }
    std::println("\ncount = {}", count);
}

আউটপুটের প্রথম লাইনে ১৫টি মৌলিক সংখ্যা: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47, দ্বিতীয় লাইনে count = 15. make_sieve() একটি সাধারণ দেখতে ফাংশন — লুপ, শর্ত, array-তে লেখা — শুধু সামনে constexpr. prime constexpr বলে compiler ফাংশনটি নিজে চালিয়ে ৫০টি bool-এর টেবিল প্রোগ্রাম ফাইলের শুধু-পড়ার অংশে লিখে দিয়েছে। চলার সময় main() শুধু টেবিল পড়ছে। আর static_assert টেবিলের দুটি ঘর যাচাই করছে — ছাঁকনির লুপে ভুল থাকলে (ধরুন i * i < N-এর বদলে i < 7) প্রোগ্রাম তৈরিই হতো না।

একই কৌশল বাস্তবের বড় টেবিলেও। ফাইল বা নেটওয়ার্কের ডেটা ঠিক আছে কি না যাচাই করতে বহুল ব্যবহৃত CRC-32 হিসাব দ্রুত হয় একটি ২৫৬ ঘরের টেবিল দিয়ে। পুরোনো কোডে এই টেবিল হাতে কপি করা ২৫৬টি hex সংখ্যা — একটি অঙ্ক ভুল হলে খুঁজে পাওয়া প্রায় অসম্ভব। constexpr দিয়ে টেবিলটি নিয়ম থেকেই তৈরি হয়:

constexpr std::array<std::uint32_t, 256> make_crc_table() {
    std::array<std::uint32_t, 256> t{};
    for (std::uint32_t i = 0; i < 256; ++i) {
        std::uint32_t c = i;
        for (int k = 0; k < 8; ++k)
            c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
        t[i] = c;
    }
    return t;
}
constexpr auto crc_table = make_crc_table();
static_assert(crc_table[1] == 0x77073096u);   // প্রচলিত টেবিলের সাথে মিল

টেবিল যতই বড় হোক, চলার সময়ের খরচ শূন্য — আর static_assert প্রমাণ দেয় যে হিসাবটি প্রচলিত CRC-32 টেবিলের সাথে মেলে। একই ধাঁচে বানানো যায় একক-রূপান্তরের টেবিল (মিটার থেকে ফুট, টাকা থেকে পয়সা), বর্গ বা ঘাতের টেবিল, গেমের sine টেবিল, বা একটি ছোট parser-এর অক্ষর-শ্রেণির টেবিল।

constexpr-এ কী কী চলে — একটি ক্রমবর্ধমান তালিকা

উপরের ছাঁকনির ফাংশনটি C++11-এ লেখা যেত না। constexpr শুরু হয়েছিল খুব কড়া নিয়মে, আর প্রতিটি নতুন standard-এ এর সীমা বেড়েছে:

Standardconstexpr ফাংশনে যা নতুন চলল
C++11constexpr-এর জন্ম। ফাংশনের দেহে কার্যত একটিই return বাক্য; লুপের কাজ করতে হতো recursion আর ?: দিয়ে
C++14local ভ্যারিয়েবল, একাধিক বাক্য, if, for/while লুপ, local ভ্যারিয়েবল বদলানো — সাধারণ ফাংশনের মতো লেখা
C++17constexpr lambda, if constexpr; std::array-এর বেশিরভাগ কাজ constexpr
C++20virtual ফাংশন, try/catch ব্লক (তবে exception ছোড়া চলবে না), dynamic_cast; new/delete — শর্ত হলো হিসাব শেষের আগেই মুছতে হবে; তাই std::vector আর std::string constexpr ফাংশনের ভেতরে; সাথে consteval, constinit, std::is_constant_evaluated
C++23if consteval; constexpr ফাংশনে static constexpr local ভ্যারিয়েবল; std::unique_ptr আর std::bitset-সহ আরও লাইব্রেরি constexpr; কিছু পুরোনো নিষেধ শিথিল
C++26 (প্রক্রিয়াধীন)constant evaluation-এ exception ছোড়া-ধরা, constexpr placement new, আর static reflection-এর মতো বড় সংযোজন

C++20-এর new-এর নিয়মটি ভালো করে বুঝুন — একে বলে transient allocation (ক্ষণস্থায়ী বরাদ্দ)। compile-এর সময় heap-এ মেমরি নেওয়া যায়, কিন্তু constant evaluation শেষ হওয়ার আগে সেটি ফেরত দিতে হয়। কারণটি স্বাভাবিক: compiler-এর heap চলার সময়ের প্রোগ্রামে নেই, তাই সেখানের কোনো ঠিকানা প্রোগ্রামে “বেঁচে” থাকতে পারে না। ফলে:

constexpr int count_words(std::string_view s) {
    std::vector<std::string> words;              // C++20: ভেতরে vector চলে
    /* ... s ভেঙে words-এ রাখা ... */
    return static_cast<int>(words.size());       // vector এখানেই মুছে যায়: ঠিক
}
constexpr int n = count_words("ami banglay gaan gai");   // ঠিক
constexpr std::vector<int> v{1, 2, 3};          // error: মেমরি constant evaluation-এর পরেও বেঁচে থাকত

ফল যদি একটি তালিকা হয়, তাহলে ভেতরে std::vector-এ কাজ করে শেষে একটি নির্দিষ্ট মাপের std::array-এ কপি করে ফেরত দিন — যেমন ছাঁকনির উদাহরণে।

৮ · compiler যখন বাগ ধরে — UB নিষেধ, আর এর দাম

constant evaluation-এর সবচেয়ে কম আলোচিত, কিন্তু সবচেয়ে দামি গুণটি হলো: সেখানে undefined behavior নিষিদ্ধ। standard বলে, যে হিসাবে ভাষার স্তরের UB ঘটে, সেটি constant expression নয়। তাই constexpr প্রসঙ্গে signed integer overflow, array-র সীমার বাইরে পড়া, শূন্য দিয়ে ভাগ, null pointer অনুসরণ, মুছে ফেলা মেমরি পড়া — সবই compile error হয়ে ধরা পড়ে। চলার সময় এগুলো নীরবে ভুল ফল দিত, বা কখনো কিছুই না দেখিয়ে চলে যেত। ধাপে ধাপে তুলনা করুন:

একটি সরল ফাংশনint-এর সীমা ভুলে যাওয়া
constexpr int square(int x) { return x * x; }

// int সাধারণত 32-বিট: সর্বোচ্চ 2147483647
// square(50000) = 2500000000  — int-এ ধরে না
constexpr প্রসঙ্গেcompile error (GCC, মোটামুটি)
constexpr int area = square(50000);

error: overflow in constant expression [-fpermissive]
  in 'constexpr' expansion of 'square(50000)'
চলার সময়undefined behavior
int side = read_side();       // ব্যবহারকারী দিলেন 50000
int area = square(side);      // UB: standard কোনো নিশ্চয়তা দেয় না

সাধারণত দেখা যায়: area = -1794967296
কখনো optimizer ধরে নেয় overflow হয়ই না, আর পরের if-ও উধাও।
ব্যবহার: constexpr টেস্টUB-detector হিসেবে compiler
static_assert(square(46340) == 2147395600);   // ঠিক: সীমার ভেতরে
static_assert(square(46341) > 0);             // error: overflow ধরা পড়ল

=> ফাংশনের সীমা খুঁজে পাওয়া গেল প্রোগ্রাম চালানোর আগেই
  1. ফাংশনটি নিরীহ। একটি গুণ, একটি return. কিন্তু int-এর একটি সীমা আছে, আর ৫০,০০০-এর বর্গ সেটি ছাড়িয়ে যায়। signed integer overflow C++-এ undefined behavior (পর্ব ৫)।
  2. compile-এর সময় — থামিয়ে দেয়। compiler নিজে হিসাব করতে গিয়ে দেখে, ফল int-এ ধরে না। constant evaluation-এ UB চলবে না, তাই বিল্ড থামে। ঠিক বার্তা compiler-ভেদে আলাদা; Clang বলে মানটি “outside the range of representable values of type 'int'”. মূল কথা একই: ভুলটি এখনই, আপনার সামনে।
  3. চলার সময় — চুপচাপ ভুল। একই ফাংশন, একই সংখ্যা, শুধু এবার মানটি চলার সময় এল। কোনো error নেই। বাস্তবে সাধারণত একটি ঋণাত্মক সংখ্যা আসে, কিন্তু standard তারও নিশ্চয়তা দেয় না — optimizer “overflow হবে না” ধরে নিয়ে পরের পরীক্ষাগুলো মুছে দিতেও পারে। এই ধরনের বাগ খুঁজতে লাগে sanitizer (পর্ব ৪৩)।
  4. শিক্ষাটা কাজে লাগানো। constexpr ফাংশনের কিছু টেস্ট static_assert-এ লিখলে compiler একটি বিনামূল্যের UB-detector হয়ে যায়: প্রতিটি টেস্ট এমন একটি পরিবেশে চলে, যেখানে UB মানেই error. ৪৬৩৪০-এর বর্গ সীমার ভেতরে, ৪৬৩৪১-এরটি নয় — এই সীমা জানা গেল একটি লাইনও না চালিয়ে।

একটি সূক্ষ্ম ব্যতিক্রম জেনে রাখুন: নিয়মটি ভাষার নিজের UB-এর জন্য বাধ্যতামূলক। standard library-র ফাংশনের পূর্বশর্ত ভাঙা (যেমন খালি vector-এ front()) compiler compile-এর সময়ে ধরতে পারে, কিন্তু standard সেটি বাধ্যতামূলক করেনি। বাস্তবে GCC আর Clang অনেকগুলোই ধরে।

দাম কত — compile-এর সময়ের হিসাব বিনামূল্যে নয়

  • বিল্ড ধীর হয়। compiler আসলে একটি interpreter চালিয়ে আপনার কোড চালায়, যা মেশিন কোডের চেয়ে অনেক ধীর। ৫০ ঘরের টেবিল কিছুই না, কিন্তু ১০ লাখ ঘরের টেবিল বিল্ডে উল্লেখযোগ্য সময় নিতে পারে। আর header-এ থাকলে প্রতিটি .cpp ফাইল নতুন করে হিসাব করে।
  • compiler-এর সীমা আছে। লুপের পাক, recursion-এর গভীরতা আর মোট ধাপের একটি সীমা compiler নিজে বসায়; ছাড়ালে error. GCC-তে -fconstexpr-loop-limit, -fconstexpr-ops-limit; Clang-এ -fconstexpr-steps দিয়ে বদলানো যায়।
  • ডিবাগ কঠিন। compile-এর সময়ের হিসাবে breakpoint বসানো যায় না, std::println দিয়ে মান ছাপানো যায় না। কৌশল: ফাংশনটি আগে চলার সময়ে চালিয়ে debugger-এ ঠিক করুন, তারপর constexpr-এ ব্যবহার করুন — একই কোড দুই জায়গায় চলে বলেই এটি সম্ভব।
  • প্রোগ্রাম বড় হতে পারে। বড় টেবিল প্রোগ্রাম ফাইলেই থাকে। কখনো চলার সময় একবার বানানো বা ফাইল থেকে পড়াই ভালো।

নিয়মটা তাই: যে মান সত্যিই ধ্রুবক আর ছোট থেকে মাঝারি হিসাবের, সেখানে constexpr. যে ফাংশন সরল আর বিশুদ্ধ (বাইরের অবস্থা পড়ে না), তাকে constexpr করা প্রায় বিনামূল্যে — কিন্তু “সব কিছুতে constexpr” লক্ষ্য নয়। ফাইল, নেটওয়ার্ক, সময় বা random-নির্ভর কিছু কখনো constexpr হবে না।

compile-এর সময়ে হিসাবের গল্পটা শুরু হয়েছিল একটি দুর্ঘটনার মতো। ১৯৯৪ সালে C++ standard কমিটির এক সভায় Erwin Unruh একটি প্রোগ্রাম দেখান, যা compile-ই হয় না — কিন্তু compiler-এর error message-এ একে একে মৌলিক সংখ্যাগুলো ছাপা হয়। template দিয়ে লেখা সেই কোড দেখাল যে template ব্যবস্থাটি নিজেই একটি পূর্ণ হিসাবযন্ত্র। এর থেকে জন্ম নেয় template metaprogramming — শক্তিশালী, কিন্তু পড়তে ও লিখতে ভয়ংকর কঠিন। C++11-এর constexpr, যার মূল প্রস্তাবক Gabriel Dos Reis আর Bjarne Stroustrup, সেই একই ক্ষমতা সাধারণ ফাংশনের ভাষায় ফিরিয়ে দেয়।

পরীক্ষা ও ইন্টারভিউতে
  • const আর constexpr-এর পার্থক্য কী? — const মানে শুধু-পড়া, মান চলার সময়েও আসতে পারে; constexpr মানে মান compile-এর সময়ে জানা থাকতেই হবে, আর constexpr ভ্যারিয়েবল নিজে থেকেই const.
  • constexpr ফাংশন কি সবসময় compile-এর সময়ে চলে? — না; constant expression দরকার এমন জায়গায় (constexpr ভ্যারিয়েবল, static_assert, array-র মাপ) চলে, চলার সময়ের argument দিলে সাধারণ ফাংশনের মতো চলে।
  • consteval আর constexpr ফাংশনের পার্থক্য? — consteval (C++20) ফাংশনের প্রতিটি ডাক compile-এর সময়ে হতে হবে, নইলে error; constexpr দুই জায়গাতেই চলতে পারে।
  • constinit কোন সমস্যার সমাধান? — static initialization order fiasco; এটি static/global ভ্যারিয়েবলের শুরুর মান compile-এর সময়ে নিশ্চিত করে, কিন্তু const বানায় না।
  • if constexpr আর সাধারণ if-এর পার্থক্য? — if constexpr-এর শর্ত compile-এর সময় হিসাব হয়, আর template-এ মিথ্যা শাখা instantiate হয় না; সাধারণ if-এ সব শাখা compile হয়।

৯ · AI-এর যুগে — compile-time কোডের ভুল ধরা

AI টুল compile-time কোডে দুই বিপরীত দিকে ভুল করে। একদিকে, অনলাইনে C আর পুরোনো C++-এর বিপুল কোড আছে, তাই AI এখনো প্রায়ই #define দিয়ে ধ্রুবক লেখে, বা ২৫৬টি সংখ্যার টেবিল হাতে বসিয়ে দেয়। অন্যদিকে, “আধুনিক C++” বললে কখনো কখনো সবকিছুতে constexpr বসিয়ে দেয় — এমন ফাংশনেও, যা ফাইল পড়ে বা কখনো compile-এর সময়ে ডাকা হবে না। আর C++11-যুগের template metaprogramming-এর কৌশল প্রায়ই সেখানে আসে, যেখানে একটি সাধারণ constexpr ফাংশন যথেষ্ট। সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
#define MAX_USERS 100টাইপ নেই, scope নেই, debugger-এ নাম নেইconstexpr int max_users = 100;
“ধ্রুবক” বলতে const, আর ধরে নেয় সেটি compile-timeমান চলার সময়ে আসতে পারে; array-র মাপে বা static_assert-এ errorcompile-time দরকার হলে constexpr
ফলটি সাধারণ ভ্যারিয়েবলে রেখে বলে “এটি compile-time-এ হিসাব হবে”standard নিশ্চয়তা দেয় নাconstexpr ভ্যারিয়েবল, বা consteval
ফাইল পড়া বা লগ লেখা ফাংশনেও constexprকখনো compile-এর সময়ে চলবে না; পাঠককে বিভ্রান্ত করেশুধু বিশুদ্ধ, সত্যিই ব্যবহৃত ফাংশনে
factorial বা টেবিলের জন্য recursive template structপড়া কঠিন, error message দীর্ঘলুপসহ একটি constexpr ফাংশন
if constexpr (std::is_constant_evaluated())সবসময় true; চলার সময়ের শাখা মৃতif consteval বা সাধারণ if
বড় টেবিল হাতে কপি করা hex সংখ্যায়একটি অঙ্ক ভুল হলে ধরা অসম্ভবconstexpr ফাংশনে নিয়ম থেকে তৈরি + static_assert

AI-কে compile-time কোড লিখতে বলার সময় কোনটি কখন হিসাব হবে, তা স্পষ্ট করে দিন, আর প্রমাণ হিসেবে static_assert চান:

আমি C++23 শিখছি (g++, -std=c++23 -Wall -Wextra). একটি কুইজ অ্যাপের জন্য compile-time টেবিল বানাও: ১. 1000-এর নিচের মৌলিক সংখ্যার তালিকা একটি constexpr ফাংশন দিয়ে তৈরি করো, ফল std::array-তে; কোনো #define নয়, কোনো template metaprogramming নয়, হাতে লেখা সংখ্যার তালিকা নয়। ২. টেবিলটি একটি constexpr ভ্যারিয়েবলে রাখো, যাতে compile-এর সময় হিসাব হওয়া নিশ্চিত হয়। ৩. অন্তত চারটি static_assert দাও: মোট সংখ্যা কত, প্রথম আর শেষ মৌলিক সংখ্যা, আর একটি যৌগিক সংখ্যা যে তালিকায় নেই। ৪. কোন ফাংশন constexpr, কোনটি consteval আর কোনটি সাধারণ — প্রতিটির কারণ এক লাইনে লেখো। যেটি কখনো compile-এর সময়ে চলবে না, তাতে constexpr দিও না। ৫. const আর constexpr কোথায় কোনটি দিলে, আর কেন — কোডের পরে একটি ছোট তালিকা দাও। ৬. টেবিল বানাতে compile-এর সময় কতটা কাজ হচ্ছে, আর বিল্ড-সময়ের ওপর প্রভাব কেমন হতে পারে, তা সংক্ষেপে বলো।

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

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

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

প্র ০১C++11-এ constexpr কেন আনা হলো? template আর macro দিয়েই তো compile-এর সময়ে হিসাব করা যেত।

C++11-এর আগে “compile-এর সময়ের ধ্রুবক” পাওয়ার তিনটি পথ ছিল, আর তিনটিরই বড় ত্রুটি। প্রথম, macro — টাইপহীন লেখা-বদল, যা scope মানে না আর argument একাধিকবার হিসাব করে। দ্বিতীয়, const int — কিন্তু এটি শুধু integer টাইপে আর শুধু সরল প্রকাশে constant expression হতো; একটি ফাংশন ডাকলেই সেই সুযোগ শেষ। std::numeric_limits<int>::max() একটি ফাংশন, তাই C++03-এ এটি দিয়ে array-র মাপ ঠিক করা যেত না — লাইব্রেরিকে আলাদা করে INT_MAX macro রাখতে হতো। তৃতীয়, template metaprogramming — শক্তিশালী, কিন্তু একটি factorial লিখতে recursive struct, specialization আর enum { value = ... }-এর মতো কৌশল লাগত, যা সাধারণ প্রোগ্রামারের কাছে আরেকটি ভাষা।

Gabriel Dos Reis আর Bjarne Stroustrup-এর “generalized constant expressions” প্রস্তাব (২০০০-এর দশকের মাঝামাঝি থেকে আলোচিত, C++11-এ গৃহীত) একটি সরল প্রশ্ন তুলেছিল: যে ফাংশন বিশুদ্ধ — শুধু argument থেকে ফল বানায় — তাকে compiler কেন চালাতে পারবে না? তাঁদের লক্ষ্য ছিল শুধু সুবিধা নয়। সিস্টেম প্রোগ্রামিংয়ে, বিশেষ করে embedded ডিভাইসে, অনেক ডেটা ROM-এ রাখা দরকার — আর সেজন্য compiler-কে নিশ্চিত হতে হয় যে মানটি চলার আগেই জানা। আরেকটি লক্ষ্য ছিল user-defined টাইপকেও ধ্রুবক বানানো, যেমন একটি complex সংখ্যা বা একটি একক-সহ মাপ।

C++11-এর প্রথম সংস্করণ ইচ্ছা করেই খুব কড়া ছিল — ফাংশনের দেহ কার্যত একটি return. কারণ compiler নির্মাতারা জানতেন না পুরো ভাষা compile-এর সময়ে চালানো কতটা কঠিন হবে, আর কমিটি ধাপে ধাপে এগোতে চেয়েছিল। বাস্তব অভিজ্ঞতা দেখাল কাজটি সম্ভব, আর C++14 থেকে প্রায় প্রতিটি standard সীমা আরও খুলেছে।

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

প্র ০২error message-এ মৌলিক সংখ্যা ছাপানো একটি প্রোগ্রাম থেকে কীভাবে template metaprogramming-এর জন্ম, আর constexpr কি সেটিকে অপ্রয়োজনীয় করে দিয়েছে?

১৯৯৪ সালে C++ standard কমিটির একটি সভায় Erwin Unruh একটি ছোট প্রোগ্রাম দেখান। প্রোগ্রামটি compile হতো না — ইচ্ছা করেই। template-এর recursive instantiation দিয়ে সে compiler-কে প্রতিটি সংখ্যা মৌলিক কি না যাচাই করতে বাধ্য করত, আর মৌলিক হলে এমন একটি টাইপ রূপান্তর চাইত যা অসম্ভব। ফলে compiler-এর error message-এর তালিকায় একে একে মৌলিক সংখ্যাগুলো উঠে আসত। এটি ছিল একটি আবিষ্কার, পরিকল্পনা নয়: template ব্যবস্থাটি টাইপ-নিরাপদ generic কোডের জন্য বানানো হয়েছিল, কিন্তু দেখা গেল সেটি নিজেই একটি পূর্ণ প্রোগ্রামিং ভাষা — compile-এর সময়ে চলে।

এরপর Todd Veldhuizen “expression templates” আর template metaprogram নিয়ে লেখেন, Andrei Alexandrescu-র “Modern C++ Design” (২০০১) আর Boost-এর MPL লাইব্রেরি এই কৌশলকে একটি পুরো শাখায় পরিণত করে। কিন্তু এর ভাষা ছিল অদ্ভুত: লুপ নেই, শুধু recursion; ভ্যারিয়েবল নেই, শুধু টাইপ আর enum-এর মান; if নেই, শুধু specialization. একটি ভুল হলে error message শত লাইনের হতো। আর compiler প্রতিটি ধাপের জন্য একটি নতুন টাইপ তৈরি করত, তাই বড় হিসাবে compile সময় আর মেমরি দুটোই খুব বেড়ে যেত।

constexpr মান-এর হিসাব সেখান থেকে সরিয়ে এনেছে। factorial, টেবিল, string-এর হিসাব — এখন সাধারণ ফাংশন, আর বেশিরভাগ compiler-এ template recursion-এর চেয়ে দ্রুত ও কম মেমরিতে। কিন্তু template metaprogramming মরে যায়নি, কারণ একটি কাজ constexpr করতে পারে না: টাইপ নিয়ে হিসাব। “T যদি pointer হয়, তার ভেতরের টাইপ দাও”, “একটি টাইপের তালিকা থেকে সবচেয়ে বড়টি বেছে নাও” — এগুলো এখনো template, type trait, if constexpr আর পরের পর্বের concepts দিয়ে হয়। C++26-এর জন্য গৃহীত static reflection এই সীমাও কমানোর চেষ্টা করছে — টাইপকে একটি সাধারণ মান হিসেবে constexpr কোডে আনা।

মূল উপলব্ধি: নিয়ম হলো — মানের হিসাবে constexpr ফাংশন, টাইপের হিসাবে template ও type trait. একটি দুর্ঘটনাক্রমে আবিষ্কৃত ক্ষমতাকে ভাষা ধীরে ধীরে সত্যিকারের, পড়ার মতো যন্ত্রে বদলেছে।

প্র ০৩compile-এর সময়ে undefined behavior কেন নিষিদ্ধ, যখন চলার সময় C++ সেটি “অনুমতি” দেয়?

চলার সময়ের UB আসলে “অনুমতি” নয় — এটি একটি চুক্তি। standard বলে: আপনি প্রতিশ্রুতি দিন যে overflow বা সীমার বাইরে পড়া ঘটবে না, বিনিময়ে compiler প্রতিটি যোগে বা প্রতিটি array পড়ায় পরীক্ষা বসাবে না, আর সেই প্রতিশ্রুতি ধরে নিয়ে কোড দ্রুত করবে। চলার সময় প্রতিটি অপারেশন যাচাই করা ব্যয়বহুল, তাই এই চুক্তি গতির জন্য।

compile-এর সময় হিসাব বদলে যায়। compiler তখন আপনার কোড একটি interpreter-এ চালায়, যার কাছে প্রতিটি object-এর টাইপ, জীবনকাল আর সীমা জানা। প্রতিটি যোগে overflow পরীক্ষা করা তার কাছে প্রায় বিনামূল্যে, কারণ সে এমনিতেই প্রতিটি মান হাতে ধরে হিসাব করছে। আরেকটি গভীর কারণ: compile-এর সময়ের ফলটি প্রোগ্রামের অংশ হয়ে যায় — array-র মাপ, template argument, টাইপের পরিচয়। UB থাকলে “ফল যা খুশি হতে পারে” — তখন একই কোড দুটি compiler-এ দুটি ভিন্ন টাইপ তৈরি করতে পারত, বা cross-compile করার সময় (ধরুন x86-এ বসে ARM-এর জন্য বিল্ড) হোস্টের আচরণ টার্গেটে ঢুকে পড়ত। template-এর একটি instantiation দুই ফাইলে দুই রকম হলে one definition rule ভাঙে, যা আরও বড় বিপদ।

তাই standard constant expression-এর সংজ্ঞাতেই বলে দেয়: ভাষার স্তরের UB ঘটে এমন হিসাব constant expression নয়। এর একটি চমৎকার পার্শ্বফল আছে, যা C++ সম্প্রদায়ে জনপ্রিয় হয়েছে: constexpr টেস্টিং। একটি ফাংশনের unit test static_assert-এ লিখলে সেই টেস্ট এমন একটি পরিবেশে চলে, যেখানে UB মানেই compile error — অনেকটা sanitizer-এর মতো, কিন্তু চালানো ছাড়াই। সীমাবদ্ধতাও মনে রাখুন: এটি শুধু সেই পথগুলো যাচাই করে, যা টেস্টে চলে; আর standard library-র পূর্বশর্ত ভাঙা ধরা বাধ্যতামূলক নয়।

মূল উপলব্ধি: UB একটি গতি-বনাম-নিরাপত্তার বিনিময়। চলার সময় C++ গতি বেছে নেয়; compile-এর সময় নিরাপত্তা প্রায় বিনামূল্যে, আর ফলটি প্রোগ্রামের কাঠামোর অংশ — তাই সেখানে নিশ্চয়তাই একমাত্র যুক্তিসংগত পথ।

প্র ০৪static reflection কী, আর কেন একে C++-এর compile-time যাত্রার পরের বড় ধাপ বলা হয়?

একটি সাধারণ সমস্যা ভাবুন: একটি struct Student { std::string name; int roll; double gpa; };-কে JSON-এ লিখতে চান। আজকের C++-এ প্রতিটি struct-এর জন্য হাতে লিখতে হয় — “name” লেখো, তারপর name-এর মান, তারপর “roll”... নতুন ফিল্ড যোগ করে JSON-এর কোড বদলাতে ভুলে গেলে নীরব বাগ। একটি enum-এর মানকে তার নামের string-এ বদলানোও একই সমস্যা। কারণ C++-এর কোড নিজের গঠন সম্পর্কে প্রশ্ন করতে পারে না: “এই struct-এ কয়টি ফিল্ড, তাদের নাম কী?” Java বা C#-এর reflection এই প্রশ্নের উত্তর দেয়, কিন্তু চলার সময়ে, খরচসহ।

Static reflection মানে এই প্রশ্নগুলো compile-এর সময়ে করা। বহু বছর ধরে নানা প্রস্তাব এসেছে — প্রথমে একটি Technical Specification-এ template-ভিত্তিক নকশা, পরে মান-ভিত্তিক নকশা। শেষ পর্যন্ত Wyatt Childers, Peter Dimov, Dan Katz, Barry Revzin, Andrew Sutton, Faisal Vali, Daveed Vandevoorde প্রমুখের P2996 প্রস্তাব ২০২৫ সালে C++26-এর জন্য গৃহীত হয়। এর মূল ভাবনা: ^^Student লিখলে একটি “reflection” মান পাওয়া যায়, যা একটি constexpr ফাংশনে দিয়ে ফিল্ডের তালিকা, নাম, টাইপ জানা যায় — সবই compile-এর সময়ে। (compiler-এ এর পূর্ণ সমর্থন আসতে সময় লাগবে; বিস্তারিত সিনট্যাক্স নিয়ে চূড়ান্ত বাস্তবায়নই প্রমাণ।)

কেন এটি constexpr-এর গল্পের অংশ? কারণ নকশাটি দাঁড়িয়ে আছে এতদিনের constexpr-এর ওপর। C++20-এ constexpr-এ vector, C++23-এ আরও লাইব্রেরি — এগুলো ছাড়া compile-এর সময়ে ফিল্ডের তালিকা নিয়ে লুপ চালানো, string বানানো সম্ভব হতো না। template metaprogramming-এ যে কাজ টাইপের জটিল কৌশলে হতো, reflection সেটিকে সাধারণ মান আর সাধারণ ফাংশনে নামিয়ে আনতে চায় — ঠিক যেমন C++11-এর constexpr মানের হিসাবে করেছিল।

মূল উপলব্ধি: C++-এর compile-time যাত্রার দিক একটাই — “compile-এর সময়ের কোড” আর “সাধারণ কোড”-এর ফারাক কমানো। প্রথমে মান (constexpr), তারপর মেমরি (transient allocation), এবার প্রোগ্রামের নিজের গঠন — reflection দিয়ে।

প্র ০৫Zig-এর comptime আর Rust-এর const fn একই সমস্যা কীভাবে সমাধান করে? C++-এর পথ থেকে কী শেখার আছে?

Zig ভাষা (Andrew Kelley-র তৈরি, ২০১৬ সালের দিকে প্রকাশ্যে আসে) একটি সাহসী পথ নিয়েছে: আলাদা কোনো “compile-time ভাষা” নেই। যেকোনো সাধারণ ফাংশন comptime শব্দ দিয়ে compile-এর সময়ে চালানো যায়, আর compile-এর সময়ে টাইপও একটি সাধারণ মান। একটি ফাংশন একটি টাইপ নিয়ে আরেকটি টাইপ ফেরত দিতে পারে — Zig-এর generic কোড এভাবেই লেখা, template বলে আলাদা কিছু নেই। C++ যা চার দশকে template, constexpr, if constexpr, concepts আর reflection-এর আলাদা আলাদা যন্ত্রে করছে, Zig শুরু থেকেই একটি ধারণায় রেখেছে। দাম হলো, generic ফাংশনের শর্ত ঘোষণার আলাদা ব্যবস্থা কম, তাই ভুল ধরা পড়ে ব্যবহারের জায়গায়।

Rust-এর const fn C++-এর constexpr-এর খুব কাছের আত্মীয়: চিহ্নিত ফাংশন compile-এর সময়ে চলতে পারে, আবার চলার সময়েও। ২০১৮ সালে খুব ছোট রূপে স্থিতিশীল হয়, তারপর ধাপে ধাপে if, লুপ আর আরও অনেক কিছু যোগ হয় — ঠিক C++11 থেকে C++14-এর মতো একটি গল্প। Rust-এর compile-time interpreter-ও C++-এর মতো UB ধরতে চেষ্টা করে। তবে Rust সাধারণ কোডে টাইপ নিয়ে হিসাব করে trait আর generic দিয়ে, macro-ও আলাদা একটি ব্যবস্থা।

তুলনা থেকে কয়েকটি শিক্ষা। এক, সব ভাষাই একই জায়গায় পৌঁছাচ্ছে — compile-এর সময়ে চলা কোড সাধারণ কোডের মতো দেখতে হওয়া উচিত। দুই, C++-এর পথ ধীর আর স্তরে স্তরে, কারণ পুরোনো কোডের সাথে সামঞ্জস্য রাখতে হয় — template metaprogramming-এ লেখা লক্ষ লক্ষ লাইন এখনো চলে। তিন, “একই ফাংশন দুই জায়গায় চলে” নকশাটি তিন ভাষাতেই আছে, কারণ এতে টেস্ট আর ডিবাগ সহজ। (D ভাষাও অনেক আগেই সাধারণ ফাংশন compile-এর সময়ে চালানোর ব্যবস্থা এনেছিল, আর C++-এর আলোচনায় সেটি প্রায়ই উদাহরণ হিসেবে আসে।)

মূল উপলব্ধি: compile-time হিসাব আর কোনো বিশেষ কৌশল নয়, আধুনিক সিস্টেম ভাষার মূল নকশা-প্রশ্ন। C++ জানলে আপনি ধারণাটি জানেন; অন্য ভাষায় শুধু সিনট্যাক্স আর সীমা আলাদা।

অনুশীলন

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

  1. compile হবে কি? constexpr int twice(int x) { return 2 * x; } থাকলে নিচের চারটি লাইনের কোনগুলো compile হবে? (ক) constexpr int a = twice(21); (খ) int n = 5; constexpr int b = twice(n); (গ) int n = 5; int c = twice(n); (ঘ) const int m = 5; int arr[twice(m)];

    (ক) হবে — ধ্রুবক argument, ফল 42, compile-এর সময়ে। (খ) হবে না — n সাধারণ ভ্যারিয়েবল, তাই constant expression নয়, অথচ b constexpr. (গ) হবে — চলার সময়ের সাধারণ ডাক। (ঘ) হবে — m একটি integer const, যার প্রথম মান ধ্রুবক, তাই পুরোনো নিয়মে এটি constant expression; twice(m) = 10, array-র মাপ বৈধ। তবে পরিষ্কারতার জন্য m-কে constexpr লিখুন।

  2. কোন শব্দ বসবে? প্রতিটির জন্য constexpr, consteval, constinit বা const বেছে নিন: (ক) একটি global গণক, যা ০ থেকে শুরু হয়ে প্রতিটি অনুরোধে বাড়ে (খ) একটি ফরম্যাট-স্ট্রিং যাচাইয়ের ফাংশন, যা কখনো চলার সময় চলা উচিত নয় (গ) প্রোগ্রাম চালুর সময় পড়া সেটিং ফাইলের মান, যা পরে বদলাবে না (ঘ) এক কিলোমিটারে কত মিটার।

    (ক) constinit — শুরুর মান ০ compile-এর সময়ে নিশ্চিত (ক্রমের সমস্যা নেই), কিন্তু পরে বাড়ানো যাবে। (একাধিক thread থেকে বাড়ালে std::atomic লাগবে — পর্ব ৪৬।) (খ) consteval — প্রতিটি ডাক compile-এর সময়ে হবে, নইলে error. (গ) const — মান চলার সময়ে আসে, তাই constexpr অসম্ভব; কিন্তু “বদলাবে না” প্রতিশ্রুতি দেওয়া যায়। (ঘ) constexpr int metres_per_km = 1000;.

  3. বাগ শিকার: একটি AI লিখেছে — #define PERCENT(x) x / 100.0 আর পরে double cut = PERCENT(a + b);. সে বলছে “macro compile-time, তাই দ্রুত।” কী ভুল হবে, আর constexpr দিয়ে কীভাবে লিখবেন?

    লেখা-বদলের পর হয় a + b / 100.0 — ভাগ আগে, যোগ পরে, তাই ফল ভুল। বন্ধনী দিলেও macro-র টাইপ নেই, scope নেই। তাছাড়া macro নিজে কিছু “compile-time” হিসাব করে না — শুধু লেখা বসায়; a + b চলার সময়ের মান হলে হিসাবও চলার সময়ে। সমাধান: constexpr double percent(double x) { return x / 100.0; } — argument একবারই হিসাব হয়, টাইপ পরীক্ষা হয়, আর ধ্রুবক দিলে compile-এর সময়েও চলে।

  4. নিজে লিখুন (কোড): উপরের prime_table.cpp-এ N বদলে 100 করুন, আর একটি static_assert যোগ করুন যা বলে 97 মৌলিক আর 91 মৌলিক নয়। আগে অনুমান করুন — আউটপুটে count কত আসবে? তারপর ইচ্ছা করে লিখুন static_assert(prime[91]); — কী ঘটে?

    যোগ করুন: static_assert(prime[97] && !prime[91]);. ১০০-এর নিচে মৌলিক সংখ্যা ২৫টি, তাই শেষ লাইন count = 25. ৯১ = ৭ × ১৩, তাই static_assert(prime[91]); লিখলে বিল্ড থামে: error: static assertion failed. লক্ষ করুন — ভুল অনুমানটি ধরা পড়ল প্রোগ্রাম একবারও না চালিয়ে।

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

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

ক্রমC++-এর compile-time ক্ষমতাগুলো যে standard-এ প্রথম এসেছে, সেই ক্রমে সাজান — পুরোনো থেকে নতুন (▲▼ চেপে সরান)

  1. constexpr ফাংশন, দেহে কার্যত একটি return (C++11)
  2. constexpr ফাংশনে লুপ আর local ভ্যারিয়েবল বদলানো (C++14)
  3. if constexpr আর constexpr lambda (C++17)
  4. consteval, constinit আর constexpr-এ std::vector (C++20)
  5. if consteval (C++23)

C++-এ const int a = f(); আর constexpr int b = f(); — এদের মূল পার্থক্য কী?

  • a পরে বদলানো যায়, b যায় না
  • দুটিই compile-এর সময় হিসাব হওয়া বাধ্যতামূলক
  • a-র মান চলার সময়েও আসতে পারে; b-র মান compile-এর সময়ে জানা থাকতেই হবে
  • কোনো পার্থক্য নেই, শুধু লেখার ধরন

C++-এ constexpr int sq(int x) { return x * x; } — int n; std::cin >> n; int r = sq(n); লিখলে কী হয়?

  • Compile error, কারণ n ধ্রুবক নয়
  • ঠিকঠাক চলে; sq চলার সময় সাধারণ ফাংশনের মতো চলে
  • compiler অনুমান করে n = 0 ধরে হিসাব করে
  • Undefined behavior

C++20-এ consteval দিয়ে ঘোষিত ফাংশনের বৈশিষ্ট্য কোনটি?

  • compile-এর সময় বা চলার সময় — যেকোনোটিতে চলতে পারে
  • একটি global ভ্যারিয়েবলের শুরুর মান compile-এর সময়ে নিশ্চিত করে
  • শুধু চলার সময় চলে, ফাইল পড়তে পারে
  • প্রতিটি ডাক compile-এর সময়ে হিসাব হতে হবে, নইলে compile error

C++20-এর constinit সম্পর্কে কোন কথাটি সঠিক?

  • static ভ্যারিয়েবলের শুরুর মান compile-এর সময়ে নিশ্চিত করে, কিন্তু পরে মান বদলানো যায়
  • ভ্যারিয়েবলটিকে constexpr-এর মতো চিরকাল অপরিবর্তনীয় করে
  • ফাংশনের সামনে বসে তাকে compile-time করে
  • শুরুর মান চলার সময় পরীক্ষা করে, ভুল হলে exception ছোড়ে

C++-এ একটি function template-এর ভেতরে if constexpr (std::is_integral_v<T>) { ... } else { ... } লেখা হলো। T = double দিয়ে instantiate করলে কী ঘটে?

  • দুটি শাখাই compile হয়, চলার সময় else চলে
  • Compile error, কারণ শর্ত মিথ্যা
  • প্রথম শাখা বাদ পড়ে, শুধু else শাখা compile হয়ে থাকে
  • প্রতিবার ডাকার সময় T পরীক্ষা করা হয়

C++-এ constexpr int big = 50000 * 50000; (int ৩২-বিট) compile করলে কী হয়?

  • big-এ মোড়ানো (wrap) একটি ঋণাত্মক মান বসে
  • Compile error — overflow constant expression-এ চলে না
  • compiler নিজে থেকে long long ব্যবহার করে, big = 2500000000
  • প্রোগ্রাম চলার সময় crash করে

C++20-এ constexpr ফাংশনের ভেতরে std::vector ব্যবহার নিয়ে কোন কথাটি সঠিক?

  • একেবারেই ব্যবহার করা যায় না, কারণ vector heap ব্যবহার করে
  • constexpr std::vector<int> v{1,2,3}; লিখে টেবিল রাখা যায়
  • ব্যবহার করা যায়, আর মেমরি চলার সময়ের heap-এ চলে যায়
  • ব্যবহার করা যায়, তবে নেওয়া মেমরি constant evaluation শেষের আগেই মুছে যেতে হবে

শব্দকোষ

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

মনে রাখুন

  1. যে উত্তর কখনো বদলায় না, তা compile-এর সময়ে হিসাব করুন — কাজ হয় একবার, আর ভুল ধরা পড়ে চালানোর আগেই।
  2. const = বদলাবে না; constexpr = compile-এর সময়ে জানা। ধ্রুবকের জন্য #define নয়, নাম দেওয়া constexpr.
  3. constexpr ফাংশন দুই জায়গায় চলে; সিদ্ধান্ত নেয় ডাকার জায়গা। শুধু compile-time চাইলে consteval; global-এর শুরুর মান নিশ্চিত করতে constinit.
  4. static_assert অনুমান যাচাই করে (struct-এর মাপ, টেবিলের মান); if constexpr template-এ টাইপ অনুযায়ী শাখা ছাঁটে।
  5. constant evaluation-এ UB নিষিদ্ধ — overflow compile error হয়; constexpr টেবিল std::array-এ রাখুন, আর বিল্ড-সময়ের দাম মনে রাখুন।

পরের পর্বে: এই পর্বে static_assert(std::is_integral_v<T>) দিয়ে template-কে ভুল টাইপ থেকে পাহারা দিয়েছেন। C++20-এর concepts এই কাজটিকে template-এর ঘোষণাতেই নিয়ে আসে — template-এর শর্ত একটি নাম পায়, আর error message হয় এক লাইনের।

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

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