এই পর্বে যা পাবেন
- কাগজে-কলমে 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) ও ৫২ | পর্ব ৫৪-এর লিখিত ও ভাইভা অংশ |
| সফটওয়্যার কোম্পানির ইন্টারভিউ | পর্ব ৫৫ (কোডিং) | পর্ব ৫২ — অনেক টেকনিক্যাল রাউন্ড “এই কোড কী ছাপবে?” দিয়েই শুরু হয় |
| শুধু নিজের শেখা যাচাই | এই পর্ব, প্রথম থেকে শেষ | যে ভাগে বেশি ভুল, সেই পর্বে ফিরে যান |
এই পাতা কীভাবে ব্যবহার করবেন
- প্রথম দফা — অপশন ঢেকে রাখুন। প্রতিটি কোড পড়ে খাতায় নিজের উত্তর লিখুন, তারপর অপশনে চাপুন। প্রথম চেষ্টায় কতগুলো ঠিক হলো, অনুশীলনের শেষে স্কোর দেখাবে।
- দ্বিতীয় দফা — ভুলগুলোর কারণ খুঁজুন। প্রতিটি ভুল অপশনের ব্যাখ্যা পড়ুন। প্রতিটি ভাগের শুরুতে লেখা আছে কোন পর্বগুলো ঝালাতে হবে।
- তৃতীয় দফা — নিজে চালান। প্রতিটি ভাগের “নিজে চালিয়ে দেখুন” কোড চালান। একটি লাইন বদলে আগে ফল অনুমান করুন, তারপর চালিয়ে মেলান।
সব উত্তর 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-এর “আউটপুট কী হবে?” প্রশ্নের প্রায় সব ফাঁদ (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-এর ছয়টি নিয়ম
- প্রতিটি ভ্যারিয়েবল ও object-এর জন্য একটি কলাম, আর একটি কলাম আউটপুটের জন্য।
staticlocal, global আর ফাংশনের প্যারামিটারও object — তাদের কলাম ভুলবেন না। - প্রতিটি object-এর জন্ম ও মৃত্যুর লাইন চিহ্নিত করুন। জন্ম: সংজ্ঞার লাইনে (member হলে ঘোষণার ক্রমে, base আগে)। মৃত্যু: যে
}-এ scope শেষ, সেখানে — জন্মের উল্টো ক্রমে। Temporary মরে পুরো statement শেষ হলে। - প্রতিটি initialization-এ জিজ্ঞেস করুন: কোন constructor? নতুন মান থেকে → সাধারণ constructor; একটি lvalue থেকে → copy;
std::moveকরা বা মরতে যাওয়া object থেকে → move; একই টাইপের prvalue থেকে → কোনোটিই নয় (guaranteed copy elision). - প্রতিটি ফাংশন ডাকে জিজ্ঞেস করুন: কোন ফাংশন? একাধিক overload থাকলে overload resolution (হুবহু মিল > promotion > conversion); virtual হলে object-এর আসল টাইপ (dynamic dispatch), না হলে pointer/reference-এর টাইপ।
- ফাংশন ডাকলে call stack-এ নতুন একটি ঘর — নিজস্ব প্যারামিটার আর local নিয়ে। ফেরার সময় সেই ঘরের সব object-এর destructor চলে।
- আউটপুট কলামে হুবহু লিখুন — 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}
| a.id | |
| b.id | |
| use()-এর t.id | |
| call stack |
- লাইন ১০ — a জন্মাল।
T a{1}সাধারণ constructor ডাকে, ছাপে C1. খাতায় a-এর কলামে জন্মের লাইন লিখুন: ১০. - লাইন ১১ — b, কিন্তু কোনো কপি নেই।
make(2)-এর ভেতরেreturn T{2}একটি prvalue. C++17 থেকে নিয়ম: একই টাইপের prvalue দিয়ে object initialize করলে সেটি সরাসরি গন্তব্যেই তৈরি হয় — temporary, copy বা move কিছুই হয় না। তাই শুধু C2, আর এটি b নিজেই। - লাইন ১২ — by-value প্যারামিটার।
use(a)-এ t একটি নতুন object, তৈরি হয় a (একটি lvalue) থেকে — তাই copy constructor: id = 1 + 10 = 11, ছাপে K11. call stack-এ এখন main → use. - লাইন ৮ — ফাংশনের শরীর। ছাপে U11. লক্ষ করুন, t আর a আলাদা object — t-কে বদলালে a বদলাত না।
- t মরল। ফাংশন শেষ, প্যারামিটার t-এর destructor: D11. (প্যারামিটার ঠিক ফাংশন ফেরার মুহূর্তে মরে নাকি পুরো statement শেষে, তা implementation-defined; কিন্তু দুই ক্ষেত্রেই পরের লাইনের আগে — তাই আউটপুট একই।)
- লাইন ১৩ — temporary দিয়ে ডাক।
T{3}একটি prvalue, আর প্যারামিটার t একই টাইপের। আবার guaranteed elision: t সরাসরি তৈরি হয়, ছাপে শুধু C3 — কোনো K নেই। - শরীর: U3.
- t মরল: D3. এখানে একটিই object ছিল, তাই একটিই D.
- লাইন ১৪ — main-এর scope শেষ। local object মরে জন্মের উল্টো ক্রমে: আগে b — D2.
- তারপর 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)).
ক্রমনিচের কোডে struct A, B, C-এর constructor বড় হাতের আর destructor ছোট হাতের অক্ষর ছাপে। struct D : A { B b; C c; }; — { D d; } চালালে ঘটনাগুলো কোন ক্রমে ঘটবে? (▲▼ চেপে সাজান)
- base A-এর constructor — A
- member b-এর constructor — B (ঘোষণার ক্রমে প্রথম member)
- member c-এর constructor — C
- D-এর নিজের শরীর শেষ; scope শেষে member c-এর destructor — c
- member b-এর destructor — b
- base A-এর destructor — a
৩ · ভিত্তি — টাইপ, অপারেটর আর ইনপুট-আউটপুট
এই ভাগের কোড ছোট, কিন্তু ফাঁদ গভীর। প্রতিটি এক্সপ্রেশনে তিনটি প্রশ্ন করুন: হিসাব কোন টাইপে হচ্ছে (implicit conversion)? কোন অপারেটর আগে (precedence)? আর auto আসলে কোন টাইপ ধরল (type deduction)? মনে রাখুন, বাঁ পাশের টাইপ ডান পাশের হিসাব বদলায় না — double d = 7 / 2;-এ ভাগটা আগে int-এ হয়ে যায়।
ঝালিয়ে নিন: পর্ব ০৪ · প্রথম প্রোগ্রাম, পর্ব ০৫ · টাইপ, auto আর const, পর্ব ০৬ · অপারেটর, পর্ব ০৭ · ইনপুট-আউটপুট.
(সহজ) double-এ রাখলেই কি দশমিক আসে?
int a = 7, b = 2;
double d = a / b;
std::println("{} {}", d, a / 2.0);
3.5 3.53 3.53.0 3.53 3
(মাঝারি) auto কোন টাইপ ধরল — কী ছাপা হবে?
auto x = 5 / 2;
auto y = 5 / 2.0f;
auto z = 'a' + 1;
std::println("{} {} {}", x, y, z);
2.5 2.5 b2 2.5 b2 2.5 982 2 98
(মাঝারি) cout-এর << আর shift-এর << — কী ছাপা হবে?
std::cout << 1 << 2 + 3 << ' '
<< (1 << 2) + 3 << '\n';
15 77 71 5 715 32
(কঠিন) Signed আর unsigned তুলনা — কী ছাপা হবে?
int i = -1;
unsigned u = 1;
std::cout << (i < u) << '\n';
10falseUndefined 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);
34Compile errorUndefined 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.
ঝালিয়ে নিন: পর্ব ০৮ · সিদ্ধান্ত ও লুপ, পর্ব ০৯ · ফাংশন ও overloading, পর্ব ১০ · reference, পর্ব ১২ · enum class, struct আর structured bindings.
(সহজ) 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 72 3 41 2 34 6 8
(মাঝারি) কোন overload চলবে?
void f(int) { std::print("int "); }
void f(double) { std::print("double "); }
f('a'); f(2.5f); f(true);
int double intint int intint double doubleCompile error: ambiguous
(কঠিন) একই দুটি overload, এবার long দিয়ে ডাক
void f(int) { std::print("int "); }
void f(double) { std::print("double "); }
f(5L);
intdoubleCompile error: no matching functionCompile error: ambiguous
(মাঝারি) Reference-কে কি অন্য ভ্যারিয়েবলে সরানো যায়?
int a = 1;
int& r = a;
int b = 5;
r = b;
b = 9;
std::println("{} {} {}", a, r, b);
1 5 91 9 95 5 99 9 9
(কঠিন) static counter, তিনবার এক লাইনে
int counter() { static int c = 0; return ++c; }
std::println("{} {} {}", counter(), counter(), counter());
1 2 33 2 1Undefined behaviorUnspecified — ক্রম নির্দিষ্ট নয়
(মাঝারি) 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 101 20 101 2 101 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 ও array, পর্ব ১৪ · string ও string_view, পর্ব ১৫ · pointer, পর্ব ১৭ · RAII ও smart pointer, পর্ব ১৮ · lifetime ও dangling.
(সহজ) গোল বন্ধনী আর বাঁকা বন্ধনী — vector-এ কতটি element?
std::vector<int> a(3, 7), b{3, 7};
std::println("{} {} {}", a.size(), b.size(), a[2]);
3 3 72 2 73 2 73 2 3
(মাঝারি) String literal-এ + 1
std::string t = "abc" + 1;
std::string u = std::string("abc") + "1";
std::println("{} {}", t, u);
abc1 abc1bc abc1abcb abc1Compile error
(মাঝারি) দুটি string literal জোড়া
std::string s = "Dhaka" + " city";
std::println("{}", s);
Dhaka cityDhakaUndefined behaviorCompile error
(মাঝারি) unique_ptr move করার পরে
auto p = std::make_unique<int>(5);
auto q = std::move(p);
std::println("{} {}", p == nullptr, *q);
true 51 5false 5Compile 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 03 2 13 3 23 2 0
(কঠিন) local ভ্যারিয়েবলের reference ফেরত — C++23-এ কী হয়?
int& f() {
int x = 5;
return x;
}
int main() { std::println("{}", f()); }
5Undefined behaviorCompile error0
(কঠিন) temporary string-এর ওপর string_view
std::string_view sv = std::string("hello") + " world";
std::cout << sv << '\n';
hello worldhelloCompile errorUndefined 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.
ঝালিয়ে নিন: পর্ব ২০ · class ও object, পর্ব ২১ · constructor ও destructor, পর্ব ২২ · copy, move আর Rule of 0/3/5, পর্ব ২৩ · operator overloading, পর্ব ২৪ · inheritance, পর্ব ২৫ · polymorphism ও virtual, পর্ব ২৬ · abstract class.
(মাঝারি) 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; }
CABcbaABCcbaABCabcBACcab
(কঠিন) 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; }
XYYXxyXYCompile 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 dtorctor move dtor dtorctor copy dtor dtorctor 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 dtorctor copy move move dtor dtor dtorctor copy move dtor dtor dtorctor 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 DerDer base BaseBase base BaseDer 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; }
DDBBBDCompile 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 D2D2 D2B1 D2Compile 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~BaseCompile errorUndefined 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)?
ঝালিয়ে নিন: পর্ব ২৮ · function template, পর্ব ২৯ · class template, পর্ব ৩০ · sequence container, পর্ব ৩১ · map, set আর unordered_map, পর্ব ৩২ · iterator, পর্ব ৩৩ · STL algorithm, পর্ব ৩৪ · lambda, পর্ব ৩৫ · ranges ও views.
(মাঝারি) Template-এ দুটি ভিন্ন টাইপ
template <class T>
T mx(T a, T b) { return a > b ? a : b; }
std::println("{}", mx(3, 4.5));
4.54Compile errorUndefined behavior
(কঠিন) Template বনাম সাধারণ ফাংশন — কে জিতবে?
template <class T> void f(T) { print("T "); }
void f(int) { print("int "); }
f(1); f(1.0); f('a');
int T intint T TT T TCompile 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 0rice egg dal | 4 0dal egg rice | 3 0dal 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 dd c b aUndefined 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 55 3 1 3 55 3 1 2 35 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);
abcddbcabdacbdca
(কঠিন) mutable lambda — কার মান বাড়ল?
int c = 0;
auto f = [c]() mutable { return ++c; };
f();
f();
std::println("{} {}", f(), c);
3 33 01 0Compile 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” উত্তর আর সঠিক নয়।
ঝালিয়ে নিন: পর্ব ৩৬ · move semantics, পর্ব ৩৭ · exception আর std::expected, পর্ব ৩৮ · optional, variant আর any, পর্ব ৩৯ · constexpr, পর্ব ৪০ · concepts, পর্ব ৪৩ · Undefined Behavior ও নিরাপদ C++.
(মাঝারি) const object-এ std::move
const std::string s = "hi";
std::string t = std::move(s);
std::println("{} {}", s, t);
hi hihi(s খালি)Compile errorUndefined behavior
(মাঝারি) Move করার পরে উৎস string-এ কী থাকে?
std::string a = "rice";
std::string b = std::move(a);
std::println("{} [{}]", b, a);
rice [rice]Undefined behaviorrice [ ] — 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 catch1catch1
(মাঝারি) catch-এর ক্রম
try {
throw std::out_of_range("x");
}
catch (const std::exception&) { std::println("exception"); }
catch (const std::out_of_range&) { std::println("range"); }
exceptionrangeexception rangeCompile error
(কঠিন) variant<bool, std::string>-এ একটি string literal
std::variant<bool, std::string> v = "abc";
std::println("{}", v.index());
01Compile errorUndefined 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 otherother other other otherint float int otherCompile error: ambiguous
(কঠিন) বিখ্যাত i = i++ + 1 — C++17 ও পরের মানে
int i = 5;
i = i++ + 1;
std::println("{}", i);
76Undefined behavior5
(মাঝারি) কিন্তু i++ + i++?
int i = 5;
int j = i++ + i++;
std::println("{} {}", i, j);
7 117 10Compile errorUndefined 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 সত্য হয় পড়তে ব্যর্থ হওয়ার পরে, আগে নয়।
ঝালিয়ে নিন: পর্ব ৪৪ · ফাইল I/O ও filesystem, পর্ব ৪৫ · মাল্টিথ্রেডিং, পর্ব ৪৬ · async, future আর atomics. টেস্ট আর sanitizer দিয়ে এমন বাগ ধরার কৌশল: পর্ব ৪৮ · টেস্টিং ও ডিবাগিং.
(মাঝারি) দুটি 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);
200000100000Compile errorUndefined behavior
(কঠিন) join না করে std::thread ধ্বংস হলে
{
std::thread t([] {});
}
std::println("end");
endUndefined behaviorকিছু ছাপে না — std::terminateCompile 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 304 90 304 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);
deabcdedbcee
সব প্রশ্নের উত্তর দেওয়া শেষ হলে এখানে আপনার প্রথম চেষ্টার স্কোর দেখাবে:
নিজে চালিয়ে দেখুন — শুধু নিশ্চিত আউটপুটের 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) সে প্রায়ই ভুলে যায়।
আর 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 উত্তর লুকাতে না পারে:
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-conststd::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 পর্যন্ত” হলে সেটি একটি বাগ।
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বে ব্যবহৃত সব শব্দ এখানে একসাথে:
মনে রাখুন
- আউটপুট অনুমান নয় — trace table এঁকে লাইন ধরে চালান: ভ্যারিয়েবল, call stack, আর প্রতিটি object-এর জন্ম ও মৃত্যুর লাইন। জন্ম আর মৃত্যুর সংখ্যা মিলিয়ে দেখুন।
- জন্ম: base → member (ঘোষণার ক্রমে) → শরীর; মৃত্যু উল্টো। prvalue থেকে কোনো copy/move নেই (C++17), NRVO নিশ্চিত নয়, lvalue থেকে copy.
- কোন ফাংশন: হুবহু মিল > promotion > conversion; সমান হলে non-template ও বেশি constrained জেতে; virtual হলে object-এর আসল টাইপ, কিন্তু default argument আর slicing static type দেখে।
- উত্তর চার রকম: নির্দিষ্ট আউটপুট, compile error, undefined behavior, unspecified/implementation-defined — শেষ তিনটির ক্ষেত্রে কারণ আর C++ সংস্করণ লিখুন।
- AI-এর আউটপুট-অনুমান যাচাই করুন: নিজে trace করুন,
-Wall -Wextraদিয়ে চালান, sanitizer দিয়ে UB খুঁজুন, দুটি compiler-এ মেলান।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৩ · MCQ প্রশ্নব্যাংক — যেকোনো পরীক্ষার প্রস্তুতিপরবর্তী পর্বপুরো কোর্সের ধারণা যাচাইয়ের বিষয়ভিত্তিক MCQ.
- পর্ব ৫৪ · লিখিত ও মৌখিক (ভাইভা) প্রশ্নলিখিত পরীক্ষা“পার্থক্য লিখুন”, “class লিখুন” আর ভাইভার উত্তর সাজানোর কৌশল।
- পর্ব ৫৫ · C++ কোডিং ইন্টারভিউ — সমস্যা থেকে সমাধানইন্টারভিউসমস্যা বোঝা, পরিকল্পনা, কোড আর টেস্ট — ইন্টারভিউর পুরো ধারা।
- পর্ব ৪৩ · Undefined Behavior ও নিরাপদ C++গভীরে যানকেন UB-এর কোডের কোনো নির্দিষ্ট আউটপুট নেই, আর sanitizer দিয়ে তা কীভাবে ধরবেন।
- C প্রোগ্রামিং · আউটপুট কী হবে?C কোর্সinteger promotion, pointer, macro আর C-এর UB নিয়ে ৫৪টি প্রশ্ন।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — যে ভাগে ভুল বেশি হলো, সেই পর্বে ফিরে যান।