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

আউটপুট কী হবে? — C++ কোড পড়ার অনুশীলন

What Will This Print? — C++ Output-Prediction Practice

Taught in Bangla, this practice page has 47 "what will this print?" C++ questions, from types and operators through references, constructor and destructor order, copy elision, virtual calls, STL, lambdas, ranges, move semantics, exceptions and threads. Every option is explained, with a method for running code in your head.

এই পর্বে যা পাবেন

  • কাগজে-কলমে C++ কোড চালানোর (dry run) পদ্ধতি — ভ্যারিয়েবলের টেবিল, call stack আর object-এর lifetime একসাথে
  • কোন constructor, কোন destructor, কোন overload আর কোন virtual ফাংশন চলবে — তা ঠিক করার নিয়মগুলো এক জায়গায়
  • মডিউল ১ থেকে ৭ পর্যন্ত ৪৭টি প্রশ্ন — সহজ থেকে কঠিন, প্রতিটির পূর্ণ ব্যাখ্যা আর কোন পর্ব ঝালাবেন তার লিংক
  • compile error, undefined behavior আর unspecified — তিনটি “অদ্ভুত” উত্তর আলাদা করে চেনা ও কারণসহ লেখা
  • AI-কে দিয়ে নতুন প্রশ্ন বানানো, আর তার উত্তর compiler দিয়ে যাচাইয়ের নিয়ম

১ · মডিউল ৮ শুরু — পরীক্ষা ও ইন্টারভিউর প্রস্তুতি

অভিনন্দন! ৫১টি পর্বে আপনি C++-এর টাইপ আর অপারেটর থেকে শুরু করে class, template, STL, move semantics, concurrency আর একটি পূর্ণ ক্যাপস্টোন প্রজেক্ট পর্যন্ত এসেছেন। এবার শেখা জিনিস এমনভাবে ঝালিয়ে নেওয়ার পালা, যাতে পরীক্ষার হলে বা ইন্টারভিউর টেবিলে চাপের মধ্যেও ঠিক উত্তর মাথায় আসে।

এই শেষ মডিউলে নতুন কোনো বিষয় নেই। আছে চারটি অনুশীলনের পর্ব, প্রতিটি আলাদা ধরনের প্রশ্নের জন্য:

পর্ব ৫২ · আউটপুট কী হবে?

এই পর্ব। ছোট কোড দেখে বলতে হবে কী ছাপা হবে — বা আদৌ compile হবে কি না। লিখিত পরীক্ষা, MCQ আর টেকনিক্যাল ইন্টারভিউ, তিন জায়গাতেই আসে।

কৌশল: trace table

পর্ব ৫৩ · MCQ প্রশ্নব্যাংক

ধারণা, সংজ্ঞা আর তথ্যের দ্রুত প্রশ্ন — keyword, STL-এর জটিলতা, Rule of 5, smart pointer. চাকরির প্রাথমিক বাছাই পরীক্ষায় এ ধরনের প্রশ্ন প্রায়ই আসে।

কৌশল: সময় ধরে চর্চা

পর্ব ৫৪ · লিখিত ও ভাইভা

“পার্থক্য লিখুন”, “উদাহরণসহ ব্যাখ্যা করুন”, “class লিখুন” আর মুখোমুখি প্রশ্নের উত্তর কীভাবে সাজাবেন।

কৌশল: উত্তরের কাঠামো

পর্ব ৫৫ · কোডিং ইন্টারভিউ

সমস্যা শুনে প্রশ্ন করা, পরিকল্পনা বলা, C++-এ কোড লেখা, টেস্ট করা — সফটওয়্যার কোম্পানির ইন্টারভিউর পুরো ধারা।

কৌশল: জোরে ভাবা

“আউটপুট কী হবে?” প্রশ্ন এত বেশি আসে কেন

প্রশ্নকর্তা আসলে জানতে চান: আপনি কি compiler আর CPU-র মতো করে কোড পড়তে পারেন? C++-এ এই দক্ষতা অন্য ভাষার চেয়ে বেশি দরকার, কারণ এখানে অনেক কাজ চোখের আড়ালে ঘটে — কোনো লাইনে না লিখলেও constructor চলে, scope শেষ হলে destructor চলে, compiler নিজে overload বেছে নেয়, আর কখনো কপি হওয়ার কথা থাকলেও কপি হয় না। যিনি এই অদৃশ্য কাজগুলো দেখতে পান, তিনিই বাগ খুঁজে পান, AI-এর লেখা কোড যাচাই করতে পারেন, আর code review-তে কাজের মন্তব্য দিতে পারেন।

আপনার লক্ষ্যশুরু করুনতারপর
বিশ্ববিদ্যালয়ের লিখিত পরীক্ষাপর্ব ৫২ (আউটপুট) ও ৫৪ (লিখিত)পর্ব ৫৩ দিয়ে দ্রুত ঝালাই
সরকারি বা ব্যাংকের IT/প্রোগ্রামার পদের পরীক্ষাপর্ব ৫৩ (MCQ) ও ৫২পর্ব ৫৪-এর লিখিত ও ভাইভা অংশ
সফটওয়্যার কোম্পানির ইন্টারভিউপর্ব ৫৫ (কোডিং)পর্ব ৫২ — অনেক টেকনিক্যাল রাউন্ড “এই কোড কী ছাপবে?” দিয়েই শুরু হয়
শুধু নিজের শেখা যাচাইএই পর্ব, প্রথম থেকে শেষযে ভাগে বেশি ভুল, সেই পর্বে ফিরে যান

এই পাতা কীভাবে ব্যবহার করবেন

  1. প্রথম দফা — অপশন ঢেকে রাখুন। প্রতিটি কোড পড়ে খাতায় নিজের উত্তর লিখুন, তারপর অপশনে চাপুন। প্রথম চেষ্টায় কতগুলো ঠিক হলো, অনুশীলনের শেষে স্কোর দেখাবে।
  2. দ্বিতীয় দফা — ভুলগুলোর কারণ খুঁজুন। প্রতিটি ভুল অপশনের ব্যাখ্যা পড়ুন। প্রতিটি ভাগের শুরুতে লেখা আছে কোন পর্বগুলো ঝালাতে হবে।
  3. তৃতীয় দফা — নিজে চালান। প্রতিটি ভাগের “নিজে চালিয়ে দেখুন” কোড চালান। একটি লাইন বদলে আগে ফল অনুমান করুন, তারপর চালিয়ে মেলান।
এই পাতার ধরে নেওয়া পরিবেশ

সব উত্তর 64-বিট Linux-এ GCC ও তার standard library (libstdc++), C++23 মোডের জন্য: int 4 বাইট, long ও pointer 8 বাইট। কোডের টুকরোতে #include লেখা না থাকলে ধরে নিন দরকারি header আছে (<print>, <iostream>, <vector> ইত্যাদি), আর main না থাকলে লাইনগুলো main()-এর ভেতরে। std::print ও std::println C++23 থেকে আছে; পুরোনো compiler-এ এগুলোর জায়গায় std::cout ভাবুন। যে উত্তর লাইব্রেরি বা প্ল্যাটফর্মের ওপর নির্ভর করে, প্রশ্নেই তা বলা আছে।

C জানলে

C-এর “আউটপুট কী হবে?” প্রশ্নের প্রায় সব ফাঁদ (integer division, promotion, precedence, signed/unsigned, UB) C++-এও আছে। নতুন যা যোগ হয়: অদৃশ্য constructor/destructor ডাক, overload আর template-এর মধ্যে বাছাই, virtual dispatch, copy বনাম move, আর STL-এর আচরণ। আবার কিছু নিয়ম বদলেছে — যেমন C++17 থেকে i = i++ + 1 আর UB নয় (৮ নম্বর ভাগে দেখবেন)। C-এর দিকটা ঝালাতে C কোর্সের আউটপুট কী হবে? (C) পর্ব দেখুন।

পরীক্ষা ও ইন্টারভিউতে এ ধরনের প্রশ্ন সাধারণত এই চেহারায় আসে:

  • “নিচের প্রোগ্রামের আউটপুট লিখুন” — খাতায় trace table এঁকে দেখান; শুধু উত্তর নয়, ধাপগুলোও নম্বর পায়।
  • “কোন constructor আর destructor কোন ক্রমে চলবে?” — প্রতিটি object-এর জন্ম আর মৃত্যুর লাইন আলাদা করে চিহ্নিত করুন।
  • MCQ-তে চারটি আউটপুট থেকে বেছে নেওয়া — আগে নিজে হিসাব করুন, তারপর অপশন দেখুন।
  • ইন্টারভিউতে “এই কোডে কী সমস্যা?” — dangling reference, slicing বা virtual destructor-এর অভাব আগে বলুন; এটাই অভিজ্ঞতার লক্ষণ।

২ · C++ কোড মাথায় চালানোর পদ্ধতি

আউটপুট বলার একটাই নির্ভরযোগ্য উপায়: কম্পিউটার যেভাবে চালায়, ঠিক সেভাবে এক লাইন এক লাইন করে চালানো, আর প্রতিটি পরিবর্তন লিখে রাখা। একে বলে dry run. C-তে একটি ভ্যারিয়েবলের টেবিলই যথেষ্ট ছিল। C++-এ তার সাথে আরও দুটি জিনিস রাখতে হয়: কোন ফাংশন কাকে ডেকেছে (call stack), আর কোন object এখন বেঁচে আছে।

