Lesson ১২পড়তে ৯ মিনিট লাগবে

স্মার্ট পয়েন্টার (Smart Pointers)

আপনার মেমোরির (memory) অটোমেটিক ক্লিনার বা পরিচ্ছন্নতাকর্মী (Automatic janitors) — এই RAII-এর মানেই হলো আপনাকে আর কখনোই বলতে হবে না যে 'হায়, আমি তো এটি মুছে ফেলতে বা ডিলিট করতেই (delete) ভুলে গেছি'

যে ক্লিনার (Janitor) কখনোই কিছু ভোলে না (Never Forgets)

কোনো একটি কনফারেন্স রুমের (conference room) কথা কল্পনা করুন। মানুষজন সেখানে হেঁটে বেড়াচ্ছে (walk in), হোয়াইটবোর্ড (whiteboard) ব্যবহার করছে, আর কফি পান করছে (drink coffee)। আর সেখানকার শেষ মানুষটিও (last person) যখন ওই রুম থেকে বেরিয়ে যায় (leaves), তখন সেখানকার ক্লিনার (janitor) নিজে থেকেই এর দরজাটি বন্ধ করে দেয় (locks the door) এবং রুমটিকে পরিষ্কার করে ফেলে (cleans up)। এক্ষেত্রে অন্য কাউকে এটি মনে রাখতে হয় না — এটি শুধু নিজে থেকেই ঘটে যায় (just happens)।

এটিকে মূলত RAII (Resource Acquisition Is Initialization) বলা হয়। আর সি++ (C++)-এ এই স্মার্ট পয়েন্টারগুলোই (smart pointers) হলো সেই ক্লিনার (janitor)। এদের মূলত হিপের (heap) ওপর বিভিন্ন অবজেক্টকে অ্যালোকেট (allocate) বা বরাদ্দ করার ক্ষমতা বা ওন (own) করার অধিকার দেওয়া থাকে, আর এই স্মার্ট পয়েন্টারগুলো (smart pointer) যখনই তাদের নিজস্ব জগৎ বা স্কোপ (scope) থেকে বেরিয়ে যায়, তখন তারা মূলত তাদের অধীনে থাকা যেকোনো অবজেক্টকেই সাথে সাথে বা বলা যায় অটোমেটিক্যালি ডিলিট (automatically deletes) করে দেয়। যার ফলে এখানে কোনো কিছুকে ম্যানুয়ালি ডিলিটের (manual delete) প্রয়োজন পড়ে না। কোনো মেমোরি লিকের (memory leaks) চিন্তা থাকে না। কোনো ডাবল ফ্রি-এরও (double-frees) ভয় থাকে না。

কেন এই র পয়েন্টার (raw new/delete) এত বিপদজনক (Dangerous)?

ক্লিনারের (janitor) এই উদাহরণে র পয়েন্টার (raw pointers) দিয়ে মূলত আপনাকেই (you) বোঝানো হয়। আর মানুষ মাত্রই ভুল বা ভুলে যাওয়ার (humans forget) সম্ভাবনা থাকে।

  • কোনো কিছুকে delete করতে ভুলে গেছেন (Forget to delete)? তার মানে মেমোরি লিক (Memory leak)।
  • একই জিনিসকে দুইবার মুছে ফেললে (Delete twice)? তার মানে এটি আনডিফাইন্ড বিহেভিয়ার (Undefined behavior)।
  • new এবং delete-এর মাঝে কোনো এক্সেপশন থ্রো (Exception thrown) হয়েছে? এর মানে লিক (Leak)।
  • কোনো ফাংশন থেকে খুব শুরুতেই ফিরে এলে (Return early)? তার মানে লিক (Leak)।

এই স্মার্ট পয়েন্টারগুলো (Smart pointers) মূলত আপনার এই সমস্ত সমস্যারই সমাধান নিয়ে আসে। চলুন এদের তিনটি ধরন (three kinds) সম্পর্কে বিস্তারিত জেনে নেওয়া যাক。

ইউনিক পয়েন্টার (unique_ptr) — এর একচ্ছত্র মালিকানা (Exclusive Ownership)

