এই পর্বে যা শিখবেন
- RAII কী, আর কেন একে C++-এর রিসোর্স-নিরাপত্তার মূল ভিত্তি বলা হয়
- Scope শেষ হলে আর exception-এর সময় destructor কীভাবে, কোন ক্রমে চলে
std::unique_ptr,std::make_uniqueআরstd::moveদিয়ে মালিকানা হাতবদল; ফাংশনে কীভাবে পাঠাবেনstd::shared_ptr-এর reference count, তার খরচ, আরstd::weak_ptrদিয়ে cycle ও cache- Custom deleter দিয়ে C লাইব্রেরির রিসোর্স মোড়ানো, মালিকানার নিয়ম-সারাংশ, আর AI-এর কোডে এসব ভুল ধরা
১ · সমস্যাটা আগে — যা ধার নেবেন, তা ফেরত দিতে হবে
আগের পর্বে আপনি new দিয়ে heap থেকে মেমরি নিয়েছেন আর delete দিয়ে ফেরত দিয়েছেন। সেখানেই দেখেছেন, ফেরত দিতে ভুলে গেলে কী হয়। কিন্তু মেমরি একমাত্র জিনিস নয়, যা প্রোগ্রাম ধার নেয়। খোলা ফাইল, ডেটাবেসের সংযোগ, নেটওয়ার্ক socket, একাধিক thread-এর মধ্যে ভাগ করা তালা (lock) — এ সবই সীমিত, আর সবই কাজ শেষে ফেরত দিতে হয়। এমন যেকোনো জিনিসকে বলে resource (রিসোর্স)।
ধরুন আপনি শহরের একটি পাবলিক লাইব্রেরি থেকে বই নেন। পুরোনো নিয়মে বই ফেরত দেওয়া আপনার দায়িত্ব — মনে রাখতে হবে, কাউন্টারে যেতে হবে। হঠাৎ বৃষ্টি নামল, আপনি তাড়াহুড়ো করে বাসায় চলে গেলেন, বা অসুস্থ হয়ে কয়েকদিন আসতে পারলেন না — বই ফেরত হলো না। এবার কল্পনা করুন একটি আধুনিক লাইব্রেরি, যেখানে প্রতিটি বইয়ে একটি চিপ আছে। আপনি যেভাবেই লাইব্রেরির পড়ার ঘর থেকে বের হন — দরজা দিয়ে হেঁটে, তাড়াহুড়োয় দৌড়ে, বা আগুনের ঘণ্টা বাজলে জরুরি দরজা দিয়ে — দরজার স্ক্যানার বইটিকে নিজে থেকেই “ফেরত” হিসেবে লিখে নেয়। আপনাকে আর মনে রাখতে হয় না। ঘর ছাড়লেই বই ফেরত। C++-এর RAII ঠিক এই ব্যবস্থা।
আগে পুরোনো ব্যবস্থার সমস্যাটা কোডে দেখুন। একটি অনলাইন দোকানের অর্ডার প্রক্রিয়া করার ফাংশন — হাতে new আর delete দিয়ে লেখা:
void processOrder(int id) {
Order* o = new Order(id); // heap থেকে নেওয়া
if (!o->valid()) return; // ফাঁক ১: আগেভাগে ফেরা — delete হলো না
o->charge(); // ফাঁক ২: এখানে exception হলে? delete হলো না
o->ship();
delete o; // শুধু সব ঠিকঠাক চললেই এখানে পৌঁছায়
}কোডটি দেখতে নিরীহ, কিন্তু এতে দুটি গর্ত। অর্ডার অবৈধ হলে ফাংশন আগেভাগে ফেরে, আর delete লাইনে কখনো পৌঁছায় না। আর charge() যদি কোনো exception ছোড়ে (ধরুন bKash-এর সার্ভারে সংযোগ পাওয়া গেল না) — exception ছোড়ার পর ফাংশনের বাকি অংশ আর চলে না, আবারও delete বাদ। প্রতিবার এমন হলে কিছু মেমরি চিরতরে আটকে যায় — একে বলে memory leak. দিনে লাখ অর্ডার হলে সার্ভারের মেমরি ধীরে ধীরে ভরে যায়, আর কয়েকদিন পর কেউ বুঝতে পারে না কেন সার্ভার রিস্টার্ট করতে হচ্ছে।
হাতে ঠিক করার চেষ্টা করলে প্রতিটি return-এর আগে delete বসাতে হয়, প্রতিটি সম্ভাব্য exception-এর চারপাশে try/catch দিতে হয়। ফাইল আর lock যোগ হলে প্রতিটি বের হওয়ার পথে তিনটি জিনিস মনে রাখতে হয়। ছয় মাস পর কেউ একটি নতুন return যোগ করলেন, আর ভুলে গেলেন — বাগ ফিরে এল। সমস্যাটা প্রোগ্রামারের অমনোযোগ নয়, পদ্ধতির। “মনে রেখে ফেরত দেওয়া” এমন একটি দায়িত্ব, যা মানুষ বড় কোডে নির্ভরযোগ্যভাবে পালন করতে পারে না।
প্রশ্নটা তাই বদলাতে হবে: “ফেরত দিতে কীভাবে মনে রাখব?” নয়, বরং “ফেরত দেওয়ার কাজটা এমন জায়গায় রাখব কীভাবে, যা ভোলা অসম্ভব?” C++-এ এমন একটি জায়গা আছে, যেটি compiler নিজে নিশ্চিত করে — object-এর destructor.
২ · RAII — রিসোর্স বাঁধা থাকে object-এর জীবনের সাথে
C++-এ প্রতিটি class-এর দুটি বিশেষ ফাংশন থাকতে পারে। constructor চলে object তৈরির মুহূর্তে। destructor — নাম ~ চিহ্ন দিয়ে শুরু, যেমন ~Book() — চলে object-এর জীবন শেষ হওয়ার মুহূর্তে। পর্ব ২১-এ এ দুটি বিস্তারিত দেখবেন; আজ শুধু একটি কথা জরুরি: একটি local object-এর scope যেভাবেই শেষ হোক, compiler তার destructor ডাকবেই। স্বাভাবিকভাবে }-এ পৌঁছালে, মাঝপথে return করলে, লুপ থেকে break করলে, এমনকি exception উড়ে গেলেও।
এই নিশ্চয়তাকে কাজে লাগানোর কৌশলের নাম RAII — Resource Acquisition Is Initialization. নামটা খটমটে, কিন্তু ধারণা দুই লাইনের:
constructor নেয়
object তৈরির সময়েই রিসোর্স নেওয়া হয় — ফাইল খোলা, মেমরি চাওয়া, lock নেওয়া। নিতে না পারলে object তৈরিই হয় না (constructor exception ছোড়ে)।
acquiredestructor ফেরত দেয়
object-এর জীবন শেষ হলে destructor রিসোর্স ছেড়ে দেয়। কাউকে মনে রাখতে হয় না, কারণ destructor ডাকা compiler-এর দায়িত্ব।
releaseফল: রিসোর্সের আয়ু = object-এর আয়ু
object যতক্ষণ বেঁচে আছে, রিসোর্স ততক্ষণ ধরা। object মরল, রিসোর্স ফেরত গেল। দুটির lifetime এক সুতোয় বাঁধা।
tie the lifetimeএবার লাইব্রেরির বইটিকেই একটি ছোট RAII class বানিয়ে দেখুন। constructor-এ “বই নিলাম” আর destructor-এ “বই ফেরত দিলাম” প্রিন্ট হবে, যাতে আপনি নিজের চোখে দেখেন destructor কখন চলে। study ফাংশনটি দুবার ডাকা হয় — একবার সব ঠিকঠাক, একবার মাঝপথে exception (লোডশেডিং!) সহ:
#include <print>
#include <stdexcept>
#include <string>
class Book { // RAII: constructor-e nei, destructor-e ferot
std::string title;
public:
explicit Book(const std::string& t) : title(t) { std::println("borrow : {}", title); }
~Book() { std::println("return : {}", title); }
};
void study(bool fail) {
Book a("C++ Primer");
Book b("Algorithms");
if (fail) throw std::runtime_error("load-shedding");
std::println("studying done");
}
int main() {
study(false);
try { study(true); }
catch (const std::exception& e) { std::println("caught: {}", e.what()); }
}প্রথম ডাকে আসে borrow : C++ Primer, borrow : Algorithms, studying done, তারপর return : Algorithms আর return : C++ Primer. খেয়াল করুন, ফেরত হয়েছে উল্টো ক্রমে — শেষে নেওয়া বই আগে ফেরত, ঠিক থালার স্তূপের মতো। দ্বিতীয় ডাকে studying done আসে না, কারণ তার আগেই exception উড়েছে। তবু দুটি return : ... লাইন আসে, আর আসে caught: load-shedding-এর আগে. অর্থাৎ catch ব্লক চলার আগেই দুটি বই ফেরত হয়ে গেছে। কোথাও আমরা “ফেরত দাও” লিখিনি — destructor নিজেই চলেছে।
দ্বিতীয় ডাকে ভেতরে ঠিক কী ঘটে, ধাপে ধাপে দেখুন। প্রতিটি ধাপে আগে নিজে অনুমান করুন, তারপর “পরে” চাপুন:
1void study() {2 Book a("C++ Primer");3 Book b("Algorithms");4 throw std::runtime_error("load-shedding");5 std::println("studying done"); // কখনো চলে না6}7try { study(); }8catch (const std::exception& e) { std::println("caught: {}", e.what()); }
| a | |
| b | |
| অবস্থা |
- ফাংশন ডাকা হলো।
try-এর ভেতর থেকেstudy()ডাকা হলো। stack-এ ফাংশনটির নিজস্ব frame তৈরি হলো, যেখানে এর local object-রা থাকবে। - প্রথম রিসোর্স নেওয়া।
a-এর constructor চলল — বই নেওয়া হলো। এই মুহূর্ত থেকে C++ প্রতিশ্রুতি দেয়:aযেভাবেই মরুক, তার destructor চলবে। - দ্বিতীয় রিসোর্স।
bতৈরি হলো। এখন দুটি object বেঁচে আছে, আর compiler মনে রাখছে কে আগে, কে পরে তৈরি হয়েছে। - Exception উড়ল।
throwচলতেই স্বাভাবিক পথ থেমে যায় — লাইন ৫ আর কখনো চলে না। প্রোগ্রাম এখন খুঁজছে কাছের কোনcatchএই exception ধরতে পারে। সেটি আছেstudy-এর বাইরে, তাইstudy-এর frame ছেড়ে বের হতে হবে। - Stack unwinding — উল্টো ক্রমে পরিষ্কার। frame ছাড়ার আগে প্রতিটি তৈরি-হওয়া local object-এর destructor চলে, তৈরির উল্টো ক্রমে: আগে
b, তারপরa. এই প্রক্রিয়ার নাম stack unwinding (স্ট্যাক গুটিয়ে আনা)। মাঝে আরও ফাংশন থাকলে প্রতিটির frame এভাবে গুটানো হতো। - এবার catch. সব পরিষ্কার হওয়ার পরে catch ব্লক চলে। ফলে catch-এ পৌঁছানোর সময় কোনো বই, ফাইল বা মেমরি আটকে নেই। এটাই RAII-এর আসল জাদু: ভুলের পথেও পরিষ্কারের কাজ নিশ্চিত, আর তার জন্য একটি লাইনও আলাদা লিখতে হয়নি।
দুটি সূক্ষ্ম নিয়ম মনে রাখুন। এক, exception যদি কোথাও ধরা না-ই হয় (কোনো catch নেই), তখন প্রোগ্রাম std::terminate দিয়ে বন্ধ হয়, আর stack unwinding আদৌ হবে কি না, তা compiler-এর ওপর নির্ভর করে। তাই গুরুত্বপূর্ণ প্রোগ্রামে main-এ অন্তত একটি catch রাখা ভালো (exception-এর পুরো গল্প পর্ব ৩৭-এ)। দুই, destructor থেকে কখনো exception ছুড়বেন না। C++11 থেকে destructor ডিফল্টভাবে noexcept; unwinding চলার সময় আরেকটি exception বেরোলে প্রোগ্রাম সঙ্গে সঙ্গে terminate হয়।
৩ · RAII সবখানে — ফাইল, lock আর মেমরি
সুখবর হলো, বেশিরভাগ সময় RAII class আপনাকে নিজে লিখতে হয় না। C++-এর standard library-র প্রায় প্রতিটি রিসোর্স-ধরা টাইপ ইতিমধ্যে RAII. আপনি এগুলো ব্যবহার করে আসছেন, হয়তো খেয়াল না করেই।
ফাইল — std::ofstream
void saveReport(const std::string& text) {
std::ofstream out("report.txt"); // constructor: ফাইল খোলে
if (text.empty()) return; // আগেভাগে ফেরা — তবু ফাইল বন্ধ হবে
out << text;
} // destructor: ফাইল বন্ধ করেএখানে কোথাও close() নেই, দরকারও নেই। out-এর scope যেভাবেই শেষ হোক, তার destructor ফাইলের বাকি লেখা ডিস্কে পাঠিয়ে ফাইলটি বন্ধ করে। (ফাইল নিয়ে বিস্তারিত পর্ব ৪৪-এ।)
Lock — std::lock_guard (এক ঝলক)
একাধিক thread একসাথে একটি দোকানের মোট বিক্রির হিসাব বাড়ালে গোলমাল হয়, তাই একটি তালা লাগে — mutex. তালা লাগিয়ে খুলতে ভুলে গেলে বাকি সব thread চিরকাল দাঁড়িয়ে থাকে, যেন ঢাকার মোড়ে সিগন্যাল লাল হয়ে আটকে গেছে। RAII সমাধান std::lock_guard:
std::mutex m;
long long totalSales = 0;
void addSale(int taka) {
std::lock_guard<std::mutex> lock(m); // constructor: তালা লাগায়
totalSales += taka; // এখানে exception হলেও...
} // destructor: তালা খোলেthread আর mutex-এর পুরো ব্যাখ্যা পর্ব ৪৫-এ। এখন শুধু নকশাটা দেখুন — একই ছাঁচ: তৈরিতে নেওয়া, ধ্বংসে ফেরত।
মেমরি — std::vector আর std::string
পর্ব ১৩-এর std::vector আর পর্ব ১৪-এর std::string দুটিই ভেতরে heap থেকে মেমরি নেয়। আপনি কখনো তাদের জন্য delete লেখেননি, কারণ তাদের destructor সেটা করে। vector হলো সবচেয়ে বেশি ব্যবহৃত RAII object. তাই heap-এ একসাথে অনেক জিনিস দরকার হলে প্রথম উত্তর সবসময় std::vector — আলাদা কিছু ভাবার আগে।
| রিসোর্স | হাতে করলে (ভুলের ঝুঁকি) | RAII টাইপ |
|---|---|---|
| অনেক উপাদানের মেমরি | new[] / delete[] | std::vector, std::string |
| একটি heap object | new / delete | std::unique_ptr (অংশ ৪) |
| ভাগাভাগি করা heap object | হাতে গোনা, কে শেষে delete করবে? | std::shared_ptr (অংশ ৬) |
| ফাইল | fopen / fclose | std::ifstream, std::ofstream |
| mutex lock | m.lock() / m.unlock() | std::lock_guard, std::scoped_lock |
| thread | শেষে join করতে ভোলা | std::jthread (C++20, destructor-এ নিজে join করে) |
C-তেও একই রিসোর্স — malloc/free, fopen/fclose — কিন্তু C-তে destructor নেই, তাই ফেরত দেওয়া পুরোপুরি প্রোগ্রামারের দায়িত্ব। বড় C কোডে (যেমন Linux kernel) এজন্য goto cleanup; রীতি চলে: সব বের হওয়ার পথ একটি লেবেলে এনে সেখানে একসাথে ফেরত দেওয়া। C++-এর RAII সেই লেবেলটিকে compiler-এর হাতে তুলে দেয়। C কোর্সের dynamic memory পর্বে দেখবেন হাতে মেমরি সামলানো কতটা সাবধানতা চায় — এই পর্ব পড়ার পর পার্থক্যটা স্পষ্ট হবে।
৪ · std::unique_ptr — একজনই মালিক
কখনো কখনো সত্যিই একটি একক object heap-এ রাখতে হয় — যেমন object-টি অনেক বড়, বা তার আসল টাইপ চলার সময় ঠিক হয় (পর্ব ২৫-এর polymorphism), বা সেটি থাকতেও পারে, না-ও থাকতে পারে। তখন new-এর বদলে আধুনিক C++-এর উত্তর একটি smart pointer — pointer-এর মতো ব্যবহার করা যায় এমন একটি RAII object, যে নিজের destructor-এ heap-এর object মুছে দেয়। সবচেয়ে সরল আর সবচেয়ে বেশি ব্যবহৃতটি হলো std::unique_ptr (<memory> header-এ)।
এর পেছনের ধারণাটির নাম ownership বা মালিকানা: কে এই রিসোর্স ফেরত দেওয়ার দায়িত্বে? unique_ptr-এর উত্তর — ঠিক একজন। একটি জমির দলিল যেমন একজনের নামে থাকে; বিক্রি করলে দলিল হাতবদল হয়, কিন্তু দুজনের হাতে একই দলিলের দুটি আসল কপি থাকতে পারে না।
#include <memory>
auto order = std::make_unique<Order>(1042); // heap-এ Order, মালিক order
order->charge(); // pointer-এর মতোই ব্যবহার
Order& o = *order; // * দিয়ে আসল object
// scope শেষে order-এর destructor → Order মুছে যায়। delete লাগে না।std::make_unique (C++14 থেকে) heap-এ object বানিয়ে সরাসরি একটি unique_ptr ফেরত দেয়, তাই কোডে new শব্দটিই আসে না। আকারে আর গতিতে ডিফল্ট unique_ptr একটি সাধারণ raw pointer-এর মতোই — বাস্তব compiler-এ এর আকার একটি pointer-এর সমান, আর ব্যবহারে বাড়তি খরচ প্রায় শূন্য। নিরাপত্তা পাচ্ছেন, দাম দিচ্ছেন না।
কপি নিষেধ, move চলে
“একজনই মালিক” নিয়মটা compiler জোর করে চালায়। unique_ptr কপি করার চেষ্টা করলে compile error — কারণ কপি হলে দুজন মালিক হতো, আর দুজনেই একদিন delete করত (double delete, undefined behavior). কিন্তু মালিকানা হাতবদল করা যায়, std::move দিয়ে। std::move নিজে কিছু সরায় না; সে শুধু বলে “এই object-এর ভেতরের জিনিস নিয়ে যাওয়া যাবে” — বাকি গল্প পর্ব ৩৬-এ। এমন টাইপ, যা কপি হয় না কিন্তু move হয়, তাকে বলে move-only type. একটি অর্ডারের পার্সেল গুদাম থেকে কুরিয়ারের হাতে যাওয়ার পথ ধাপে ধাপে দেখুন:
1void ship(std::unique_ptr<Order> o) { /* ... */ } // o মালিক হবে2auto order = std::make_unique<Order>(1042);3auto copy = order; // error4auto courier = std::move(order);5if (!order) std::println("order is empty");6ship(std::move(courier));7// ship() শেষ — o-এর scope শেষ
| order | |
| courier | |
| ship-এর o | |
| Order #1042 @7000 |
- একজন মালিক।
make_uniqueheap-এ অর্ডার #1042 বানাল (ধরুন ঠিকানা 7000).orderতার একমাত্র মালিক — দলিল একজনের নামে। - কপির চেষ্টা — compiler আটকায়।
auto copy = order;লিখলে compile error. unique_ptr-এর copy constructor ইচ্ছা করে “deleted” রাখা হয়েছে। এই error-টাই আপনার বন্ধু: দুজন মালিকের সম্ভাবনা চলার আগেই ধরা পড়ল। - Move — দলিল হাতবদল।
std::move(order)দিয়ে মালিকানাcourier-এর কাছে গেল। heap-এর অর্ডারটি একটুও নড়েনি, কপিও হয়নি — শুধু ঠিকানাটা এক হাত থেকে আরেক হাতে গেছে, আর পুরোনো হাত খালি। - পুরোনো মালিক খালি। move-এর পর
orderহয়ে যায় nullptr. এটা standard-এর নিশ্চয়তা: সরানো-হয়ে-যাওয়া unique_ptr সবসময় খালি। তাইif (!order)সত্য। (move-এর পরorder->charge()লিখলে খালি pointer ব্যবহার — undefined behavior. move করা জিনিস আর ছোঁবেন না।) - ফাংশনকে মালিকানা দেওয়া।
shipতার প্যারামিটার unique_ptr by value নেয়। তাই ডাকার সময়std::moveলাগে, আর মালিকানা চলে যায় ফাংশনেরo-তে। ডাকার লাইনেstd::moveদেখেই পাঠক বোঝেন: “এই জিনিস এখন থেকে আর আমার নয়।” - শেষ মালিকের সাথে object-ও শেষ।
shipশেষ হলেo-এর scope শেষ, তার destructor অর্ডারটি মুছে দেয়। পুরো যাত্রায় তিনজন হাত বদলেছে, কিন্তু delete হয়েছে ঠিক একবার — ঠিক জায়গায়, ঠিক সময়ে। double delete বা leak — কোনোটিরই সুযোগ নেই।
বাস্তব উদাহরণ — Order আর তার Item
একটি অর্ডারে কয়েকটি পণ্য (Item) আছে, আর অর্ডার মুছলে পণ্যের তালিকাও মুছে যাওয়া উচিত — এটি পরিষ্কার “একজন মালিক” সম্পর্ক। সবচেয়ে সরল রূপ std::vector<Item> — এখানে vector নিজেই মালিক, smart pointer লাগেই না। কিন্তু Item যদি নানা ধরনের হয় (ডিজিটাল পণ্য, ভারী পণ্য, ফ্রিজে রাখার পণ্য — পর্ব ২৫-এ দেখবেন কেন তখন pointer লাগে), তখন:
struct Order {
int id;
std::vector<std::unique_ptr<Item>> items; // প্রতিটি Item-এর একমাত্র মালিক Order
};
order.items.push_back(std::make_unique<DigitalItem>("e-book"));
// Order ধ্বংস → vector ধ্বংস → প্রতিটি unique_ptr ধ্বংস → প্রতিটি Item মুছে যায়খেয়াল করুন, Order-এ কোনো destructor লিখতে হয়নি। vector জানে তার উপাদানদের ধ্বংস করতে, আর প্রতিটি unique_ptr জানে তার Item মুছতে। RAII object-এর ভেতরে RAII object — দায়িত্ব নিজে থেকে শিকলের মতো নেমে যায়।
৫ · unique_ptr ফাংশনে পাঠানো — মালিকানা দেবেন, না শুধু ব্যবহার করতে দেবেন?
নতুনদের সবচেয়ে বড় বিভ্রান্তি এখানেই: “আমার কাছে unique_ptr আছে, তাই ফাংশনও unique_ptr নেবে।” না। প্রশ্ন একটাই — ফাংশনটি কি মালিকানা নিয়ে কিছু করবে, নাকি শুধু object-টি ব্যবহার করবে? বেশিরভাগ ফাংশন শুধু ব্যবহার করে, আর তাদের smart pointer-এর কথা জানারই দরকার নেই।
| ফাংশনের স্বাক্ষর | যা বোঝায় | ডাকার সময় |
|---|---|---|
void print(const Order& o) | শুধু পড়ব; object অবশ্যই আছে | print(*order) |
void update(Order& o) | বদলাব, কিন্তু মালিক হব না | update(*order) |
void maybeShow(const Order* o) | শুধু দেখব; object না-ও থাকতে পারে (nullptr) | maybeShow(order.get()) |
void ship(std::unique_ptr<Order> o) | মালিকানা নিয়ে নেব (sink) | ship(std::move(order)) |
std::unique_ptr<Order> makeOrder(int id) | নতুন বানিয়ে মালিকানা আপনাকে দিচ্ছি (factory) | auto o = makeOrder(7); |
void replace(std::unique_ptr<Order>& o) | আপনার pointer-টিকেই বদলে দিতে পারি (বিরল) | replace(order) |
প্রথম তিনটি সারি সবচেয়ে বেশি দরকার পড়ে। print(const Order&) ফাংশনটি এখন যেকোনো Order নিতে পারে — stack-এর হোক, vector-এর ভেতরের হোক, unique_ptr-এর হোক, shared_ptr-এর হোক। এটি পর্ব ১০-এর reference-এর পুরোনো শিক্ষা: শুধু ব্যবহারের জন্য T& বা const T&. আর get() ফেরত দেয় ভেতরের raw pointer — শুধু দেখার জন্য, মালিকানা ছাড়া। আধুনিক C++-এ raw pointer মানে “আমি মালিক নই” — তাই get()-এর pointer কখনো delete করবেন না।
Smart pointer প্যারামিটারে আসে শুধু তখন, যখন ফাংশনটি মালিকানা নিয়ে কিছু করে — নিয়ে নেয়, ভাগ নেয়, বা বদলায়। বাকি সব ক্ষেত্রে T&, const T& বা T*. এতে ফাংশন সরল হয়, আর ডাকার মানুষকে নির্দিষ্ট কোনো smart pointer-এ বাধ্য করা হয় না।
৬ · std::shared_ptr — ভাগাভাগির মালিকানা
কিছু ক্ষেত্রে একজন মালিক যথেষ্ট নয়। ধরুন একটি অনলাইন অফিস অ্যাপে একটি ডকুমেন্ট দুজন সম্পাদক একসাথে খুলেছেন — রিনা আর সাকিব। দুজনের জানালাই ডকুমেন্টটি ধরে আছে। রিনা জানালা বন্ধ করলে ডকুমেন্ট মোছা যাবে না, কারণ সাকিব এখনো কাজ করছেন। কে আগে বন্ধ করবেন, আগে থেকে জানা নেই। শেষজন বন্ধ করলেই ডকুমেন্টটি মেমরি থেকে সরানো উচিত।
এর জন্য std::shared_ptr. এটি reference counting ব্যবহার করে: পেছনে একটি সংখ্যা রাখে — এই মুহূর্তে কতজন shared_ptr এই object-এর মালিক। shared_ptr কপি করলে সংখ্যা বাড়ে, একটি ধ্বংস হলে কমে, আর শূন্যে নামলে object মুছে যায়। সংখ্যাটি দেখা যায় use_count() দিয়ে। পুরো জীবনচক্রটি দেখুন:
1auto doc = std::make_shared<Doc>("report");2{ auto sakib = doc; // কপি3 std::println("{}", doc.use_count());4} // sakib ধ্বংস5doc.reset(); // শেষ মালিক চলে গেল
| doc | |
| sakib | |
| strong count | |
| Doc "report" |
- জন্ম — গোনা 1. std::make_shared heap-এ একবারেই দুটি জিনিস বানায়: Doc object নিজে, আর তার পাশে একটি লুকানো হিসাবের খাতা — control block. খাতায় লেখা “মালিক: 1”. রিনার জানালা (
doc) প্রথম মালিক। - কপি — গোনা 2.
auto sakib = doc;unique_ptr-এ যা নিষেধ ছিল, shared_ptr-এ তা-ই স্বাভাবিক। কপি মানে নতুন ডকুমেন্ট নয় — একই ডকুমেন্টের দ্বিতীয় মালিক। খাতার সংখ্যা 2 হলো। - দেখা।
use_count()প্রিন্ট করে 2. এই সংখ্যা বোঝা আর ডিবাগের জন্য চমৎকার, কিন্তু একাধিক thread-এর প্রোগ্রামে এর ওপর ভিত্তি করে সিদ্ধান্ত নেবেন না — আপনি পড়ার পরমুহূর্তেই অন্য thread সেটি বদলে দিতে পারে। - একজন চলে গেল — গোনা 1.
}-এsakib-এর scope শেষ, তার destructor খাতার সংখ্যা 1 কমাল। Doc কিন্তু মোছেনি — রিনা এখনো মালিক। আউটপুটেcloseআসেনি। - শেষ মালিক — গোনা 0, object মুছে যায়।
doc.reset()শেষ মালিকানাও ছেড়ে দিল। সংখ্যা শূন্য হতেই shared_ptr Doc-এর destructor চালায় (close report) আর মেমরি ফেরত দেয়। কে শেষে ছাড়বে, তা আগে থেকে না জানলেও কাজ ঠিক মুহূর্তে, ঠিক একবার হয়েছে।
shared_ptr-এর দাম
এই সুবিধা বিনামূল্যে আসে না, আর সেটা জানা জরুরি:
- আকার দ্বিগুণ। একটি shared_ptr বাস্তব compiler-গুলোতে সাধারণত দুটি pointer-এর সমান — একটি object-এর দিকে, একটি control block-এর দিকে।
- বাড়তি বরাদ্দ। control block-এর জন্য heap-এ জায়গা লাগে।
make_sharedobject আর control block একবারে বরাদ্দ করে এটা কমায়;std::shared_ptr<Doc>(new Doc(...))লিখলে দুবার বরাদ্দ হয়। - প্রতিটি কপিতে atomic গোনা। যেকোনো thread থেকে নিরাপদে কপি করা যায় বলে সংখ্যাটি বাড়ানো-কমানো হয় atomic নির্দেশ দিয়ে — সাধারণ যোগের চেয়ে বেশ ধীর, বিশেষ করে অনেক CPU core একই সংখ্যায় হাত দিলে। তাই shared_ptr অকারণে by value পাঠালে প্রতিটি ডাকেই এই খরচ।
- কে কখন মোছে, তা অস্পষ্ট। unique_ptr-এ এক নজরে দেখা যায় object কোথায় মরবে। shared_ptr-এ object বাঁচে “যতক্ষণ কেউ ধরে আছে” — আর বড় প্রোগ্রামে সেই “কেউ” খুঁজে পাওয়া কঠিন।
“মালিক কে হবে বুঝছি না, তাই shared_ptr দিই” — এটি একটি পরিচিত ভুল সিদ্ধান্ত। shared_ptr মালিকানার প্রশ্নের উত্তর নয়, প্রশ্নটা এড়িয়ে যাওয়া। আগে ভাবুন: আসলেই কি একাধিক অংশ স্বাধীনভাবে object-টিকে বাঁচিয়ে রাখতে চায়, আর কে শেষে ছাড়বে তা আগে থেকে জানা অসম্ভব? উত্তর “হ্যাঁ” হলে shared_ptr. অন্যথায় unique_ptr — আর বাকিরা reference বা raw pointer দিয়ে শুধু ব্যবহার করুক। প্রয়োজন হলে unique_ptr পরে সহজেই shared_ptr-এ রূপান্তর করা যায় (std::shared_ptr<T> s = std::move(u);), উল্টোটা যায় না।
এবার দুই ধরনের মালিকানা একসাথে চালিয়ে দেখুন। destructor-এ close প্রিন্ট হয়, তাই আপনি দেখবেন কোন object ঠিক কখন মুছছে:
#include <memory>
#include <print>
#include <string>
#include <utility>
struct Doc {
std::string name;
explicit Doc(const std::string& n) : name(n) { std::println("open {}", name); }
~Doc() { std::println("close {}", name); }
};
int main() {
auto a = std::make_unique<Doc>("invoice");
auto b = std::move(a); // malikana a theke b-te
std::println("a empty? {}", a == nullptr);
auto s1 = std::make_shared<Doc>("report");
{ auto s2 = s1; std::println("count = {}", s1.use_count()); }
std::println("count = {}", s1.use_count());
std::weak_ptr<Doc> w = s1; // dekhe, malik hoy na
s1.reset(); // count 0 -> close report
std::println("expired? {}", w.expired());
}আউটপুট: open invoice, a empty? true, open report, count = 2, count = 1, close report, expired? true, আর একদম শেষে close invoice. invoice-এর একমাত্র মালিক move-এর পর b, তাই a খালি; আর b main-এর শেষে ধ্বংস হয় বলে close invoice সবার শেষে। report-এর গোনা ভেতরের ব্লকে 2, ব্লক শেষে 1, আর reset()-এ 0 — ঠিক তখনই close report. শেষ লাইনের weak_ptr গোনা বাড়ায়নি (তাই গোনা 1 থেকেই 0-তে নেমেছে), কিন্তু জানে object আর নেই: expired? true. এই weak_ptr কী আর কেন, পরের অংশে।
৭ · std::weak_ptr — দেখে, কিন্তু মালিক হয় না
shared_ptr-এর একটি অন্ধ জায়গা আছে। ধরুন একটি ই-কমার্স সাইটের মেনুতে ক্যাটাগরি গাছের মতো সাজানো: “ইলেকট্রনিক্স” (parent)-এর নিচে “মোবাইল” (child). parent তার child-কে ধরে রাখে, আর child-কেও জানতে হয় তার parent কে (“পেছনে যান” বোতামের জন্য)। দুই দিকেই shared_ptr দিলে কী হয়?
local p ──> [ Parent count = 1 ] local c ──> [ Child count = 1 ]
local p ──> [ Parent count = 1 ]
│ child (shared_ptr)
▼
local c ──> [ Child count = 2 ]
local p ──> [ Parent count = 2 ] ◄──┐
│ child │ parent (shared_ptr)
▼ │
local c ──> [ Child count = 2 ] ──┘
local p (নেই) [ Parent count = 1 ] ◄──┐ │ child │ parent ▼ │ local c (নেই) [ Child count = 1 ] ──┘ বাইরের কেউ এদের ছুঁতে পারে না, destructor কখনো চলবে না → memory leak
local p (নেই) [ Parent count = 0 ] → মুছে গেল │ child ┊ parent (weak — গোনে না) ▼ ┊ local c (নেই) [ Child count = 0 ] → তারপর এটিও মুছে গেল
- দুটি আলাদা object. একটি Parent আর একটি Child, প্রত্যেককে একটি করে local shared_ptr ধরে আছে। দুজনেরই গোনা 1.
- Parent → Child. parent তার child-এর দিকে একটি shared_ptr রাখল। Child-এর এখন দুই মালিক: local
cআর Parent-এর ভেতরেরchild. গোনা 2. - Child → Parent: চক্র তৈরি। child-ও parent-এর দিকে shared_ptr রাখল। এখন Parent-এর গোনাও 2. দুজন একে অপরকে ধরে আছে — যেন দুজন মানুষ একে অপরের হাত ধরে আছেন আর বলছেন, “আগে আপনি ছাড়ুন।”
- Scope শেষ — leak. local
pআরcধ্বংস হলো, দুটি গোনাই 1 কমে 1-এ থামল। শূন্য না হওয়ায় কেউ মোছে না। অথচ প্রোগ্রামের আর কোনো অংশ এই দুটি object-এ পৌঁছাতে পারে না। এই অবস্থার নাম reference cycle. destructor-এ প্রিন্ট রাখলে দেখবেন, একটি লাইনও আসে না। - সমাধান — পেছনের তীর weak. Child-এর
parent-কে std::weak_ptr করুন। weak_ptr object-কে দেখে, কিন্তু মালিকের সংখ্যায় গোনা হয় না। তাই scope শেষে Parent-এর গোনা 0 — Parent মোছে, সাথে তারchildসদস্যও ধ্বংস হয়, ফলে Child-এর গোনাও 0 — Child-ও মোছে। নিয়ম: মালিকানা যায় এক দিকে (parent → child), পেছনের দিকের সম্পর্ক weak.
weak_ptr দিয়ে সরাসরি object ব্যবহার করা যায় না, কারণ সে জানে না object এখনো বেঁচে আছে কি না। ব্যবহারের আগে lock() ডাকতে হয়: object থাকলে সে একটি নতুন shared_ptr ফেরত দেয় (ব্যবহারের সময়টুকু object বাঁচিয়ে রাখতে), না থাকলে খালি shared_ptr.
struct Child {
std::weak_ptr<Parent> parent; // দেখে, মালিক নয়
void goBack() {
if (auto p = parent.lock()) p->show(); // বেঁচে থাকলে ব্যবহার
else std::println("parent আর নেই");
}
};বাস্তব উদাহরণ — যে cache শুধু নজর রাখে
একটি ছবি-দেখার অ্যাপ বড় ছবিগুলো মেমরিতে রাখে। একই ছবি দুটি জানালায় খোলা থাকলে দুবার লোড করার মানে নেই — তাই একটি cache থাকে, যা নাম দিয়ে ছবি খুঁজে দেয়। কিন্তু cache যদি shared_ptr দিয়ে ছবিগুলো ধরে রাখে, তাহলে কেউ না দেখলেও কোনো ছবি কখনো মুছবে না — cache নিজেই মেমরি খেয়ে ফেলবে। সমাধান: cache রাখবে weak_ptr.
std::unordered_map<std::string, std::weak_ptr<Image>> cache; // পর্ব ৩১-এ map
std::shared_ptr<Image> getImage(const std::string& name) {
if (auto img = cache[name].lock()) return img; // কেউ এখনো দেখছে — একই ছবি
auto img = std::make_shared<Image>(name); // নেই — নতুন করে লোড
cache[name] = img; // cache শুধু নজর রাখে
return img;
}যতক্ষণ কোনো জানালা ছবিটি ধরে আছে, cache একই ছবি ফেরত দেয়। সব জানালা বন্ধ হলে ছবি মুছে যায়, আর cache-এর weak_ptr নিজে থেকেই “expired” হয়ে যায় — পরের বার নতুন করে লোড হবে। মালিক জানালাগুলো; cache শুধু খাতায় নাম লিখে রাখে।
একটি সূক্ষ্ম বিষয়: weak_ptr নিজেও control block-এ গোনা হয় (আলাদা একটি “weak count”-এ)। object মোছে strong count শূন্য হলে, কিন্তু control block থাকে যতক্ষণ একটি weak_ptr-ও বাকি। make_shared object আর control block একসাথে বরাদ্দ করে বলে, বড় object-এর ক্ষেত্রে দীর্ঘজীবী weak_ptr থাকলে object-এর পুরো জায়গাটাই ততক্ষণ ফেরত যায় না (destructor কিন্তু ঠিক সময়েই চলে)। সাধারণ কোডে এটা নিয়ে চিন্তার দরকার নেই, তবে বিশাল object-এর cache-এ মাথায় রাখবেন।
৮ · Custom deleter আর মালিকানার নিয়ম-সারাংশ
অনেক পুরোনো বা C লাইব্রেরি — ফাইলের FILE*, ডেটাবেস, গ্রাফিক্স — নিজের “খোলা” আর “বন্ধ” ফাংশন দেয়। এদের ফেরত delete দিয়ে হয় না। unique_ptr-কে বলে দেওয়া যায় কীভাবে ফেরত দিতে হবে — একে বলে custom deleter:
struct FileCloser {
void operator()(std::FILE* f) const { std::fclose(f); } // delete-এর বদলে fclose
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
FilePtr log{std::fopen("sales.log", "a")};
if (log) std::fputs("new sale\n", log.get());
// scope শেষে নিজে থেকেই fclose — আর fopen ব্যর্থ হলে (nullptr) কিছুই ডাকা হয় নাখালি (nullptr) unique_ptr-এর জন্য deleter ডাকা হয় না, তাই fclose(nullptr)-এর বিপদও নেই। এভাবে যেকোনো C API-কে দুই লাইনে RAII বানানো যায়। shared_ptr-ও deleter নেয় (std::shared_ptr<std::FILE> f(std::fopen(...), FileCloser{});), পার্থক্য হলো সেখানে deleter টাইপের অংশ নয়, control block-এ জমা থাকে।
মালিকানার নিয়ম — এক পাতায়
| পরিস্থিতি | যা ব্যবহার করবেন |
|---|---|
| সাধারণ মান, বা অনেক উপাদান | সরাসরি object (stack-এ), std::vector, std::string — pointer-ই নয় |
| heap-এ একটি object, মালিক একজন | std::unique_ptr + std::make_unique |
| সত্যিকারের ভাগাভাগি, কে শেষে ছাড়বে অজানা | std::shared_ptr + std::make_shared |
| shared_ptr-এর object দেখা, মালিক না হয়ে (cycle, cache) | std::weak_ptr |
| শুধু ব্যবহার, object অবশ্যই আছে | T& / const T& |
| শুধু ব্যবহার, object না-ও থাকতে পারে | T* (মালিক নয়) — বা পর্ব ৩৮-এর std::optional |
| C লাইব্রেরির রিসোর্স | std::unique_ptr + custom deleter |
কোডে new / delete | প্রায় কখনো নয় — দেখলে প্রশ্ন করুন |
এর ফল একটি সুন্দর নকশা-নীতি, যাকে বলে Rule of Zero: আপনার নিজের class-এ destructor, copy বা move নিজে লিখবেন না; রিসোর্স রাখুন vector, string, unique_ptr-এর মতো RAII সদস্যে — তারা নিজেরাই সব ঠিকঠাক করবে। উপরের Order struct ঠিক তাই করেছে। কেন এটি “শূন্যের নিয়ম”, আর কখন ৩ বা ৫টি ফাংশন নিজে লিখতে হয়, তা পর্ব ২২-এ।
C++-এর প্রথম smart pointer ছিল C++98-এর std::auto_ptr. তখন ভাষায় move ছিল না, তাই auto_ptr “কপি”-র ভান করে গোপনে মালিকানা সরিয়ে দিত — b = a; লিখলে a খালি হয়ে যেত! এই অদ্ভুত আচরণ অনেক বাগের জন্ম দিয়েছে। C++11-এ move semantics আসার পর তার বদলে এল unique_ptr, যেখানে হাতবদল করতে স্পষ্ট std::move লিখতে হয়। auto_ptr C++11-এ deprecated হয়, আর C++17-এ standard থেকে পুরোপুরি সরিয়ে ফেলা হয়। shared_ptr আর weak_ptr এসেছিল জনপ্রিয় Boost লাইব্রেরি থেকে, TR1 হয়ে C++11-এ। আর make_unique C++11-এ বাদ পড়ে গিয়েছিল — সেটি যোগ হয় C++14-এ।
- RAII কী? — রিসোর্স constructor-এ নেওয়া আর destructor-এ ফেরত দেওয়া, ফলে রিসোর্সের আয়ু object-এর আয়ুর সাথে বাঁধা; scope শেষে বা exception-এও নিজে থেকে ফেরত যায়।
- unique_ptr কপি করা যায় না কেন, হাতবদল কীভাবে? — কপি হলে দুজন মালিক, দুবার delete; তাই copy নিষিদ্ধ,
std::moveদিয়ে move করা যায় আর পুরোনোটি nullptr হয়। - shared_ptr কখন object মোছে? — যখন শেষ shared_ptr মালিক ধ্বংস হয় বা reset হয়, অর্থাৎ strong count শূন্য হলে।
- weak_ptr কেন দরকার? — shared_ptr-এর reference cycle ভাঙতে আর মালিক না হয়ে নজর রাখতে; সে গোনা বাড়ায় না, ব্যবহারে
lock()লাগে। make_sharedকেনnew-এর চেয়ে ভালো? — একবার বরাদ্দ (object + control block), কোডে খোলা new নেই, আর exception-এ leak-এর ঝুঁকি কম।
৯ · AI-এর যুগে — smart pointer-এর ভুল ধরা
AI টুল smart pointer ব্যবহার করে ঠিকই, কিন্তু প্রায়ই ভুল জায়গায় বা ভুল ভাবে। কারণ অনলাইনে প্রচুর কোড আছে যেখানে “নিরাপদ থাকতে সবখানে shared_ptr” লেখা হয়েছে, বা C++11-এর আগের রীতিতে লেখা। এই কোড compile হয়, চলেও — সমস্যা দেখা দেয় ধীরগতি, leak বা অস্পষ্ট নকশা হিসেবে। সাধারণ ভুলগুলো:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
সবখানে std::shared_ptr | মালিকানা অস্পষ্ট, বাড়তি বরাদ্দ ও atomic খরচ, cycle-এর ঝুঁকি | ডিফল্ট std::unique_ptr; shared শুধু সত্যিকারের ভাগাভাগিতে |
std::shared_ptr<T> p(new T); | দুবার বরাদ্দ; কোডে খোলা new | auto p = std::make_shared<T>(); / std::make_unique |
void print(std::shared_ptr<Item> p) | প্রতি ডাকে atomic গোনা বাড়ানো-কমানো, অকারণে মালিকানা দাবি | void print(const Item& item) |
| parent আর child দুজনেই একে অপরকে shared_ptr দিয়ে ধরে | reference cycle — destructor কখনো চলে না, leak | পেছনের দিকে std::weak_ptr |
p.get()-এর pointer দিয়ে নতুন smart pointer বা delete | দুজন মালিক — double delete | get() শুধু দেখার জন্য |
| move-এর পরেও পুরোনো unique_ptr ব্যবহার | খালি pointer — undefined behavior | move করা জিনিস আর ছোঁবেন না |
অনেক উপাদানের জন্য std::unique_ptr<int[]> বা new int[n] | আকার জানা নেই, ভুলের সুযোগ বেশি | std::vector<int> |
AI-কে কোড লিখতে বলার সময় মালিকানার নিয়ম আগেই দিয়ে দিন, আর প্রতিটি pointer-এর মালিক কে, তা ব্যাখ্যা করতে বলুন:
AI-এর কোডে মালিকানা যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১“RAII” নামটি কোথা থেকে এল, নামটি নিয়ে আপত্তি কেন, আর কেন একে C++-এর রিসোর্স-নিরাপত্তার মূল ভিত্তি বলা হয়?
নামটি Bjarne Stroustrup-এর। ১৯৮০-র দশকের শেষভাগে C++-এ exception handling নকশা করার সময় তিনি একটি কঠিন প্রশ্নের মুখোমুখি হন: exception উড়লে ফাংশনের মাঝপথ থেকে বেরিয়ে যেতে হয়, তাহলে মাঝপথে নেওয়া রিসোর্সগুলোর কী হবে? Java-র মতো ভাষা পরে এর উত্তর দিয়েছে finally ব্লক দিয়ে — প্রতিটি জায়গায় হাতে পরিষ্কারের কোড লেখা। Stroustrup অন্য পথ নেন: C++-এ তো আগে থেকেই destructor আছে, যা object-এর জীবন শেষে নিশ্চিতভাবে চলে। তাহলে রিসোর্সকে একটি object-এর ভেতরে রাখলেই exception-এর পথেও পরিষ্কার নিশ্চিত। নামটি — “Resource Acquisition Is Initialization” — এই ভাবনার অর্ধেক বলে: রিসোর্স নেওয়া মানে একটি object-কে initialize করা।
আপত্তি হলো, নামটি আসল গুরুত্বপূর্ণ অর্ধেকটা বলে না — ফেরত দেওয়া হয় destructor-এ। অনেকে, এমনকি Stroustrup নিজেও, স্বীকার করেছেন নামটি জুতসই হয়নি। বিকল্প নাম হিসেবে কেউ বলেন SBRM (Scope-Bound Resource Management), কেউ বলেন “constructor নেয়, destructor ছাড়ে”। কিন্তু নামটি এতদিনে এতটাই চালু যে বদলানো কঠিন।
কেন এটি মূল ভিত্তি? কারণ C++-এর প্রায় প্রতিটি নিরাপত্তা-নিশ্চয়তা এর ওপর দাঁড়ানো। exception-safety-র যে তিন স্তরের ধারণা (basic, strong, nothrow guarantee — Dave Abrahams-এর নামের সাথে জড়িত) standard library নকশায় ব্যবহৃত হয়েছে, তা RAII ছাড়া প্রায় অসম্ভব। vector, string, fstream, lock_guard, unique_ptr, thread — সবই RAII. আর সবচেয়ে বড় কথা, RAII রিসোর্স-ভেদে আলাদা কিছু চায় না: মেমরি, ফাইল, lock, নেটওয়ার্ক সংযোগ — সবকিছুর জন্য একই নিয়ম। garbage collector শুধু মেমরি সামলায়; ফাইল বা lock-এর জন্য সেই ভাষাগুলোকে আলাদা ব্যবস্থা (try-with-resources, using, with) আনতে হয়েছে।
আর একটি সূক্ষ্ম লাভ: RAII class একবার লিখলে তার ব্যবহারকারীকে আর কিছু মনে রাখতে হয় না। ভুল করার দায়িত্ব হাজার জায়গা থেকে সরে এক জায়গায় — class-এর destructor-এ — চলে আসে, যেখানে সেটি একবার ভালো করে পরীক্ষা করা যায়।
মূল উপলব্ধি: RAII শুধু একটি কৌশল নয়, C++-এর মানসিকতা — “পরিষ্কারের কাজ মনে রাখার ওপর ছেড়ে দিও না, ভাষার নিশ্চয়তার ওপর বেঁধে দাও।” আধুনিক C++ শেখা মানে আসলে এই একটি অভ্যাস গড়ে তোলা।
প্র ০২std::auto_ptr কেন ব্যর্থ হলো, আর কেন C++17-এ সেটিকে ভাষা থেকে পুরোপুরি সরিয়ে ফেলা হলো — যদিও C++ সাধারণত পুরোনো কোড ভাঙতে চায় না?
C++98-এ auto_ptr-এর লক্ষ্য ছিল ঠিক unique_ptr-এর মতোই: একজন মালিক, scope শেষে নিজে থেকে delete. সমস্যা ছিল সময়ের — তখন ভাষায় move semantics ছিল না। “মালিকানা হাতবদল” প্রকাশ করার একমাত্র উপায় ছিল copy constructor আর copy assignment. তাই auto_ptr-এর “কপি” আসলে কপি ছিল না: b = a; লিখলে মালিকানা b-তে চলে যেত আর a চুপচাপ খালি হয়ে যেত।
এর ফল ছিল ভয়ংকর রকম অপ্রত্যাশিত। একটি ফাংশনকে void show(std::auto_ptr<Item> p) দিয়ে by value পাঠালে ফাংশন object-টি নিয়ে নিত আর শেষে মুছে দিত — ডাকার মানুষের হাতে থাকত খালি pointer, কোনো সতর্কতা ছাড়াই। আরও খারাপ, container আর algorithm কপিকে “আসলটির মতো আরেকটি” ধরে নেয়। sort-এর মতো algorithm উপাদান কপি করে একটি অস্থায়ী ভ্যারিয়েবলে রাখলে মূল উপাদান খালি হয়ে যেত। তাই standard নিজেই auto_ptr-কে container-এ রাখা নিষেধ করেছিল — একটি smart pointer, যা vector-এ রাখা যায় না!
C++11 rvalue reference আর move semantics আনার পর সমস্যার আসল সমাধান সম্ভব হলো। unique_ptr-এ copy সরাসরি নিষিদ্ধ (deleted), আর হাতবদল করতে লিখতে হয় std::move — চোখে দেখা যায় এমন একটি শব্দ। temporary থেকে move নিজে থেকেই হয়, কারণ সেটির আর কেউ নেই। vector-এ unique_ptr রাখা যায়, কারণ vector এখন move জানে। একই সাথে auto_ptr-কে deprecated ঘোষণা করা হলো।
C++ সাধারণত পুরোনো কোড ভাঙে না, তাই C++17-এ auto_ptr সরানো একটি বিরল সিদ্ধান্ত ছিল। যুক্তি ছিল: একটি নিরাপদ ও প্রায় হুবহু বিকল্প আছে (বেশিরভাগ ক্ষেত্রে নাম বদলে আর কয়েক জায়গায় std::move বসালেই চলে), আর auto_ptr রেখে দেওয়া মানে নতুন প্রোগ্রামারদের হাতে একটি ফাঁদ রেখে দেওয়া। একই সময়ে আরও কিছু পুরোনো, বিপজ্জনক অংশ সরানো হয়, যেমন std::random_shuffle. অনেক compiler এখনো পুরোনো -std দিয়ে auto_ptr চালায়, কিন্তু C++17 বা পরের standard-এ এটি আর নেই।
মূল উপলব্ধি: ভালো নকশার জন্য সঠিক ভিত্তি লাগে। মালিকানা হাতবদল একটি আলাদা ক্রিয়া, কপির ছদ্মবেশে লুকানো উচিত নয় — move semantics আসার পরেই C++ সেটা ঠিকভাবে বলতে পেরেছে। আর API-তে “আশ্চর্য” আচরণ যত কম, বাগ তত কম।
প্র ০৩shared_ptr-এর control block-এ আসলে কী থাকে, গোনা কেন atomic, আর এর খরচ বাস্তবে কোথায় টের পাওয়া যায়?
প্রতিটি shared_ptr-পরিবার (একই object-এর সব shared_ptr আর weak_ptr) একটি control block ভাগ করে। standard ঠিক বিন্যাস বলে দেয় না, তবে বাস্তব লাইব্রেরিগুলোতে এতে থাকে: strong count (কতজন shared_ptr মালিক), weak count (কতজন weak_ptr দেখছে), object মোছার উপায় (deleter, যা টাইপের অংশ নয় বলে এখানে রাখতে হয়), আর প্রয়োজনে allocator. strong count শূন্য হলে object-এর destructor চলে; weak count-ও শূন্য হলে control block নিজে মোছে।
গোনা atomic কেন? standard নিশ্চয়তা দেয় যে আলাদা আলাদা shared_ptr কপি — যারা একই object-এর মালিক — বিভিন্ন thread থেকে একসাথে বানানো বা ধ্বংস করা নিরাপদ। ভাবুন একটি সার্ভার, যেখানে দশটি thread একই কনফিগারেশন object-এর shared_ptr কপি করছে। গোনা সাধারণ ++ দিয়ে হলে দুই thread একসাথে পড়ে-লিখে একটি বৃদ্ধি হারিয়ে ফেলতে পারত — তখন object আগেভাগে মুছে যেত, বা কখনো মুছত না। তাই x86-এ সাধারণত lock-যুক্ত নির্দেশ ব্যবহার হয়, যা সাধারণ যোগের চেয়ে বেশ ধীর। আরও বড় খরচ আসে যখন অনেক CPU core একই গোনায় বারবার হাত দেয়: সেই মেমরির টুকরোটি (cache line) core থেকে core-এ ছোটাছুটি করে। (খেয়াল রাখুন: atomic শুধু গোনা; একই shared_ptr ভ্যারিয়েবলকে দুই thread থেকে একসাথে বদলানো বা object-এর ডেটা বদলানো নিরাপদ নয় — এর জন্য C++20-এ std::atomic<std::shared_ptr<T>> আছে।)
বাস্তবে খরচ টের পাওয়া যায় তিন জায়গায়। (১) hot loop-এ shared_ptr by value পাঠালে — প্রতিটি ডাকে একটি atomic বৃদ্ধি আর একটি হ্রাস। এজন্যই নিয়ম: শুধু ব্যবহার করলে const T&. (২) লাখ লাখ ছোট object-এ — প্রতিটির আলাদা control block মেমরি আর cache নষ্ট করে। (৩) বিশাল object-গ্রাফ মোছার সময় — শেষ মালিক চলে গেলে এক শৃঙ্খলে হাজার হাজার object মুছতে পারে, যা একটি অপ্রত্যাশিত “বিরতি” তৈরি করে, অনেকটা garbage collector-এর মতো।
একটি বাড়তি সতর্কতা: shared_ptr প্রোগ্রামের যেকোনো জায়গা থেকে object বাঁচিয়ে রাখতে পারে, তাই অভিজ্ঞ প্রকৌশলীরা (যেমন Sean Parent) সতর্ক করেছেন যে অবাধে ছড়ানো shared_ptr প্রায় global ভ্যারিয়েবলের মতোই যুক্তি বোঝা কঠিন করে তোলে। C++ Core Guidelines-ও বলে, shared_ptr-এর চেয়ে unique_ptr-কে প্রাধান্য দিন, যদি না ভাগাভাগি সত্যিই দরকার।
মূল উপলব্ধি: shared_ptr একটি দারুণ যন্ত্র, কিন্তু “শূন্য খরচের” নয় — এর দাম আকারে, বরাদ্দে, atomic নির্দেশে আর নকশার স্বচ্ছতায়। দাম দিন তখনই, যখন সত্যিকারের ভাগাভাগির সমস্যা সমাধান করছেন।
প্র ০৪Java, C# বা Python garbage collector দিয়ে মেমরি সামলায়। C++ কেন সেই পথে না গিয়ে deterministic destruction বেছে নিল — আর এর মূল্য কী?
garbage collector (GC) প্রোগ্রাম চলার সময় মাঝে মাঝে খুঁজে দেখে কোন object-এ আর কেউ পৌঁছাতে পারে না, তারপর সেগুলোর মেমরি ফেরত নেয়। এতে প্রোগ্রামারকে delete নিয়ে ভাবতে হয় না, আর reference cycle-ও সমস্যা নয় — চক্রের দুজনেই অপ্রাপ্য হলে দুজনকেই সংগ্রহ করা হয়। তাহলে C++ কেন এটা নেয়নি?
প্রথম কারণ — deterministic destruction, অর্থাৎ object ঠিক কখন মরবে তা আগে থেকে জানা। GC-তে object “কোনো এক সময়ে” সংগ্রহ হয়। মেমরির জন্য সেটা চলে, কিন্তু ফাইল বা lock-এর জন্য চলে না — আপনি চান ফাইল এখনই বন্ধ হোক, lock এখনই খুলুক। তাই GC-ভাষাগুলোকে আলাদা ব্যবস্থা আনতে হয়েছে: Java-র try-with-resources, C#-এর using, Python-এর with — যা মূলত হাতে লেখা RAII, আর লিখতে ভুললে কোনো রক্ষা নেই। C++-এ একটি নিয়মেই সব রিসোর্স।
দ্বিতীয় কারণ — অনুমেয় গতি আর “zero-overhead” নীতি। GC চালাতে বাড়তি মেমরি লাগে, আর সংগ্রহের সময় প্রোগ্রাম কখনো কখনো থামে (আধুনিক GC এই বিরতি অনেক কমিয়েছে, তবু শূন্য নয়)। গেম ইঞ্জিন, ট্রেডিং সিস্টেম, গাড়ির নিয়ন্ত্রণ বা মোবাইলের সীমিত ব্যাটারিতে এই অনিশ্চয়তা গ্রহণযোগ্য নয়। C++-এর নীতি — যা ব্যবহার করেন না, তার দাম দেবেন না। RAII-তে প্রতিটি object মোছে ঠিক তার শেষ মুহূর্তে, বাড়তি কোনো পর্যবেক্ষক ছাড়া।
মজার ইতিহাস: C++11 standard-এ আসলে “ঐচ্ছিক garbage collection সমর্থনের” কিছু ছোট নিয়ম আর ফাংশন যোগ করা হয়েছিল (যেমন std::declare_reachable). কিন্তু প্রধান কোনো compiler এগুলো কাজে লাগায়নি, আর C++23-এ এগুলো সরিয়ে ফেলা হয়। বাস্তবে C++ সম্প্রদায় স্পষ্টভাবে RAII-এর পথ বেছে নিয়েছে।
মূল্যটা কী? মালিকানা নিয়ে আপনাকে ভাবতে হয়: কে মালিক, কতদিন। reference cycle নিজে খুঁজে weak_ptr বসাতে হয়। আর reference বা raw pointer যদি তার object-এর চেয়ে বেশি দিন বাঁচে, GC আপনাকে বাঁচাবে না — সেটি dangling (পরের পর্বের বিষয়)। কিন্তু এই চিন্তা আসলে ভালো নকশারই অংশ — GC-ভাষাতেও “কোন object কার” না ভাবলে memory leak হয় (যেমন একটি static তালিকায় ভুলে রেখে দেওয়া object).
মূল উপলব্ধি: GC মেমরির সমস্যাকে লুকায়, RAII সব রিসোর্সের সমস্যাকে কাঠামো দেয়। C++ নিয়ন্ত্রণ আর অনুমেয়তা বেছে নিয়েছে, আর তার বিনিময়ে আপনার কাছে চায় মালিকানা নিয়ে স্পষ্ট চিন্তা।
প্র ০৫Rust-এর ownership মডেল নাকি unique_ptr-এর মতো। মিলটা কোথায়, আর Rust কোন জায়গায় C++-এর চেয়ে এক ধাপ এগিয়ে গেছে?
মিল গভীর। Rust-এ প্রতিটি মানের ঠিক একজন মালিক থাকে, আর মালিক scope-এর বাইরে গেলে মানটি নিজে থেকে মুছে যায় — Rust-এর Drop trait হলো C++-এর destructor-এর সমতুল্য। অর্থাৎ Rust-এর ভিত্তিই RAII. heap-এ একটি মান রাখার টাইপ Box<T> প্রায় হুবহু std::unique_ptr<T>. ভাগাভাগির জন্য Rc<T> (এক thread-এ) আর Arc<T> (atomic, একাধিক thread) — shared_ptr-এর দুটি রূপ; আর চক্র ভাঙতে আছে Weak<T>. Rust-এর নকশাকাররা খোলাখুলিই C++-এর RAII আর smart pointer থেকে অনুপ্রেরণা নিয়েছেন।
পার্থক্য তিন জায়গায়। প্রথমত, move ডিফল্ট: Rust-এ let b = a; লিখলেই (সাধারণ, কপি-অযোগ্য টাইপে) মালিকানা চলে যায় — C++-এর std::move লিখতে হয় না। দ্বিতীয়ত — সবচেয়ে বড় — move-এর পর পুরোনো নাম ব্যবহার compile error. C++-এ move-এর পর order->charge() compile হয়, চলার সময় undefined behavior. Rust-এর compiler প্রমাণ করে যে সরানো মান আর ছোঁয়া হচ্ছে না।
তৃতীয়ত, borrow checker: Rust-এ “শুধু ব্যবহার” মানে borrow (&T বা &mut T), যা C++-এর const T& আর T&-এর মতো। কিন্তু compiler নিশ্চিত করে যে কোনো borrow তার মালিকের চেয়ে বেশি দিন বাঁচবে না, আর একই সময়ে হয় অনেকজন শুধু-পড়ার borrow, নয়তো একজন বদলানোর borrow. ফলে dangling reference আর data race (একাধিক thread-এর সংঘর্ষ) — সাধারণ নিরাপদ Rust কোডে compile-ই হয় না। C++-এ এই নিয়মগুলো আপনাকে নিজে মেনে চলতে হয়; Rust এগুলোকে ভাষার আইন বানিয়েছে।
তবে Rust-এও সব সমস্যা যায়নি: Rc দিয়ে চক্র বানালে সেখানেও leak হয় (Rust leak-কে “memory unsafe” ধরে না), আর কঠোর নিয়মের জন্য কিছু নকশা — যেমন দুই দিকে link থাকা তালিকা — লেখা কঠিন। C++ বেছে নিয়েছে নমনীয়তা আর পুরোনো কোডের সাথে সামঞ্জস্য; Rust বেছে নিয়েছে compile-এর সময়ের প্রমাণ। সাম্প্রতিক বছরগুলোতে C++ সম্প্রদায়ও এই দিকে কাজ করছে — sanitizer, static analyzer, আর “safety profile”-এর মতো প্রস্তাব (পর্ব ৪৩-এ)।
মূল উপলব্ধি: unique_ptr ভালোভাবে বুঝলে আপনি Rust-এর ownership-এর অর্ধেক বুঝে গেছেন। দুই ভাষার মূল প্রশ্ন একই — “এই রিসোর্সের মালিক কে, আর কে শুধু ধার নিয়েছে?” পার্থক্য শুধু, প্রশ্নটির উত্তর যাচাই করে কে: আপনি, না compiler.
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
আউটপুট অনুমান:
Bookclass আগের মতো (constructor-এborrow, destructor-এreturn).main-এ লেখা:Book x("A"); { Book y("B"); Book z("C"); } Book w("D");— পুরো আউটপুট কোন ক্রমে আসবে?borrow : A,borrow : B,borrow : C,return : C,return : B,borrow : D,return : D,return : A. ভেতরের{}শেষ হতেই y আর z উল্টো ক্রমে (আগে C, তারপর B) ফেরত যায়। তারপর D তৈরি হয়। main শেষে বাকি দুজন উল্টো ক্রমে: আগে D (শেষে তৈরি), তারপর A. -
সঠিক টাইপ বেছে নিন: কারণসহ বলুন — (ক) একটি গাড়ির
Engine, যা গাড়ির সাথেই জন্মায় ও মরে, কিন্তু ইঞ্জিনের ধরন চলার সময় ঠিক হয় (খ) একটি মেসেজিং অ্যাপে একটি ছবি, যা একসাথে তিনটি চ্যাটে দেখানো হচ্ছে, আর শেষ চ্যাট বন্ধ হলে মোছা উচিত (গ) একটিEmployee-এর ভেতরে তারDepartment-এর দিকে link, যেখানে Department-ই তার কর্মীদের shared_ptr দিয়ে ধরে রাখে (ঘ) একটি ফাংশন, যা একটি Order-এর মোট দাম হিসাব করে।(ক)
std::unique_ptr<Engine>— একজন মালিক (গাড়ি), ধরন চলার সময় ঠিক হয় বলে heap. (খ)std::shared_ptr<Image>— সত্যিকারের ভাগাভাগি, কে শেষে বন্ধ করবে অজানা। (গ)std::weak_ptr<Department>(বা raw pointer, যদি নিশ্চিত হন Department সবসময় Employee-র চেয়ে বেশি বাঁচে) — দুই দিকেই shared হলে cycle. (ঘ)double total(const Order& o)— শুধু ব্যবহার, smart pointer নয়। -
বাগ শিকার: একটি AI এই কোড দিয়েছে —
std::shared_ptr<Item> p(new Item("pen")); auto q = std::move(p); show(p); void show(std::shared_ptr<Item> it) { std::println("{}", it->name); }. কয়টি সমস্যা খুঁজে পান?তিনটি। (১)
newদিয়ে shared_ptr — দুবার বরাদ্দ;std::make_shared<Item>("pen")লিখুন (আর মালিক একজনই হলে unique_ptr যথেষ্ট)। (২) সবচেয়ে বড়টি: move-এর পরpখালি, অথচshow(p)-এর ভেতরেit->name— খালি pointer ব্যবহার, undefined behavior.show(*q)লেখা উচিত। (৩)showশুধু পড়ে, তাই shared_ptr by value নয় —void show(const Item& it). -
নিজে লিখুন (কোড): উপরের
ownership_demo.cpp-এs1.reset();-এর ঠিক আগে একটি লাইন যোগ করুন, যাw.lock()দিয়ে object নিয়ে তার নাম প্রিন্ট করে, আরreset()-এর পরেও একবারw.lock()চেষ্টা করুন। দুবার কী আসবে, আগে অনুমান করুন।যেমন:
if (auto p = w.lock()) std::println("still open: {}", p->name);— reset-এর আগেstill open: reportআসবে। সাবধান:p-কেif-এর ভেতরেই রাখুন; বাইরে ধরে রাখলে সেটিও একজন মালিক, তখন reset-এ গোনা শূন্য হবে না আরclose reportদেরিতে আসবে। reset-এর পরেw.lock()খালি shared_ptr দেয়, তাইif-এর ভেতরের লাইন চলবেই না — চাইলেelse-এ"gone"প্রিন্ট করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি ফাংশনে local RAII object a আর তারপর b তৈরি হয়, তারপর exception ছোড়া হয়, যা বাইরের catch ধরে। ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
a-এর constructor চলে, রিসোর্স নেয়b-এর constructor চলে, রিসোর্স নেয়throw— স্বাভাবিক পথ থেমে যায়b-এর destructor চলেa-এর destructor চলে- বাইরের catch ব্লক চলে
C++-এ auto p = std::make_unique<int>(5); auto q = p; লিখলে কী হয়?
- p আর q দুজনেই একই int-এর মালিক হয়
- মালিকানা নিঃশব্দে q-তে যায়, p খালি হয়
- Compile error — unique_ptr কপি করা যায় না
- q একটি নতুন int পায়, যার মান 5
C++-এ auto a = std::make_shared<int>(1); auto b = a; { auto c = b; } চলার পর a.use_count() কত?
- 1
- 2
- 3
- 0
C++-এ std::unique_ptr<Order> p একটি object-এর মালিক। auto q = std::move(p); চলার পর p-এর অবস্থা কী?
- p একটি কপি করা Order-এর মালিক
- p আর q দুজনেই মালিক
- Order মুছে গেছে, p dangling
- p খালি (nullptr), মালিক এখন q
একটি C++ ফাংশনের ভেতরে local object x, তারপর y, তারপর z তৈরি হলো। ফাংশন শেষ হলে destructor কোন ক্রমে চলে?
- z, y, x
- x, y, z
- compiler যেকোনো ক্রমে চালাতে পারে
- প্রোগ্রাম বন্ধ হওয়ার সময় একসাথে
C++-এ দুটি object একে অপরকে std::shared_ptr দিয়ে ধরে রাখে, আর বাইরের সব shared_ptr ধ্বংস হয়ে গেছে। কী ঘটে?
- shared_ptr চক্র চিনে দুটিকেই মুছে দেয়
- দুটির destructor উল্টো ক্রমে চলে
- কারও গোনা শূন্য হয় না — memory leak
- Compile error
একটি C++ ফাংশন একটি Report শুধু পড়ে প্রিন্ট করবে। ডাকার মানুষের কাছে Report-টি একটি std::unique_ptr<Report>-এ আছে। প্যারামিটারের সবচেয়ে উপযুক্ত টাইপ কোনটি?
- std::unique_ptr<Report>
- const Report&
- std::shared_ptr<Report>
- std::unique_ptr<Report>&
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- RAII: constructor রিসোর্স নেয়, destructor ফেরত দেয়। scope শেষে — return, break বা exception, যেভাবেই হোক — destructor নিজে চলে, তৈরির উল্টো ক্রমে।
- heap-এ একটি object দরকার হলে
std::make_unique; unique_ptr কপি হয় না,std::move-এ হাতবদল হয়, আর move-এর পর পুরোনোটি খালি। std::shared_ptrশুধু সত্যিকারের ভাগাভাগিতে,make_sharedদিয়ে; শেষ মালিক চলে গেলে object মোছে, কিন্তু দাম আছে।- চক্র আর cache-এ
std::weak_ptr— দেখে, গোনে না; ব্যবহার করতেlock(). - মালিকানা মানে smart pointer; শুধু ব্যবহার মানে
T&/const T&/T*. কোডেnew/deleteদেখলে প্রশ্ন করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৮ · lifetime — কে কতক্ষণ বাঁচে, dangling কেন বিপদপরবর্তী পর্বমালিক না হয়েও যারা object ব্যবহার করে, তাদের নিরাপদ রাখার নিয়ম।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C প্রোগ্রামিং · dynamic memoryগভীরে যানmalloc আর free দিয়ে হাতে মেমরি সামলানো — RAII ছাড়া পৃথিবী কেমন।