ঢাকার মেট্রো রেলের একটি স্টেশনের কথা ভাবুন। কে কখন ঢুকল (কার্ড পাঞ্চ), কে কখন বের হলো — গেটের খাতায় সব লেখা থাকে। শেষ ট্রেনের পরে স্টেশন বন্ধ হলে যাঁরা ভেতরে আছেন, তাঁরা উল্টো ক্রমে বের হন — শেষে যিনি এসেছেন, তিনি দরজার সবচেয়ে কাছে। C++-এর object-ও তাই: constructor চলা মানে গেট দিয়ে ঢোকা, destructor চলা মানে বের হওয়া, আর একটি scope-এর object-রা বের হয় ঢোকার উল্টো ক্রমে। Trace table হলো সেই গেটের খাতা।

C++-এর trace table-এর ছয়টি নিয়ম

  1. প্রতিটি ভ্যারিয়েবল ও object-এর জন্য একটি কলাম, আর একটি কলাম আউটপুটের জন্য। static local, global আর ফাংশনের প্যারামিটারও object — তাদের কলাম ভুলবেন না।
  2. প্রতিটি object-এর জন্ম ও মৃত্যুর লাইন চিহ্নিত করুন। জন্ম: সংজ্ঞার লাইনে (member হলে ঘোষণার ক্রমে, base আগে)। মৃত্যু: যে }-এ scope শেষ, সেখানে — জন্মের উল্টো ক্রমে। Temporary মরে পুরো statement শেষ হলে।
  3. প্রতিটি initialization-এ জিজ্ঞেস করুন: কোন constructor? নতুন মান থেকে → সাধারণ constructor; একটি lvalue থেকে → copy; std::move করা বা মরতে যাওয়া object থেকে → move; একই টাইপের prvalue থেকে → কোনোটিই নয় (guaranteed copy elision).
  4. প্রতিটি ফাংশন ডাকে জিজ্ঞেস করুন: কোন ফাংশন? একাধিক overload থাকলে overload resolution (হুবহু মিল > promotion > conversion); virtual হলে object-এর আসল টাইপ (dynamic dispatch), না হলে pointer/reference-এর টাইপ।
  5. ফাংশন ডাকলে call stack-এ নতুন একটি ঘর — নিজস্ব প্যারামিটার আর local নিয়ে। ফেরার সময় সেই ঘরের সব object-এর destructor চলে।
  6. আউটপুট কলামে হুবহু লিখুন — space আর newline সহ। মনে রাখুন std::println("{}", 3.0) ছাপে 3, 3.0 নয়।

নিচের ডায়াগ্রামে একটি পূর্ণ প্রোগ্রাম এই পদ্ধতিতে চালানো হয়েছে। এখানে একসাথে চারটি ফাঁদ: ফাংশন থেকে ফেরত দেওয়া object, by-value প্যারামিটার, temporary আর destructor-এর ক্রম। “পরে” চাপার আগে নিজে পুরো আউটপুট লিখে নিন — মোট কতবার C, K আর D ছাপা হবে?

1struct T {2  int id;3  T(int i) : id(i) { print("C{} ", id); }4  T(const T& o) : id(o.id + 10) { print("K{} ", id); }5  ~T() { print("D{} ", id); }6};7T make(int i) { return T{i}; }8void use(T t) { print("U{} ", t.id); }9int main() {10  T a{1};11  T b = make(2);12  use(a);13  use(T{3});14}
Trace table
a.id
b.id
use()-এর t.id
call stack
  1. লাইন ১০ — a জন্মাল। T a{1} সাধারণ constructor ডাকে, ছাপে C1. খাতায় a-এর কলামে জন্মের লাইন লিখুন: ১০.
  2. লাইন ১১ — b, কিন্তু কোনো কপি নেই। make(2)-এর ভেতরে return T{2} একটি prvalue. C++17 থেকে নিয়ম: একই টাইপের prvalue দিয়ে object initialize করলে সেটি সরাসরি গন্তব্যেই তৈরি হয় — temporary, copy বা move কিছুই হয় না। তাই শুধু C2, আর এটি b নিজেই।
  3. লাইন ১২ — by-value প্যারামিটার। use(a)-এ t একটি নতুন object, তৈরি হয় a (একটি lvalue) থেকে — তাই copy constructor: id = 1 + 10 = 11, ছাপে K11. call stack-এ এখন main → use.
  4. লাইন ৮ — ফাংশনের শরীর। ছাপে U11. লক্ষ করুন, t আর a আলাদা object — t-কে বদলালে a বদলাত না।
  5. t মরল। ফাংশন শেষ, প্যারামিটার t-এর destructor: D11. (প্যারামিটার ঠিক ফাংশন ফেরার মুহূর্তে মরে নাকি পুরো statement শেষে, তা implementation-defined; কিন্তু দুই ক্ষেত্রেই পরের লাইনের আগে — তাই আউটপুট একই।)
  6. লাইন ১৩ — temporary দিয়ে ডাক। T{3} একটি prvalue, আর প্যারামিটার t একই টাইপের। আবার guaranteed elision: t সরাসরি তৈরি হয়, ছাপে শুধু C3 — কোনো K নেই।
  7. শরীর: U3.
  8. t মরল: D3. এখানে একটিই object ছিল, তাই একটিই D.
  9. লাইন ১৪ — main-এর scope শেষ। local object মরে জন্মের উল্টো ক্রমে: আগে b — D2.
  10. তারপর a — D1. পুরো আউটপুট: C1 C2 K11 U11 D11 C3 U3 D3 D2 D1. গুনে দেখুন: ৪টি object জন্মেছে (C1, C2, K11, C3), ৪টি মরেছে — হিসাব মিলছে। এই “জন্ম = মৃত্যু” মিলিয়ে দেখা প্রতিটি প্রশ্নে কাজে লাগে।

নিচের কোডটি হুবহু ডায়াগ্রামের প্রোগ্রাম (ছাপার পরে একটি newline দেওয়ার জন্য object-গুলোকে একটি ভেতরের block-এ রাখা হয়েছে)। চালানোর আগে use(T t)-কে use(const T& t) করলে আউটপুট কী হবে, অনুমান করুন — তারপর বদলে চালান।

#include <print>

struct T {
    int id;
    T(int i) : id(i) { std::print("C{} ", id); }
    T(const T& o) : id(o.id + 10) { std::print("K{} ", id); }
    ~T() { std::print("D{} ", id); }
};

T make(int i) { return T{i}; }
void use(T t) { std::print("U{} ", t.id); }

int main() {
    {
        T a{1};
        T b = make(2);
        use(a);
        use(T{3});
    }
    std::println();
}

আউটপুট: C1 C2 K11 U11 D11 C3 U3 D3 D2 D1. প্যারামিটার const T& করলে কোনো নতুন object তৈরি হয় না: use(a) ছাপে শুধু U1, আর use(T{3})-এ temporary-টি reference-এ বাঁধা পড়ে, statement শেষে মরে — C3 U3 D3. মোট আউটপুট হয় C1 C2 U1 C3 U3 D3 D2 D1. ঝালিয়ে নিন: পর্ব ২১ · constructor ও destructor, পর্ব ২২ · copy, move আর Rule of 0/3/5.

চার রকম উত্তর হতে পারে

পরীক্ষার্থীরা ধরে নেন প্রতিটি কোড একটি নির্দিষ্ট লেখা ছাপবে। বাস্তবে উত্তর চার রকম, আর ভালো প্রশ্নকর্তা ঠিক এটাই যাচাই করেন:

✅ নির্দিষ্ট আউটপুট

C++ standard ফল পুরোপুরি ঠিক করে দেয়। যেকোনো সঠিক compiler-এ একই লেখা আসবে। বেশিরভাগ প্রশ্ন এই ধরনের।

যেমন: destructor-এর উল্টো ক্রম

⛔ Compile error

প্রোগ্রাম তৈরিই হয় না, তাই কিছুই ছাপা হয় না। কোন error, সেটাও বলুন: ambiguous overload, deleted copy constructor, narrowing, template deduction-এর অমিল।

যেমন: unique_ptr কপি

⚠️ Undefined behavior

নিয়ম ভাঙা কোড — standard কোনো ফলের নিশ্চয়তা দেয় না। আজ ঠিক ছাপল, কাল crash. UB চেনাটাই সঠিক উত্তর; এর আউটপুট “চালিয়ে দেখে” বলা যায় না।

যেমন: dangling reference

🔀 Unspecified / implementation-defined

Unspecified: কয়েকটি বৈধ ফলের যেকোনোটি, compiler-কে জানাতে হয় না (যেমন ফাংশনের আর্গুমেন্টের মূল্যায়নের ক্রম)। Implementation-defined: compiler নিজের নিয়ম লিখে রাখে (যেমন sizeof(long)).

যেমন: unordered_map-এর ক্রম

ক্রমনিচের কোডে struct A, B, C-এর constructor বড় হাতের আর destructor ছোট হাতের অক্ষর ছাপে। struct D : A { B b; C c; }; — { D d; } চালালে ঘটনাগুলো কোন ক্রমে ঘটবে? (▲▼ চেপে সাজান)

  1. base A-এর constructor — A
  2. member b-এর constructor — B (ঘোষণার ক্রমে প্রথম member)
  3. member c-এর constructor — C
  4. D-এর নিজের শরীর শেষ; scope শেষে member c-এর destructor — c
  5. member b-এর destructor — b
  6. base A-এর destructor — a