এই std::unique_ptr হলো অনেকটা কোনো প্রাইভেট লকারের (private locker) চাবির (key) মতো। একটি নির্দিষ্ট সময়ে শুধুমাত্র একটি (Only one) ইউনিক পয়েন্টারই (unique_ptr) কোনো একটি নির্দিষ্ট অবজেক্টের মালিক (own) হতে পারে। এটি যখন কোনো কারণে মুছে যায় বা ধ্বংস (destroyed) হয়ে যায়, তখন মূলত এর ওই মূল অবজেক্টটিও সম্পূর্ণভাবে মুছে (deleted) যায়। আপনি চাইলেই একে কপি (copy) করতে পারবেন না — তবে আপনি চাইলে এটিকে নিজের সুবিধামতো মুভ (move) বা স্থানান্তর করতে পারবেন, যা মূলত ওই চাবিটি অন্য কাউকে দিয়ে (handing the key) তার মালিকানা পরিবর্তন (transferring ownership) করে দেওয়ার মতোই কাজ করে。

ইউনিক পয়েন্টার (unique_ptr) — এদের তৈরি (Creation) বা মুভ করা (Move)

#include <iostream>
#include <memory>
using namespace std;
int main() {
// make_unique ব্যবহার করে তৈরি (Create) করা (এঁকে বেশি অগ্রাধিকার বা preferred দেওয়া হয়)
auto p1 = make_unique<int>(42);
cout << "p1 = " << *p1 << endl;
// কপি করা যায় না (Can't copy):
// auto p2 = p1; // এরর বা ERROR: এর কপি কনস্ট্রাক্টরটি (copy constructor) মুছে ফেলা বা deleted হয়েছে
// কিন্তু মুভ করা যায় (But can move):
auto p2 = move(p1);
cout << "p2 = " << *p2 << endl;
// p1 এখন মূলত একটি nullptr
if (!p1) cout << "p1 is null after move" << endl; // মুভ (move) করার পর p1 এখন মূলত একটি নাল (null) পয়েন্টার
// p2 স্কোপ (scope) থেকে বের হলেই এটি নিজে থেকে বা স্বয়ংক্রিয়ভাবে (Automatically) মুছে যায় বা deleted হয়
return 0;
}
Output
p1 = 42
p2 = 42
p1 is null after move

শেয়ার্ড পয়েন্টার (shared_ptr) — এর যৌথ মালিকানা (Shared Ownership)

এই std::shared_ptr-কে মূলত একটি শেয়ার করা বা ব্যবহৃত নেটফ্লিক্স (Netflix) অ্যাকাউন্টের সাথে তুলনা করা যেতে পারে। এক্ষেত্রে একাধিক শেয়ার্ড পয়েন্টারগুলো (shared_ptrs) একই অবজেক্টের (same object) দিকে পয়েন্ট (point) করে থাকতে পারে। এটি নিজস্ব কাজের জন্য মূলত একটি রেফারেন্স কাউন্ট (reference count) মেন্টেইন বা ধরে রাখে। যখন এর একেবারে শেষের (last) শেয়ার্ড পয়েন্টারটি ধ্বংস (destroyed) হয়ে যায়, তখন মূলত ঠিক ওই ক্লিনারের (janitor) মতোই এটি ওই অবজেক্টটিকে মুছে (deleted) ফেলে — যখন এর সবশেষ মানুষটিও (last person) ঘর থেকে বেরিয়ে যায়。

রেফারেন্স কাউন্টিংয়ের (Reference Counting) সাথে শেয়ার্ড পয়েন্টার (shared_ptr)