৩ · ভিত্তি — টাইপ, অপারেটর আর ইনপুট-আউটপুট

এই ভাগের কোড ছোট, কিন্তু ফাঁদ গভীর। প্রতিটি এক্সপ্রেশনে তিনটি প্রশ্ন করুন: হিসাব কোন টাইপে হচ্ছে (implicit conversion)? কোন অপারেটর আগে (precedence)? আর auto আসলে কোন টাইপ ধরল (type deduction)? মনে রাখুন, বাঁ পাশের টাইপ ডান পাশের হিসাব বদলায় না — double d = 7 / 2;-এ ভাগটা আগে int-এ হয়ে যায়।

(সহজ) double-এ রাখলেই কি দশমিক আসে?

int a = 7, b = 2;
double d = a / b;
std::println("{} {}", d, a / 2.0);
  • 3.5 3.5
  • 3 3.5
  • 3.0 3.5
  • 3 3

(মাঝারি) auto কোন টাইপ ধরল — কী ছাপা হবে?

auto x = 5 / 2;
auto y = 5 / 2.0f;
auto z = 'a' + 1;
std::println("{} {} {}", x, y, z);
  • 2.5 2.5 b
  • 2 2.5 b
  • 2 2.5 98
  • 2 2 98

(মাঝারি) cout-এর << আর shift-এর << — কী ছাপা হবে?

std::cout << 1 << 2 + 3 << ' '
          << (1 << 2) + 3 << '\n';
  • 15 7
  • 7 7
  • 1 5 7
  • 15 32

(কঠিন) Signed আর unsigned তুলনা — কী ছাপা হবে?

int i = -1;
unsigned u = 1;
std::cout << (i < u) << '\n';
  • 1
  • 0
  • false
  • Undefined behavior

(মাঝারি) std::format-এর width আর alignment — ঠিক কী দেখা যাবে?

std::println("[{:>5}] [{:<4}] [{:05.1f}]",
             42, "ab", 3.14159);
  • [42] [ab] [3.1]
  • [42 ] [ ab] [003.1]
  • [ 42] [ab ] [003.1]
  • [ 42] [ab ] [ 3.14]

(মাঝারি) Brace initialization-এ দশমিক সংখ্যা

int x{3.5};
std::println("{}", x);
  • 3
  • 4
  • Compile error
  • Undefined behavior

নিজে চালিয়ে দেখুন — টাইপ আর অপারেটরের ছোট ছোট পরীক্ষা

ওপরের কয়েকটি প্রশ্ন আর দুটি নতুন, একটি প্রোগ্রামে। চালানোর আগে প্রতিটি লাইনের ফল খাতায় লিখুন, তারপর মিলিয়ে দেখুন।

#include <iostream>
#include <print>

int main() {
    int a = 7, b = 2;
    double d = a / b;
    std::println("1) {} {} {}", d, a / 2.0, a % b);
    std::cout << "2) " << 'A' + 1 << ' ' << char('A' + 1) << '\n';

    auto x = 5 / 2;
    auto y = 5 / 2.0f;
    auto z = 'a' + 1;
    std::println("3) {} {} {}", x, y, z);

    std::println("4) [{:>5}] [{:<4}] [{:05.1f}]", 42, "ab", 3.14159);
    std::cout << "5) " << true << ' ' << std::boolalpha << true << '\n';
    std::println("6) {} {}", 10 / 3 * 3, -7 / 2);
}

১) 3 3.5 1 — int ভাগে দশমিক কাটা যায়, আর {} একটি 3.0-কে 3 দেখায়। ২) 66 B — 'A' + 1 int (65 + 1), কিন্তু char(...) করলে আবার অক্ষর। ৩) 2 2.5 98. ৪) width আর alignment-এর হুবহু রূপ। ৫) 1 true — std::boolalpha একটি manipulator; এটি দেওয়ার পর থেকে stream-এ bool লেখা হয় true/false হিসেবে, আগের লেখা বদলায় না। ৬) 9 -3 — বাঁ থেকে ডানে: 10 / 3 = 3, তারপর × 3 = 9; আর ঋণাত্মক ভাগ শূন্যের দিকে ছাঁটা হয় (C++11 থেকে নিয়মে নির্দিষ্ট)।

৪ · নিয়ন্ত্রণ, ফাংশন, overloading আর reference

এই ভাগে দুটি প্রশ্ন সবসময় মাথায় রাখুন। এক: ভ্যারিয়েবলটি কি কপি, নাকি আসলটির আরেক নাম (reference)? range-for-এ auto x কপি, auto& x আসলটি। দুই: একই নামের একাধিক ফাংশন থাকলে compiler কোনটি বাছবে? Overload resolution-এর মোটা দাগের ক্রম: হুবহু মিল, তারপর promotion (char/bool/short থেকে int, float থেকে double), তারপর সাধারণ conversion. একই স্তরে দুটি প্রার্থী সমান ভালো হলে — compile error.

(সহজ) range-for-এ কপি না reference?

std::vector<int> v{1, 2, 3};
for (auto x : v) x *= 2;
for (auto& y : v) y += 1;
for (int z : v) std::print("{} ", z);
  • 3 5 7
  • 2 3 4
  • 1 2 3
  • 4 6 8

(মাঝারি) কোন overload চলবে?

void f(int)    { std::print("int "); }
void f(double) { std::print("double "); }

f('a');  f(2.5f);  f(true);
  • int double int
  • int int int
  • int double double
  • Compile error: ambiguous

(কঠিন) একই দুটি overload, এবার long দিয়ে ডাক

void f(int)    { std::print("int "); }
void f(double) { std::print("double "); }

f(5L);
  • int
  • double
  • Compile error: no matching function
  • Compile error: ambiguous

(মাঝারি) Reference-কে কি অন্য ভ্যারিয়েবলে সরানো যায়?

int a = 1;
int& r = a;
int b = 5;
r = b;
b = 9;
std::println("{} {} {}", a, r, b);
  • 1 5 9
  • 1 9 9
  • 5 5 9
  • 9 9 9

(কঠিন) static counter, তিনবার এক লাইনে

int counter() { static int c = 0; return ++c; }

std::println("{} {} {}", counter(), counter(), counter());
  • 1 2 3
  • 3 2 1
  • Undefined behavior
  • Unspecified — ক্রম নির্দিষ্ট নয়

(মাঝারি) Structured binding — কপি নাকি আসলটি?

std::pair p{1, 2};
auto [x, y] = p;
x = 10;
auto& [u, v] = p;
v = 20;
std::println("{} {} {}", p.first, p.second, x);
  • 10 20 10
  • 1 20 10
  • 1 2 10
  • 1 20 1

নিজে চালিয়ে দেখুন — কপি, reference আর overload

#include <print>
#include <utility>
#include <vector>

void f(int)    { std::print("int "); }
void f(double) { std::print("double "); }

int add(int a, int b = 10, int c = 100) { return a + b + c; }
void twice(int& n) { n *= 2; }

enum class Color { Red, Green = 5, Blue };

int main() {
    std::vector<int> v{1, 2, 3};
    for (auto& y : v) y += 1;
    std::println("1) {} {} {}", v[0], v[1], v[2]);

    std::print("2) "); f('a'); f(2.5f); f(true); std::println();
    std::println("3) {} {} {}", add(1), add(1, 2), add(1, 2, 3));

    int n = 3; twice(n); twice(n);
    std::println("4) {} {}", n, static_cast<int>(Color::Blue));

    std::pair p{1, 2};
    auto& [u, w] = p;
    w = 20;
    std::println("5) {} {} {}", p.first, p.second, u);
}

১) 2 3 4. ২) int double int — তিনটিই promotion. ৩) 111 103 6 — default argument ডান দিক থেকে ভরে: add(1) = 1 + 10 + 100. ৪) 12 6 — reference দিয়ে দুবার দ্বিগুণ; আর enum class-এ Green = 5 হলে পরেরটি 6. তবে int x = Color::Green; লিখলে compile error, কারণ enum class নিজে থেকে int-এ রূপ নেয় না। ৫) 1 20 1. পরীক্ষা: f(5L); লাইন যোগ করে compile করুন — ambiguous error-টি নিজের চোখে দেখুন।

৫ · মেমরি — vector, string, pointer আর মালিকানা

এই ভাগে trace table-এ একটি নতুন কলাম যোগ করুন: “কে কার মালিক, আর মালিক কতক্ষণ বাঁচে?” string_view, reference আর raw pointer কারও মালিক নয় — তারা শুধু অন্যের ডেটা দেখে। যার দিকে দেখছে সে মরে গেলে, বা vector বড় হতে গিয়ে নতুন জায়গায় সরে গেলে, দেখার জিনিসটি আর নেই (dangling). এমন কোডের আউটপুট বলা যায় না — উত্তর UB.

(সহজ) গোল বন্ধনী আর বাঁকা বন্ধনী — vector-এ কতটি element?

std::vector<int> a(3, 7), b{3, 7};
std::println("{} {} {}", a.size(), b.size(), a[2]);
  • 3 3 7
  • 2 2 7
  • 3 2 7
  • 3 2 3

(মাঝারি) String literal-এ + 1

std::string t = "abc" + 1;
std::string u = std::string("abc") + "1";
std::println("{} {}", t, u);
  • abc1 abc1
  • bc abc1
  • abcb abc1
  • Compile error

(মাঝারি) দুটি string literal জোড়া

std::string s = "Dhaka" + " city";
std::println("{}", s);
  • Dhaka city
  • Dhaka
  • Undefined behavior
  • Compile error

(মাঝারি) unique_ptr move করার পরে

auto p = std::make_unique<int>(5);
auto q = std::move(p);
std::println("{} {}", p == nullptr, *q);
  • true 5
  • 1 5
  • false 5
  • Compile error

(মাঝারি) shared_ptr-এর use_count

auto a = std::make_shared<int>(1);
auto b = a;
{
    auto c = a;
    std::print("{} ", a.use_count());
}
std::print("{} ", a.use_count());
b.reset();
std::println("{}", a.use_count());
  • 2 1 0
  • 3 2 1
  • 3 3 2
  • 3 2 0

(কঠিন) local ভ্যারিয়েবলের reference ফেরত — C++23-এ কী হয়?

int& f() {
    int x = 5;
    return x;
}
int main() { std::println("{}", f()); }
  • 5
  • Undefined behavior
  • Compile error
  • 0

(কঠিন) temporary string-এর ওপর string_view

std::string_view sv = std::string("hello") + " world";
std::cout << sv << '\n';
  • hello world
  • hello
  • Compile error
  • Undefined behavior

নিজে চালিয়ে দেখুন — vector, string, pointer আর smart pointer

#include <iterator>
#include <memory>
#include <print>
#include <string>
#include <vector>

int main() {
    std::vector<int> a(3, 7), b{3, 7};
    std::println("1) {} {} {}", a.size(), b.size(), a[2]);

    std::string s = "Dhaka";
    s += '!';
    std::println("2) {} {} {}", s.size(), s.substr(1, 3), s.find('k'));

    int arr[] = {10, 20, 30};
    int* p = arr + 1;
    std::println("3) {} {} {} | {} {} {}", *p, p[1], *(p - 1),
                 sizeof(arr), sizeof(p), std::size(arr));

    auto u = std::make_unique<int>(5);
    auto u2 = std::move(u);
    std::println("4) {} {}", u == nullptr, *u2);

    auto sp = std::make_shared<int>(1);
    auto sp2 = sp;
    std::println("5) {}", sp.use_count());
}

১) 3 2 7. ২) 6 hak 3 — substr(1, 3) মানে index 1 থেকে ৩টি অক্ষর, আর find প্রথম মিলের index দেয় (না পেলে std::string::npos, একটি বিশাল unsigned সংখ্যা)। ৩) 20 30 10 | 12 8 3 — অ্যারের sizeof পুরো আকার (৩ × ৪ বাইট), pointer-এর sizeof 64-বিটে 8, আর std::size element-সংখ্যা। ৪) true 5. ৫) 2. পরীক্ষা: auto u3 = u2; লিখে compile করুন — use of deleted function error দেখবেন, কারণ unique_ptr-এর copy constructor deleted.

৬ · অবজেক্ট-ওরিয়েন্টেড — constructor-এর ক্রম, copy, move আর virtual

OOP-এর আউটপুট প্রশ্নের প্রায় সবগুলোই তিনটি নিয়মে দাঁড়িয়ে। জন্মের ক্রম: base আগে, তারপর member-রা ঘোষণার ক্রমে (initializer list-এ যে ক্রমেই লিখুন), তারপর নিজের শরীর; মৃত্যু ঠিক উল্টো। কপি হবে কি না: lvalue থেকে copy, std::move থেকে move, আর একই টাইপের prvalue থেকে কিছুই না (guaranteed copy elision). কোন ফাংশন: virtual হলে object-এর আসল টাইপ, না হলে reference/pointer-এর টাইপ — আর মান হিসেবে কপি করলে slicing.

(মাঝারি) Base, member আর নিজের শরীর — কোন ক্রমে?

struct A { A() { print("A"); } ~A() { print("a"); } };
struct B { B() { print("B"); } ~B() { print("b"); } };
struct C : A {
    B b;
    C()  { print("C"); }
    ~C() { print("c"); }
};
int main() { C c; }
  • CABcba
  • ABCcba
  • ABCabc
  • BACcab

(কঠিন) Initializer list-এর ক্রম উল্টে লিখলে?

struct M { M(const char* n) { std::print("{}", n); } };
struct S {
    M x{"x"};
    M y{"y"};
    S() : y("Y"), x("X") {}
};
int main() { S s; }
  • XY
  • YX
  • xyXY
  • Compile error

(কঠিন) Copy elision — কতবার কপি বা move?

struct T {
    T()           { print("ctor "); }
    T(const T&)   { print("copy "); }
    T(T&&)        { print("move "); }
    ~T()          { print("dtor "); }
};
T make() { return T{}; }
int main() { T t = make(); }
  • ctor dtor
  • ctor move dtor dtor
  • ctor copy dtor dtor
  • ctor move move dtor dtor dtor

(কঠিন) vector-এ push_back — copy নাকি move? (T ওপরের প্রশ্নের মতো, move constructor-টি noexcept)

std::vector<T> v;
v.reserve(2);
T t;
v.push_back(t);
v.push_back(std::move(t));
// এরপর main শেষ
  • ctor move move dtor dtor dtor
  • ctor copy move move dtor dtor dtor
  • ctor copy move dtor dtor dtor
  • ctor copy copy dtor dtor dtor

(মাঝারি) virtual, non-virtual আর মান হিসেবে কপি

struct Base {
    virtual ~Base() = default;
    virtual void hi() { print("Base "); }
    void bye()        { print("base "); }
};
struct Der : Base {
    void hi() override { print("Der "); }
    void bye()         { print("der "); }
};
Der d;
Base& b = d;   b.hi();  b.bye();
Base s = d;    s.hi();
  • Der der Der
  • Der base Base
  • Base base Base
  • Der base Der

(কঠিন) Constructor-এর ভেতরে virtual ডাক

struct B {
    B() { hello(); }
    virtual ~B() = default;
    virtual void hello() { print("B"); }
};
struct D : B {
    D() { hello(); }
    void hello() override { print("D"); }
};
int main() { D d; }
  • DD
  • BB
  • BD
  • Compile error

(কঠিন) Virtual ফাংশনের default argument

struct B {
    virtual ~B() = default;
    virtual void f(int x = 1) { print("B{} ", x); }
};
struct D : B {
    void f(int x = 2) override { print("D{} ", x); }
};
D d;
B& b = d;
b.f();  d.f();
  • D1 D2
  • D2 D2
  • B1 D2
  • Compile error

(কঠিন) Virtual destructor ছাড়া base pointer দিয়ে delete

struct Base { ~Base() { print("~Base "); } };
struct Der : Base { ~Der() { print("~Der "); } };

Base* p = new Der;
delete p;
  • ~Der ~Base
  • ~Base
  • Compile error
  • Undefined behavior

নিজে চালিয়ে দেখুন — জন্ম, মৃত্যু আর virtual

#include <print>

struct A { A() { std::print("A"); } ~A() { std::print("a"); } };
struct B { B() { std::print("B"); } ~B() { std::print("b"); } };
struct C : A {
    B b;
    C()  { std::print("C"); }
    ~C() { std::print("c"); }
};

struct Base {
    Base() { hello(); }
    virtual ~Base() = default;
    virtual void hello() { std::print("Base "); }
    virtual void f(int x = 1) { std::print("B{} ", x); }
};
struct Der : Base {
    Der() { hello(); }
    void hello() override { std::print("Der "); }
    void f(int x = 2) override { std::print("D{} ", x); }
};

int main() {
    std::print("1) "); { C c; } std::println();
    std::print("2) "); Der d; std::println();
    Base& r = d;
    std::print("3) "); r.f(); d.f(); r.hello(); std::println();
    Base s = d;
    std::print("4) "); s.hello(); std::println();
}

১) ABCcba. ২) Base Der — constructor-এর ভেতরে virtual ডাক যার constructor চলছে তার সংস্করণে যায়। লক্ষ করুন, Base s = d; লাইনে Base-এর copy constructor চলে, সাধারণ constructor নয় — তাই এখানে আর কিছু ছাপা হয় না। ৩) D1 D2 Der. ৪) Base — slicing. চেষ্টা করুন: hello()-কে Base-এ non-virtual করে দিন, তারপর ৩ নম্বরের শেষ শব্দটি কী হয় অনুমান করে চালান।

নিজে চালিয়ে দেখুন — copy, move আর elision

#include <print>
#include <utility>
#include <vector>

struct T {
    T() { std::print("ctor "); }
    T(const T&) { std::print("copy "); }
    T(T&&) noexcept { std::print("move "); }
    T& operator=(const T&) { std::print("assign "); return *this; }
    ~T() { std::print("dtor "); }
};

T make() { return T{}; }                    // prvalue: elision নিশ্চিত
T makeNamed() { T local; return local; }    // NRVO: অনুমোদিত, নিশ্চিত নয়

int main() {
    std::print("1) "); { T t = make(); } std::println();
    std::print("2) "); { T t = makeNamed(); } std::println();
    std::print("3) "); { T a; T b = a; T c; c = a; } std::println();
    std::print("4) ");
    {
        std::vector<T> v;
        v.reserve(2);
        T t;
        v.push_back(t);
        v.push_back(std::move(t));
    }
    std::println();
}