#include <iostream>
#include <memory>
using namespace std;
int main() {
auto sp1 = make_shared<string>("Hello, RAII!");
cout << "sp1: " << *sp1 << endl;
cout << "count: " << sp1.use_count() << endl;
{
auto sp2 = sp1; // কপি (copy) — এক্ষেত্রে এই উভয়টিই মূলত স্ট্রিংটির (string) মালিক (own)
cout << "count after sp2: " << sp1.use_count() << endl; // sp2-এর পর কাউন্ট বা count:
auto sp3 = sp1; // আরেকটি কপি বা another copy
cout << "count after sp3: " << sp1.use_count() << endl; // sp3-এর পর কাউন্ট বা count:
} // sp2 এবং sp3 ঠিক এখানেই মূলত ধ্বংস বা destroyed হয়ে গেছে
cout << "count after block: " << sp1.use_count() << endl; // ব্লকের বা block-এর পর কাউন্ট বা count:
// sp1 স্কোপ (scope) থেকে বেরিয়ে গেলেই স্ট্রিংটি মূলত মুছে যায় বা deleted হয়
return 0;
}
Output
sp1: Hello, RAII!
count: 1
count after sp2: 2
count after sp3: 3
count after block: 1

উইক পয়েন্টার (weak_ptr) — এক ধরনের অজার্ভার বা দর্শক (The Observer)

এই std::weak_ptr-কে মূলত দূরবীন (binoculars) হাতে দাঁড়িয়ে থাকা কোনো দর্শকের (spectator) সাথে তুলনা করা যেতে পারে — এটি মূলত ওই শেয়ার্ড পয়েন্টারের (shared_ptr) মালিকানাধীন অবজেক্টটিকে (object) দেখতে (see) পারে, কিন্তু এটি কখনোই একে জীবিত (keep alive) রাখতে পারে না। এটি মূলত এর এই সার্কুলার রেফারেন্সিংগুলোকে (circular references) ভেঙে দেয় বা ব্রেক করে (breaks) (যেখানে দুটি শেয়ার্ড পয়েন্টার বা shared_ptrs একে অপরের দিকে পয়েন্ট করে থাকে, এবং এরা কেউই কখনো এর কাউন্টে (count) গিয়ে জিরো বা शून्य-তে পৌঁছায় না)।

এখানকার এই অবজেক্টটিকে ব্যবহার (use) করতে চাইলে, আপনাকে অবশ্যই .lock()-টি কল (call) করতে হবে, যা মূলত আপনাকে একটি অস্থায়ী শেয়ার্ড পয়েন্টারগামী (temporary shared_ptr) পথ প্রদান করবে — তবে তা শুধুমাত্র তখন, যদি সেখানকার ওই অবজেক্টটি তখনও বিদ্যমান (still exists) বা বেঁচে থাকে。

ইউনিক পয়েন্টার (unique_ptr) থেকে শেয়ার্ড পয়েন্টারে (shared_ptr) কনভার্ট বা রূপান্তর (Converting) করা

#include <iostream>
#include <memory>
using namespace std;
int main() {
// একচ্ছত্র মালিকানা (exclusive ownership) দিয়ে শুরু (Start) করুন
auto uniq = make_unique<int>(100);
// এদেরকে মূলত শেয়ার্ড ওনারশিপে (shared ownership) ট্রান্সফার বা রূপান্তর (Transfer) করুন (এটি মূলত একটি ওয়ান ওয়ে ট্রিপ বা one-way trip!)
shared_ptr<int> shared = move(uniq);
cout << "shared: " << *shared << endl; // শেয়ার্ড বা shared:
cout << "uniq is null: " << (uniq == nullptr) << endl; // uniq হলো নাল বা null:
// আপনি কোনোভাবেই (CANNOT) shared_ptr থেকে আগের ওই unique_ptr-এ ফেরত যেতে পারবেন না
// কারণ তা এর একচ্ছত্র মালিকানার (exclusive ownership) নিয়মটিকে অমান্য করে বা violate করে দিয়ে পার পেয়ে যাবে!
return 0;
}
Output
shared: 100
uniq is null: 1

কাস্টম ডিলিটার (Custom Deleter)

#include <iostream>
#include <memory>
using namespace std;
struct Connection {
string name;
Connection(string n) : name(n) {
cout << "Opening " << name << endl; // খুলছে বা Opening
}
};
void closeConnection(Connection* c) {
cout << "Closing " << c->name << endl; // বন্ধ হচ্ছে বা Closing
delete c;
}
int main() {
// কাস্টম ডিলিটার বা Custom deleter: এটি যেকোনো প্লেইন ডিলিটের (plain delete) পরিবর্তে মূলত এই closeConnection-টিকেই রান (runs) করে
unique_ptr<Connection, decltype(&closeConnection)>
conn(new Connection("DB-1"), closeConnection);
cout << "Using " << conn->name << endl; // ব্যবহার করা হচ্ছে বা Using
// স্কোপ (scope) থেকে বেরিয়ে যাওয়ার সময় এই closeConnection-টি মূলত স্বয়ংক্রিয়ভাবেই বা automatically কল (called) হয়ে যায়
return 0;
}
Output
Opening DB-1
Using DB-1
Closing DB-1