১) ctor dtor — guaranteed elision. ২) ctor dtor — GCC এখানে NRVO করে, কিন্তু এটি নিশ্চিত নয়; না করলে ctor move dtor dtor হতো (named local ফেরত দিলে copy-র বদলে আগে move চেষ্টা হয়)। ৩) ctor copy ctor assign dtor dtor dtor — T b = a; একটি initialization (copy constructor), আর c = a; আগে থেকে থাকা object-এ assignment (copy assignment). = চিহ্ন দেখে বিভ্রান্ত হবেন না। ৪) ctor copy move dtor dtor dtor. পরীক্ষা: reserve(2) মুছে দিন। জায়গা ফুরালে vector নতুন মেমরি নেয় আর পুরোনো element-গুলো সেখানে সরায় — তখন বাড়তি move ও dtor দেখা যায় (move constructor noexcept বলেই copy-র বদলে move). কিন্তু প্রথমবার কত ঘরের জায়গা নেওয়া হবে, তা standard বলে না, library ঠিক করে। এই পাতার GCC-র libstdc++ প্রথম push_back-এই কয়েকটি element-এর জায়গা নিয়ে রাখে, তাই এখানে আউটপুট বদলাবে না — আর ঠিক এ কারণেই “capacity কত?” ধরনের প্রশ্নের উত্তর implementation-নির্ভর।

৭ · Template, STL, lambda আর ranges

STL-এর প্রশ্নে সবচেয়ে বড় ফাঁদ হলো ফাংশনের নাম দেখে কাজ অনুমান করা। std::remove আসলে কিছু মুছে না, m[key] শুধু পড়ে না — নতুন entry বানায়, আর lazy view তৈরি করার সময় কিছুই হিসাব করে না। প্রতিটি algorithm-এর জন্য জিজ্ঞেস করুন: container-এর আকার কি বদলায়? সমান মানের element-এর ক্রম কি নিশ্চিত? কাজটা কখন ঘটে — এখন, নাকি লুপ চলার সময়? আর lambda-র জন্য: ধরা মানটি কি তৈরির মুহূর্তের কপি (by value), নাকি আসল ভ্যারিয়েবল (by reference)?

(মাঝারি) Template-এ দুটি ভিন্ন টাইপ

template <class T>
T mx(T a, T b) { return a > b ? a : b; }

std::println("{}", mx(3, 4.5));
  • 4.5
  • 4
  • Compile error
  • Undefined behavior

(কঠিন) Template বনাম সাধারণ ফাংশন — কে জিতবে?

template <class T> void f(T) { print("T "); }
void f(int)                  { print("int "); }

f(1);  f(1.0);  f('a');
  • int T int
  • int T T
  • T T T
  • Compile error: ambiguous

(মাঝারি) map-এর ক্রম আর operator[]

std::map<std::string, int> m{
    {"rice", 3}, {"egg", 12}, {"dal", 2}};
for (const auto& [k, v] : m) std::print("{} ", k);
m["fish"];
std::println("| {} {}", m.size(), m["fish"]);
  • dal egg rice | 4 0
  • rice egg dal | 4 0
  • dal egg rice | 3 0
  • dal egg rice | এরপর std::out_of_range

(মাঝারি) unordered_map ঘুরলে কোন ক্রমে key আসবে?

std::unordered_map<std::string, int> m{
    {"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}};
for (const auto& [k, v] : m) std::print("{} ", k);
  • a b c d
  • d c b a
  • Undefined behavior
  • চারটি key, কিন্তু ক্রম নির্দিষ্ট নয়

(কঠিন) erase ছাড়া std::remove

std::vector<int> v{1, 2, 3, 2, 5};
auto it = std::remove(v.begin(), v.end(), 2);
std::println("{} {} {} {} {}",
             v.size(), it - v.begin(), v[0], v[1], v[2]);
  • 3 3 1 3 5
  • 5 3 1 3 5
  • 5 3 1 2 3
  • 5 2 1 3 5

(মাঝারি) stable_sort — সমান key-এর ক্রম

std::vector<std::pair<int, char>> v{
    {2, 'a'}, {1, 'b'}, {2, 'c'}, {1, 'd'}};
std::ranges::stable_sort(v, {}, &std::pair<int, char>::first);
for (auto& p : v) std::print("{}", p.second);
  • abcd
  • dbca
  • bdac
  • bdca

(কঠিন) mutable lambda — কার মান বাড়ল?

int c = 0;
auto f = [c]() mutable { return ++c; };
f();
f();
std::println("{} {}", f(), c);
  • 3 3
  • 3 0
  • 1 0
  • Compile error

(কঠিন) View কখন কাজ করে? (filter একটি ছাপার কাজ করে)

std::vector<int> v{1, 2, 3, 4, 5, 6};
auto r = v
  | std::views::filter([](int x) {
        std::print("f{} ", x); return x % 2 == 0; })
  | std::views::take(2);
std::println("start");
for (int x : r) std::print("[{}] ", x);
  • f1 f2 f3 f4 f5 f6 start, তারপর [2] [4]
  • start, তারপর f1 f2 [2] f3 f4 [4]
  • start, তারপর f1 f2 f3 f4 [2] [4]
  • start, তারপর f1 f2 [2] f3 f4 [4] f5 f6

নিজে চালিয়ে দেখুন — STL-এর আচরণ

#include <algorithm>
#include <map>
#include <numeric>
#include <print>
#include <string>
#include <vector>

int main() {
    std::map<std::string, int> m{{"rice", 3}, {"egg", 12}, {"dal", 2}};
    m["fish"];
    std::println("1) {} {}", m.size(), m.begin()->first);

    std::vector<int> v{1, 2, 3, 2, 5};
    auto it = std::remove(v.begin(), v.end(), 2);
    std::println("2) {} {}", v.size(), it - v.begin());
    v.erase(it, v.end());
    std::println("3) {}", v);

    int x = 1;
    auto byVal = [x] { return x; };
    auto byRef = [&x] { return x; };
    x = 10;
    std::println("4) {} {}", byVal(), byRef());

    std::vector<double> d{0.5, 0.5, 0.5};
    std::println("5) {} {}", std::accumulate(d.begin(), d.end(), 0),
                 std::accumulate(d.begin(), d.end(), 0.0));
}

১) 4 dal — map-এর প্রথম key অভিধান ক্রমে সবচেয়ে ছোটটি। ২) 5 3, আর erase-remove-এর পরে ৩) [1, 3, 5] — C++23 থেকে std::println পুরো vector-ও ছাপতে পারে। ৪) 1 10 — by-value capture তৈরির মুহূর্তের কপি রাখে। ৫) 0 1.5 — এটি একটি বিখ্যাত ফাঁদ: std::accumulate-এর ফলের টাইপ তৃতীয় আর্গুমেন্টের টাইপ। 0 একটি int, তাই প্রতিটি ধাপে 0 + 0.5 = 0.5 কেটে 0 হয়; ফল 0. 0.0 দিলে ফল 1.5.

৮ · আধুনিক C++ — move, exception, optional/variant, concepts আর sequencing

এই ভাগের প্রশ্নগুলো সবচেয়ে বেশি ভুল হয়, কারণ নামগুলো বিভ্রান্তিকর। std::move কিছু সরায় না — শুধু একটি cast; সত্যিকারের সরানো হয় move constructor-এ, যদি সেটি বেছে নেওয়া যায়। Exception ছুড়লে ফাংশনগুলো থেকে ধাপে ধাপে বের হওয়ার পথে প্রতিটি local object-এর destructor চলে (stack unwinding), আর catch-গুলো যাচাই হয় ওপর থেকে নিচে, সবচেয়ে ভালো মিল অনুযায়ী নয়। আর C++17 এক্সপ্রেশনের মূল্যায়নের ক্রমের (sequencing) কিছু নিয়ম বদলেছে — পুরোনো বইয়ের কিছু “UB” উত্তর আর সঠিক নয়।

(মাঝারি) const object-এ std::move

const std::string s = "hi";
std::string t = std::move(s);
std::println("{} {}", s, t);
  • hi hi
  • hi (s খালি)
  • Compile error
  • Undefined behavior

(মাঝারি) Move করার পরে উৎস string-এ কী থাকে?

std::string a = "rice";
std::string b = std::move(a);
std::println("{} [{}]", b, a);
  • rice [rice]
  • Undefined behavior
  • rice [ ] — a-এর মান unspecified (GCC-তে খালি)
  • Compile error: a আর নেই

(মাঝারি) Stack unwinding — destructor-এর ক্রম

struct G { const char* n; ~G() { print("~{} ", n); } };
void f() { G a{"a"}; G b{"b"}; throw 1; }

try { G t{"t"}; f(); }
catch (int x) { print("catch{}", x); }
  • catch1 ~b ~a ~t
  • ~b ~a ~t catch1
  • ~a ~b ~t catch1
  • catch1

(মাঝারি) catch-এর ক্রম

try {
    throw std::out_of_range("x");
}
catch (const std::exception&)    { std::println("exception"); }
catch (const std::out_of_range&) { std::println("range"); }
  • exception
  • range
  • exception range
  • Compile error

(কঠিন) variant<bool, std::string>-এ একটি string literal

std::variant<bool, std::string> v = "abc";
std::println("{}", v.index());
  • 0
  • 1
  • Compile error
  • Undefined behavior

(কঠিন) Concepts — কোন overload?

void show(std::integral auto)       { print("int "); }
void show(std::floating_point auto) { print("float "); }
void show(auto)                     { print("other "); }

show(5); show(2.5); show('c'); show("hi");
  • int float other other
  • other other other other
  • int float int other
  • Compile error: ambiguous

(কঠিন) বিখ্যাত i = i++ + 1 — C++17 ও পরের মানে

int i = 5;
i = i++ + 1;
std::println("{}", i);
  • 7
  • 6
  • Undefined behavior
  • 5

(মাঝারি) কিন্তু i++ + i++?

int i = 5;
int j = i++ + i++;
std::println("{} {}", i, j);
  • 7 11
  • 7 10
  • Compile error
  • Undefined behavior

নিজে চালিয়ে দেখুন — move, exception, variant আর concepts

#include <concepts>
#include <optional>
#include <print>
#include <stdexcept>
#include <string>
#include <utility>
#include <variant>

struct G { const char* n; ~G() { std::print("~{} ", n); } };
void f() { G a{"a"}; G b{"b"}; throw std::runtime_error("boom"); }
void show(std::integral auto)       { std::print("int "); }
void show(std::floating_point auto) { std::print("float "); }
void show(auto)                     { std::print("other "); }
int main() {
    const std::string s = "hi";
    std::string t = std::move(s);
    std::println("1) {} {}", s, t);

    std::print("2) ");
    try { G g{"g"}; f(); }
    catch (const std::exception& e) { std::println("catch:{}", e.what()); }

    std::optional<int> a, b = 0;
    std::println("3) {} {} {}", a.has_value(), b.has_value(), a.value_or(-1));

    std::variant<bool, std::string> v = "abc";
    std::println("4) {}", v.index());
    std::print("5) "); show(5); show(2.5); show('c'); show("hi"); std::println();
}

১) hi hi. ২) ~b ~a ~g catch:boom. ৩) false true -1 — b = 0 দিয়ে তৈরি optional-এ একটি মান আছে (0), তাই সেটি “খালি” নয়; if (b) সত্য, যদিও ভেতরের মান শূন্য। এই পার্থক্য পরীক্ষায় প্রায়ই আসে। ৪) 1. ৫) int float int other. পরীক্ষা: int n = 5; constexpr int m = n * 2; যোগ করুন — compile error আসবে, কারণ constexpr ভ্যারিয়েবলের মান compile-এর সময়েই জানা থাকতে হয়, আর n const নয়।

৯ · সিস্টেম — thread, atomic আর ফাইল

Thread-এর প্রশ্নে প্রথম কাজ হলো আলাদা করা: কোন আউটপুট নিশ্চিত, আর কোনটি scheduler-এর ইচ্ছা? join()-এর পরে thread-এর সব কাজ শেষ — তার পরের লাইনে ফল নিশ্চিত। দুটি thread একই সাধারণ ভ্যারিয়েবল একসাথে বদলালে (data race) কোনো উত্তরই নিশ্চিত নয় — সেটি UB. আর ফাইলের প্রশ্নে মনে রাখুন: ofstream ডিফল্টভাবে পুরোনো লেখা মুছে দেয় (open mode), আর eof সত্য হয় পড়তে ব্যর্থ হওয়ার পরে, আগে নয়।

(মাঝারি) দুটি thread, একটি সাধারণ int

int plain = 0;
auto work = [&] { for (int k = 0; k < 100000; ++k) ++plain; };
std::thread a(work), b(work);
a.join();
b.join();
std::println("{}", plain);
  • 200000
  • 100000
  • Compile error
  • Undefined behavior

(কঠিন) join না করে std::thread ধ্বংস হলে

{
    std::thread t([] {});
}
std::println("end");
  • end
  • Undefined behavior
  • কিছু ছাপে না — std::terminate
  • Compile error

(কঠিন) while (!in.eof()) — কয়টি সংখ্যা গোনা হলো?

{ std::ofstream out("t.txt"); out << "10 20 30\n"; }
std::ifstream in("t.txt");
int a = 0, s = 0, n = 0;
while (!in.eof()) { in >> a; s += a; ++n; }
std::println("{} {} {}", n, s, a);
  • 3 60 30
  • 4 90 30
  • 4 60 0
  • অসীম লুপ

(মাঝারি) ofstream-এর open mode

{ std::ofstream o("a.txt"); o << "abc"; }
{ std::ofstream o("a.txt"); o << "d"; }
{ std::ofstream o("a.txt", std::ios::app); o << "e"; }
std::ifstream in("a.txt");
std::string s;
in >> s;
std::println("{}", s);
  • de
  • abcde
  • dbce
  • e

সব প্রশ্নের উত্তর দেওয়া শেষ হলে এখানে আপনার প্রথম চেষ্টার স্কোর দেখাবে:

নিজে চালিয়ে দেখুন — শুধু নিশ্চিত আউটপুটের thread আর ফাইল

#include <atomic>
#include <fstream>
#include <print>
#include <string>
#include <thread>

int main() {
    int x = 0;
    std::thread t([&] { x = 42; });
    t.join();
    std::println("1) {}", x);

    std::atomic<int> safe{0};
    auto work = [&] { for (int k = 0; k < 100000; ++k) ++safe; };
    std::thread a(work), b(work);
    a.join(); b.join();
    std::println("2) {}", safe.load());
    { std::ofstream out("nums.txt"); out << "10 20 30\n"; }
    std::ifstream in("nums.txt");
    int n = 0, sum = 0, cnt = 0;
    while (in >> n) { sum += n; ++cnt; }
    std::println("3) {} {}", cnt, sum);
    { std::ofstream o("a.txt"); o << "abc"; }
    { std::ofstream o("a.txt"); o << "d"; }
    { std::ofstream o("a.txt", std::ios::app); o << "e"; }
    std::ifstream ia("a.txt");
    std::string s;
    ia >> s;
    std::println("4) {}", s);
}

১) 42 — join() নিশ্চিত করে thread-এর লেখা মূল thread দেখবে। ২) 200000 — std::atomic-এর প্রতিটি ++ অবিভাজ্য, তাই কোনো পরিবর্তন হারায় না। ৩) 3 60 — while (in >> n) পড়া সফল হলে তবেই শরীর চালায়। ৪) de. লক্ষ করুন, এই প্রোগ্রামে কোনো thread কিছু ছাপে না — সব ছাপা main থেকে, join-এর পরে। thread-এর ভেতর থেকে ছাপলে লাইনের ক্রম আর নিশ্চিত থাকত না।

১০ · AI-এর যুগে — compiler-ই শেষ বিচারক

আজ অনেকে AI-কে কোড দেখিয়ে জিজ্ঞেস করেন “এটা কী ছাপবে?” সাধারণ কোডে উত্তর প্রায়ই ঠিক আসে। কিন্তু C++-এর ঠিক যে জায়গাগুলো এই পাতায় অনুশীলন করলেন — copy elision, overload resolution, virtual dispatch-এর সূক্ষ্মতা, sequencing আর UB — সেখানে ভাষা-মডেল প্রায়ই আত্মবিশ্বাসী সুরে ভুল বলে। কারণ সে কোড চালায় না; সে অনুরূপ লেখা দেখে সম্ভাব্য উত্তর অনুমান করে। আর ইন্টারনেটের পুরোনো লেখায় C++98/03-এর নিয়মই বেশি, তাই C++17-এর পরের বদলগুলো (guaranteed elision, নতুন sequencing, C++23-এর implicit move) সে প্রায়ই ভুলে যায়।

AI-এর উত্তর একটি অনুমান, compiler-এর আউটপুট একটি পর্যবেক্ষণ।
আর UB-এর কোডে দুটোর কোনোটিই প্রমাণ নয় — প্রমাণ হলো standard-এর নিয়ম।

AI সাধারণত যেখানে C++-এর আউটপুট ভুল বলে

যে ধরনের কোডAI-এর সাধারণ ভুলকীভাবে ধরবেন
ফাংশন থেকে object ফেরতঅতিরিক্ত copy/move ছাপা হবে বলে, অথবা NRVO-কে নিশ্চিত বলেনিজে চালান; elision বন্ধ করে তুলনা করতে GCC-তে -fno-elide-constructors
overload আর template একসাথে“সবচেয়ে কাছের টাইপ” ধরে ভুল overload বাছে, ambiguous error মিস করেcompile করুন; error-এর candidate তালিকা পড়ুন
i++ + i++, আর্গুমেন্টের ক্রমএকটি নির্দিষ্ট সংখ্যা বলে, অথবা C++17-এর বদল না জেনে সবকিছুকে UB বলে-Wall-এর warning দেখুন, তবে মনে রাখুন GCC-র এই warning C++17-এর আগের নিয়মে চলে
string_view, view, reference ফেরতtemporary-র lifetime বাড়বে ধরে নিয়ে নির্দিষ্ট আউটপুট বলে-fsanitize=address দিয়ে চালান — use-after-free সঙ্গে সঙ্গে ধরা পড়ে
unordered_map, thread-এর আউটপুটএকটি নির্দিষ্ট ক্রম লিখে দেয়প্রশ্ন করুন: standard কি এই ক্রমের নিশ্চয়তা দেয়?