একটি ইউনিক পয়েন্টার বা unique_ptr-কে রিটার্ন (Returning) করে আসা ফ্যাক্টরি ফাংশন (Factory Function)

#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Shape {
public:
virtual string name() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
string name() const override { return "Circle"; }
};
class Square : public Shape {
public:
string name() const override { return "Square"; }
};
// ফ্যাক্টরি বা Factory: এর कॉলার (caller) মূলত unique_ptr-এর মাধ্যমে এর মালিকানা বা ownership পেয়ে যায়
unique_ptr<Shape> makeShape(const string& type) {
if (type == "circle") return make_unique<Circle>();
if (type == "square") return make_unique<Square>();
return nullptr;
}
int main() {
auto s1 = makeShape("circle");
auto s2 = makeShape("square");
if (s1) cout << s1->name() << endl;
if (s2) cout << s2->name() << endl;
// এখানকার উভয়টিই বা Both মূলত নিজে থেকে বা automatically ক্লিন (cleaned up) বা পরিষ্কার হয়ে যায়
return 0;
}
Output
Circle
Square

কুইক রেফারেন্স (Quick Reference)

স্মার্ট পয়েন্টার (Smart Pointer)মালিকানা বা ওনারশিপ (Ownership)কপি করা যায় কি না? (Copyable?)কখন ব্যবহার করবেন (Use When)
unique_ptrএকচ্ছত্র বা Exclusive (1 মালিক বা owner)না (শুধু মুভ করা যায় বা move only)যেকোনো কিছু নির্বাচনের সময় এটিই ডিফল্ট পছন্দ (Default choice)। একক মালিকানা বা Single owner।
shared_ptrশেয়ার্ড বা Shared (একাধিক বা N মালিক বা owners)হ্যাঁ (Yes)বিভিন্ন ধরনের কোড যখন তাদের কোনো একটি নির্দিষ্ট রিসোর্সকে শেয়ার (share) করে
weak_ptrঅজার্ভার বা দর্শক (Observer) (কোনো মালিক নেই বা 0 owners)হ্যাঁ (Yes)যেকোনো সার্কেলকে ব্রেক (Breaking cycles) করার সময়, ক্যাশ (caches), এবং বিভিন্ন অপশনাল অ্যাক্সেসগুলোর (optional access) সময়
Note: আধুনিক সি++ (C++)-এ ভুলেও (NEVER) কখনো র পয়েন্টার বা new/delete ব্যবহার করবেন না। এর বদলে সব সময় make_unique এবং make_shared ব্যবহার করার চেষ্টা করুন — এগুলো মূলত যেকোনো ধরনের এক্সেপশনের জন্য সম্পূর্ণ নিরাপদ (exception-safe) এবং এগুলো তুলনামূলক অনেকটাই পরিষ্কার বা ক্লিনার (cleaner)। যদি আপনি কখনো নিজের কোডে 'new' লিখতে যান, তবে তার মানে হলো আপনি জিনিসটিকে ভুল (wrong) উপায়ে করছেন। এর একমাত্র ব্যতিক্রম (exception) হলো যখন unique_ptr-এর সাহায্যে আপনার কোনো কাস্টম ডিলিটারের (custom deleter) প্রয়োজন পড়ে, ঠিক তখনই কেবল আপনাকে সরাসরি এই 'new'-টি ব্যবহার (use) করতে হয়।
চ্যালেঞ্জ

ছোট কুইজ

আপনি যদি std::unique_ptr-কে কপি (copy) করার চেষ্টা করেন তবে আসলে কী ঘটবে?
Pointers & ReferencesSTL Containers