AI দিয়ে নতুন অনুশীলনের প্রশ্ন বানাতে পারেন — শর্ত একটাই, উত্তর যাচাইয়ের দায়িত্ব আপনার। নিচের prompt-টি AI-কে এমনভাবে বাঁধে, যাতে সে আগে নিজের যুক্তি দেখায় আর UB বা unspecified উত্তর লুকাতে না পারে:

আমি C++23-এর “আউটপুট কী হবে?” ধরনের প্রশ্নের অনুশীলন করছি। বিষয়: [যেমন: constructor/destructor-এর ক্রম আর copy elision]. ধরে নাও 64-বিট Linux, GCC, libstdc++, -std=c++23. ১. ৮টি ছোট সম্পূর্ণ C++ প্রোগ্রাম দাও (প্রতিটি ১৫ লাইনের মধ্যে, #include সহ)। কঠিনতা মেশাও: ৩টি সহজ, ৩টি মাঝারি, ২টি কঠিন। ২. অন্তত ১টির উত্তর "Undefined behavior", ১টির "Compile error" (কোন error তা-ও বলবে), আর ১টির "unspecified" রাখো। ৩. প্রতিটির জন্য ৪টি অপশন দাও, কিন্তু উত্তর এখন বলো না। ৪. শেষে আলাদা অংশে প্রতিটির উত্তর দাও: কোন object কোন লাইনে তৈরি ও ধ্বংস হয়, কোন constructor/overload কেন বাছা হলো, আর standard-এর কোন নিয়ম কাজ করছে। ৫. কোনো আচরণ C++ সংস্করণভেদে বদলালে (যেমন C++17 বা C++23-এ) তা স্পষ্ট লেখো। নিশ্চিত না হলে "নিশ্চিত নই" বলো। আমি প্রতিটি প্রোগ্রাম g++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined দিয়ে চালিয়ে তোমার উত্তর মিলিয়ে দেখব।

AI-এর দেওয়া প্রশ্ন বা উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

যে ফাঁদে সবাই পড়ে

C++-এর “আউটপুট কী হবে?” প্রশ্নে বারবার ফিরে আসা পাঁচটি গভীর ফাঁদ। প্রতিটি নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১কখন কপি হবেই না, আর কখন “হয়তো” হবে না — copy elision-এর নিয়ম ঠিক কী?

Elision মানে compiler-এর একটি copy বা move constructor ডাক বাদ দেওয়া, যদিও সেই constructor-এ ছাপার মতো দৃশ্যমান কাজ থাকে। এটি সেই বিরল জায়গা, যেখানে C++ standard “as-if” নিয়মের বাইরে গিয়ে প্রোগ্রামের দৃশ্যমান আচরণ বদলানোর অনুমতি দেয়। তাই আউটপুট প্রশ্নে এর নিয়ম জানা অপরিহার্য।

  • নিশ্চিত (C++17 থেকে): একই টাইপের prvalue দিয়ে object initialize করলে — T t = T{};, return T{};, T t = make(); যেখানে make একটি prvalue ফেরত দেয়, বা by-value প্যারামিটারে use(T{3}). এখানে আসলে “বাদ দেওয়ার” কিছু নেই: P0135 প্রস্তাব value category-র সংজ্ঞাই বদলে দিয়েছে — prvalue এখন একটি object নয়, বরং object তৈরির “নির্দেশ”, যা সরাসরি গন্তব্যে কার্যকর হয়। তাই copy/move constructor deleted হলেও এই কোড compile হয়।
  • অনুমোদিত, কিন্তু নিশ্চিত নয়: নাম দেওয়া local ফেরত দেওয়া — T local; return local; (NRVO). GCC ও Clang সাধারণ ক্ষেত্রে করে, কিন্তু একাধিক return পথে ভিন্ন object ফেরত দিলে প্রায়ই পারে না। না পারলে copy নয়, আগে move চেষ্টা হয় (implicit move).
  • কখনোই নয়: lvalue থেকে initialize (T b = a;), প্যারামিটার ফেরত দেওয়া (T f(T p) { return p; } — এখানে move হয়, elision নয়), আর assignment (c = a; সবসময় operator= ডাকে)।

একটি জনপ্রিয় “অপটিমাইজেশন” উল্টো ফল দেয়: return std::move(local);. এখানে ফেরত এক্সপ্রেশনটি আর একটি সাধারণ নাম নয়, তাই NRVO-র সুযোগ নষ্ট হয় — একটি নিশ্চিত move যোগ হয়। GCC এর জন্য -Wpessimizing-move warning দেয়।

পরীক্ষায় C++17-এর আগের compiler ধরে নেওয়া প্রশ্ন পেলে লিখুন: “elision ঐচ্ছিক; compiler না করলে move constructor চলত।” নিজের চোখে দেখতে GCC-তে -fno-elide-constructors দিয়ে compile করুন — শুধু ঐচ্ছিক elision বন্ধ হয়, নিশ্চিত elision থাকে।

মূল উপলব্ধি: “এখানে কপি হবে কি না” প্রশ্নের উত্তর অনুমানে নয়, value category দেখে দিন: prvalue — কখনো না; নাম দেওয়া local ফেরত — সম্ভবত না, কিন্তু নিশ্চিত নয়; lvalue — হ্যাঁ।

প্র ০২Constructor-এর ভেতর থেকে virtual ফাংশন ডাকলে derived class-এর সংস্করণ চলে না কেন?

কারণ সেই মুহূর্তে derived অংশটি এখনো জন্মায়নি। Derived object তৈরির সময় আগে base অংশের constructor চলে; তখন derived-এর member-গুলো তৈরিই হয়নি — হয়তো একটি std::string member এখনো শুধু কাঁচা মেমরি। যদি তখন derived-এর override চলত, আর সে সেই member ছুঁত, তাহলে এমন object ব্যবহার হতো যার constructor চলেনি। C++ এই বিপদ নিয়ম দিয়েই বন্ধ করেছে: constructor (আর destructor) চলাকালীন object-এর dynamic type হলো যার constructor চলছে সেই class.

  • বাস্তবায়নে কী ঘটে: সাধারণ compiler-এ প্রতিটি constructor শুরুতে object-এর vptr-কে নিজের class-এর vtable-এ বসায়। Base() চলার সময় vptr দেখায় Base-এর টেবিলে, Derived() শুরু হলে Derived-এর টেবিলে। তাই ৬ নম্বর ভাগের প্রশ্নে BD ছাপা হয়েছিল।
  • Destructor-এ উল্টো: ~Derived শেষ হলে derived অংশ মৃত; ~Base চলার সময় object আবার শুধু Base, তাই virtual ডাক Base-এর সংস্করণে যায়।
  • Pure virtual হলে: constructor বা destructor থেকে (সরাসরি বা পরোক্ষে) pure virtual ফাংশন ডাকা UB. বাস্তবে অনেক সময় প্রোগ্রাম “pure virtual method called” লিখে থেমে যায়।

অন্য ভাষার সাথে তুলনা শিক্ষণীয়। Java ও C#-এ object শুরু থেকেই পূর্ণ derived টাইপের, তাই base constructor থেকে ডাকলেও override চলে — আর সেখানে derived-এর field তখনো ডিফল্ট মানে (null বা 0) থাকে, যা নিজেই একটি পরিচিত বাগের উৎস। C++ নিরাপত্তা বেছে নিয়েছে, “প্রত্যাশিত” আচরণ নয়। Scott Meyers তাঁর Effective C++ বইয়ে এ নিয়ে একটি আলাদা নিয়মই রেখেছেন: construction বা destruction-এর সময় কখনো virtual ফাংশন ডাকবেন না।

ডিজাইনের সমাধান: derived-এর তথ্য দরকার হলে সেটি base-এর constructor-এ প্যারামিটার হিসেবে পাঠান, অথবা দুই ধাপে তৈরি করুন — আগে object, তারপর একটি factory ফাংশন থেকে init().

মূল উপলব্ধি: C++-এর object “ধাপে ধাপে” জন্মায় ও মরে — base থেকে derived, আবার derived থেকে base. প্রতিটি ধাপে object ঠিক ততটুকুই, যতটুকু তৈরি হয়ে গেছে; virtual dispatch-ও সেটুকুই দেখে।

প্র ০৩i = i++ + 1 এখন ঠিক, কিন্তু i++ + i++ এখনো UB — C++17 মূল্যায়নের ক্রমে কী বদলেছে?

C++11 থেকে standard মূল্যায়নের ক্রম বর্ণনা করে “sequenced before” সম্পর্ক দিয়ে। মূল নিয়ম: একই memory location-এ দুটি পরিবর্তন, অথবা একটি পরিবর্তন আর একটি পড়া, যদি একে অপরের সাপেক্ষে unsequenced হয়, তাহলে UB. C++14 পর্যন্ত বেশিরভাগ অপারেটরের দুই পাশ unsequenced ছিল — তাই i = i++ + 1 আর std::cout << i << i++ দুটোই UB ছিল।

২০১৬ সালে গৃহীত P0145 প্রস্তাব (লেখক Gabriel Dos Reis, Herb Sutter ও Jonathan Caves) C++17-এ কিছু ক্রম নির্দিষ্ট করে দেয়। প্রস্তাবের বিখ্যাত উদাহরণ ছিল Bjarne Stroustrup-এর The C++ Programming Language (৪র্থ সংস্করণ)-এর একটি chained কোড — s.replace(...).replace(s.find(...), ...) — যা অভিজ্ঞ বিশেষজ্ঞদের চোখেও স্বাভাবিক লাগত, অথচ পুরোনো নিয়মে তার ফল নিশ্চিত ছিল না।

  • যা নির্দিষ্ট হলো: a = b (এবং += ইত্যাদি)-তে ডান পাশ আগে, বাঁ পাশ পরে; a << b ও a >> b-এ বাঁ পাশ আগে; a[b]-এ a আগে; a.b, a->b, a->*b-এ a আগে; ফাংশন ডাকে ফাংশনের এক্সপ্রেশনটি আর্গুমেন্টের আগে।
  • যা নির্দিষ্ট হলো না: +, *, == ইত্যাদির দুই পাশ — তাই i++ + i++ এখনো UB. আর ফাংশনের আর্গুমেন্টগুলোর পারস্পরিক ক্রম — সেগুলো এখন indeterminately sequenced (একটি পুরো শেষ হয়ে তারপর আরেকটি, ক্রম অজানা) — UB নয়, কিন্তু unspecified. ৪ নম্বর ভাগের counter() প্রশ্নটি তাই “unspecified”.

C++17-এর ফলে std::cout << i << i++ (i = 5) এখন নির্দিষ্টভাবে 55 ছাপে — বাঁ দিকের cout << i পুরো শেষ হয়ে তবে i++. এই পাতার GCC-তে চালিয়ে তাই এসেছে, যদিও GCC এখানেও “may be undefined” warning দেখায় — তার -Wsequence-point নতুন নিয়ম অনুযায়ী হালনাগাদ হয়নি। C-তে এই সব এখনো UB; C++-এর বদল C-তে আসেনি।

মূল উপলব্ধি: নিয়ম বদলায়, তাই উত্তরে C++ সংস্করণ লিখুন। আর সবচেয়ে ভালো উত্তর হলো এমন কোড না লেখা — এক statement-এ একটি ভ্যারিয়েবল একবারই বদলান, তাহলে কোনো সংস্করণেই সমস্যা নেই।

প্র ০৪std::move লিখলাম, তবু কপি হলো — std::move আসলে কী করে?

std::move কিছুই সরায় না। এটি একটি cast — মোটামুটি static_cast<T&&>(x) — যা compiler-কে বলে: “এই object-কে এখন rvalue (xvalue) হিসেবে দেখো; এর ভেতরের জিনিস নিয়ে যাওয়া চলবে।” আসল সরানোর কাজ করে move constructor বা move assignment — যদি overload resolution সেটি বেছে নেয়। নিচের প্রতিটি ক্ষেত্রে বেছে নেয় না, আর কোনো error বা warning ছাড়াই কপি হয়:

  • const object: std::move(s)-এর টাইপ const std::string&&; move constructor নেয় non-const std::string&&, তাই copy constructor জেতে (৮ নম্বর ভাগের প্রথম প্রশ্ন)। ফাংশনের প্যারামিটার const T& হলে ভেতরে std::move করেও তাই লাভ নেই।
  • move constructor নেই: class-এ user-declared copy constructor বা destructor থাকলে compiler নিজে move constructor বানায় না; তখন rvalue-ও copy constructor-এ যায় (Rule of 5 এই কারণেই)।
  • move constructor noexcept নয়: vector বড় হওয়ার সময় পুরোনো element সরাতে std::move_if_noexcept-এর নিয়ম মানে — move ছুড়তে পারলে আর copy সম্ভব হলে, strong exception guarantee রাখতে copy করে।
  • int, double-এর মতো টাইপ: এদের move আর copy একই জিনিস; move-এর পরে উৎসের মান বদলায় না।

আর move হয়ে গেলে উৎস কী অবস্থায় থাকে? standard library-র টাইপের জন্য নিয়ম: valid but unspecified — destructor চালানো, নতুন মান দেওয়া বা size() জিজ্ঞেস করা বৈধ, কিন্তু মান নিয়ে ভরসা নেই। ব্যতিক্রম কিছু টাইপ, যাদের নিয়ম নির্দিষ্ট: যেমন std::unique_ptr move-এর পরে নিশ্চিতভাবে nullptr. তাই “move-এর পরে a কী ছাপবে?” প্রশ্নে string হলে উত্তর “unspecified (GCC-তে খালি)”, আর unique_ptr হলে “null”.

মূল উপলব্ধি: std::move একটি অনুমতিপত্র, হুকুম নয়। আসল কাজটা হবে কি না, ঠিক করে টাইপের constructor আর overload resolution — তাই “move হলো” বলার আগে দেখুন কোন constructor সত্যিই বাছা হলো।

প্র ০৫Temporary-র ওপর string_view বা view কেন বিপজ্জনক, যখন const std::string& নিরাপদ?

C++-এর একটি পুরোনো নিয়ম আছে: একটি temporary-কে সরাসরি একটি local const T& (বা T&&)-এ বাঁধলে temporary-টির lifetime সেই reference-এর lifetime পর্যন্ত বেড়ে যায় (lifetime extension). তাই const std::string& r = std::string("a") + "b"; নিরাপদ। কিন্তু নিয়মটি খুব সংকীর্ণ — শুধু সত্যিকারের reference সরাসরি temporary-তে বাঁধলে খাটে।

  • string_view একটি reference নয়: এটি একটি ছোট class (pointer + দৈর্ঘ্য), যা temporary-র ডেটা থেকে তৈরি হয়। lifetime extension class-এ খাটে না, তাই temporary statement শেষে মরে আর view একটি মুক্ত হয়ে যাওয়া মেমরি দেখায় — ৫ নম্বর ভাগের শেষ প্রশ্ন। GCC বাস্তবে প্রায়ই “ঠিক” আউটপুট দেয়, কারণ মুক্ত মেমরি তখনো মোছা হয়নি — এটাই বাগটিকে লুকিয়ে রাখে।
  • ফাংশন ফেরত দিলে extension নেই: const std::string& f() { return std::string("x"); } — return-এর temporary কখনো বাড়ে না।
  • ranges views: view-ও মালিক নয়। auto v = make_vector() | std::views::filter(...) এর ক্ষেত্রে C++20-এর ranges সাবধানী: rvalue vector-কে সে owning_view-এ নিজের ভেতরে সরিয়ে রাখে, তাই এটি নিরাপদ। কিন্তু std::ranges::find(make_vector(), 3)-এর মতো algorithm temporary range পেলে iterator-এর বদলে std::ranges::dangling ফেরত দেয় — ভুল ব্যবহার compile error হয়ে ধরা পড়ে।
  • range-for: for (auto x : get().items())-এ C++20 পর্যন্ত get()-এর temporary লুপ শুরুর আগেই মরত। C++23 (P2718) range-for-এর initializer-এর সব temporary-কে লুপ শেষ পর্যন্ত বাঁচিয়ে রাখার নিয়ম করেছে — একটি পুরোনো বাগকে নিয়ম দিয়ে বন্ধ করা হয়েছে।

পরীক্ষায় এমন প্রশ্নে লিখুন: “temporary statement শেষে ধ্বংস হয়; string_view মালিক নয় বলে lifetime বাড়ায় না; পরে পড়া UB.” আর সমাধান হিসেবে বলুন: মালিক (std::string) একটি নাম দিয়ে আগে রাখুন, তারপর তার ওপর view নিন।

মূল উপলব্ধি: প্রতিটি view, reference আর pointer-এর পাশে একটি প্রশ্ন লিখে রাখুন — “মালিক কে, আর সে কতক্ষণ বাঁচে?” উত্তর “পরের semicolon পর্যন্ত” হলে সেটি একটি বাগ।

শব্দকোষ

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

মনে রাখুন

  1. আউটপুট অনুমান নয় — trace table এঁকে লাইন ধরে চালান: ভ্যারিয়েবল, call stack, আর প্রতিটি object-এর জন্ম ও মৃত্যুর লাইন। জন্ম আর মৃত্যুর সংখ্যা মিলিয়ে দেখুন।
  2. জন্ম: base → member (ঘোষণার ক্রমে) → শরীর; মৃত্যু উল্টো। prvalue থেকে কোনো copy/move নেই (C++17), NRVO নিশ্চিত নয়, lvalue থেকে copy.
  3. কোন ফাংশন: হুবহু মিল > promotion > conversion; সমান হলে non-template ও বেশি constrained জেতে; virtual হলে object-এর আসল টাইপ, কিন্তু default argument আর slicing static type দেখে।
  4. উত্তর চার রকম: নির্দিষ্ট আউটপুট, compile error, undefined behavior, unspecified/implementation-defined — শেষ তিনটির ক্ষেত্রে কারণ আর C++ সংস্করণ লিখুন।
  5. AI-এর আউটপুট-অনুমান যাচাই করুন: নিজে trace করুন, -Wall -Wextra দিয়ে চালান, sanitizer দিয়ে UB খুঁজুন, দুটি compiler-এ মেলান।

পরের পর্বে: কোড পড়ার পরে এবার দ্রুত ধারণার যাচাই — পুরো C++ কোর্স থেকে বিষয়ভিত্তিক MCQ প্রশ্নব্যাংক, সময় ধরে অনুশীলনের কৌশলসহ।

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

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