ট্র্যাক
/
C++
C++
/
সিলেবাস
/
স্মার্ট পয়েন্টার
স্

স্মার্ট পয়েন্টার মধ্যে C++

১টি অনুশীলনী

স্মার্ট পয়েন্টার সম্পর্কে

স্মার্ট পয়েন্টার হলো C++-এর একটি আধুনিক ফিচার, যা স্বয়ংক্রিয় মেমরি ম্যানেজমেন্টের সুবিধা দেয়। সাধারণত র পয়েন্টারের সঙ্গে যুক্ত থাকা মেমরি লিক ও ড্যাঙ্গলিং পয়েন্টার প্রতিরোধে এটি সাহায্য করে। এগুলো র পয়েন্টারের চারপাশে র্যাপার হিসেবে কাজ করে এবং বাড়তি সুবিধা যোগ করে, যেমন পয়েন্টারটির আর প্রয়োজন না হলে স্বয়ংক্রিয়ভাবে মেমরি ডি-অ্যালোকেট করা।

সাধারণ সিনট্যাক্স

স্মার্ট পয়েন্টার সাধারণত C++ স্ট্যান্ডার্ড লাইব্রেরিতে ক্লাস টেমপ্লেট হিসেবে তৈরি করা হয়। সবচেয়ে বেশি ব্যবহৃত দুটি স্মার্ট পয়েন্টার হলো std::unique_ptr ও std::shared_ptr।

ইউনিক পয়েন্টার

std::unique_ptr একটি স্মার্ট পয়েন্টার, যা অবজেক্টটির একচেটিয়া মালিকানা রাখে। এটি নিশ্চিত করে যে যেকোনো সময়ে কেবল একটি std::unique_ptr অবজেক্টই রিসোর্সটির মালিক থাকে। মালিকানা রাখা std::unique_ptr ধ্বংস বা রিসেট হলে এটি স্বয়ংক্রিয়ভাবে অবজেক্টগুলো ডেস্ট্রাক্ট করে ও তার মেমরি মুক্ত করে দেয়।

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

std::make_unique()-এর সুবিধা

একটি std::unique_ptr তৈরি করার সময় মেমরি অ্যালোকেট করতে সরাসরি new ব্যবহার না করে std::make_unique() ব্যবহার করাই ভালো। std::make_unique() বেশ কিছু সুবিধা দেয়:

  1. এক্সেপশন সেফটি: std::make_unique() এক্সেপশন সেফটির নিশ্চয়তা দেয়। অবজেক্টটি তৈরি হওয়ার সময় কোনো এক্সেপশন ছোড়া হলে মেমরি স্বয়ংক্রিয়ভাবে ডি-অ্যালোকেট হয়ে যায়, ফলে মেমরি লিক প্রতিরোধ হয়।
  2. স্পষ্টতা: std::make_unique() ব্যবহার করলে কোড আরও স্পষ্ট ও সংক্ষিপ্ত হয়। এতে যে টাইপটি অ্যালোকেট করা হচ্ছে তা আলাদা করে উল্লেখ করার দরকার পড়ে না, কারণ টেমপ্লেট আর্গুমেন্ট স্বয়ংক্রিয়ভাবে অনুমান করা হয়।
  3. অপ্টিমাইজেশনের সুযোগ: new দিয়ে হাতে হাতে মেমরি অ্যালোকেট করার চেয়ে কম্পাইলাররা std::make_unique()-কে আরও কার্যকরভাবে অপ্টিমাইজ করার সুযোগ পায়, যা পারফরম্যান্স উন্নত করতে পারে।
  4. ভুল ব্যবহার এড়ানো: std::unique_ptr হাতে তৈরি করলে তার অন্তর্নিহিত রিসোর্স মুছে ফেলা সম্ভব হয়ে পড়ে। এর ফলে std::unique_ptr যখন স্কোপের শেষে সেটি মুছে ফেলতে যায়, তখন আনডিফাইনড বিহেভিয়ার দেখা দেয়।

শেয়ার্ড পয়েন্টার

std::shared_ptr একটি স্মার্ট পয়েন্টার, যা একাধিক std::shared_ptr অবজেক্টকে একই রিসোর্সের মালিকানা ভাগ করে নিতে দেয়। এটি হিসাব রাখে কতগুলো শেয়ার্ড পয়েন্টার রিসোর্সটির রেফারেন্স ধরে রেখেছে, এবং কেবল রিসোর্সটির মালিকানা রাখা শেষ শেয়ার্ড পয়েন্টারটি স্কোপের বাইরে চলে গেলে বা রিসেট হলে তবেই মেমরি ডি-অ্যালোকেট করে।

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Caution

C++17 এবং তার আগের ভার্সনে std::make_shared<T[]>-এর মাধ্যমে std::shared_ptr দিয়ে অ্যারে ব্যবহার সরাসরি সমর্থিত নয়। std::make_shared<T[]> দিয়ে অ্যারে অ্যালোকেট করা সম্ভব হলেও, সেগুলো থেকে সরাসরি শেয়ার্ড পয়েন্টার তৈরি করলে আনডিফাইনড বিহেভিয়ার ঘটতে পারে, কারণ একক অবজেক্ট আর অ্যারের মেমরি ম্যানেজমেন্টে পার্থক্য থাকে। এর বদলে শেয়ার্ড পয়েন্টার দিয়ে অ্যারে কার্যকরভাবে ম্যানেজ করতে std::vector বা কাস্টম ডিলিশন ফাংশন ব্যবহার করার কথা ভাবুন। C++17-এ অ্যারে অ্যালোকেশন আর শেয়ার্ড পয়েন্টার নিয়ে কাজ করার সময় সবসময় আপনার কম্পাইলার ও স্ট্যান্ডার্ড লাইব্রেরি ইমপ্লিমেন্টেশনের সঙ্গে সামঞ্জস্য নিশ্চিত করুন।

std::make_shared()-এর সুবিধা

std::make_unique()-এর মতোই std::make_shared() উন্নত মেমরি এফিশিয়েন্সি, এক্সেপশন সেফটি আর রিডেবিলিটির মতো সুবিধা দেয়। এটি কন্ট্রোল ব্লক ও ম্যানেজড অবজেক্টের মেমরি অ্যালোকেশনকে একটিমাত্র অপারেশনে মিলিয়ে দেয়, ফলে দক্ষতা বাড়ে ও মেমরি লিকের ঝুঁকি কমে। এছাড়া টেমপ্লেট আর্গুমেন্ট স্বয়ংক্রিয়ভাবে অনুমান করার সুবিধা কোডকে সহজ করে ও রিডেবিলিটি বাড়ায়। C++-এ std::shared_ptr অবজেক্ট নিয়ে কাজ করার সময় std::make_shared() ব্যবহার করলে কোড আরও পরিচ্ছন্ন, নিরাপদ ও দক্ষ হয়।

উইক পয়েন্টার

std::weak_ptr হলো std::shared_ptr-এর সঙ্গী ক্লাস, যা শেয়ার্ড পয়েন্টার দিয়ে ম্যানেজ করা কোনো অবজেক্টের প্রতি একটি মালিকানাহীন "উইক" রেফারেন্স দেয়। এটি রিসোর্সটির লাইফটাইমে কোনো প্রভাব না ফেলেই সেটিতে অ্যাক্সেস করতে দেয়। মেমরি লিক প্রতিরোধ করতে সাইক্লিক রেফারেন্স ভাঙার প্রয়োজন হলে উইক পয়েন্টার কাজে লাগে।

// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;

// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.

std::weak_ptr ও সাইক্লিক ওনারশিপ

std::shared_ptr ব্যবহারের সময় যে সাইক্লিক ওনারশিপ, অর্থাৎ সার্কুলার রেফারেন্স, দেখা দিতে পারে, সেই সমস্যা সমাধানের জন্যই std::weak_ptr তৈরি করা হয়েছে।

সাইক্লিক ওনারশিপের পরিস্থিতিতে দুই বা তার বেশি std::shared_ptr অবজেক্ট একে অপরের রেফারেন্স ধরে রাখে, ফলে একটি চক্র তৈরি হয় যেখানে কোনো অবজেক্টই মুছে ফেলা সম্ভব হয় না, কারণ তারা একে অপরের প্রতি স্ট্রং রেফারেন্স রাখে। এর ফলে মেমরি লিক ঘটে।

std::weak_ptr এই সমস্যার একটি সমাধান দেয়: এটি শেয়ার্ড অবজেক্টের প্রতি উইক রেফারেন্স রাখতে দেয়, কিন্তু তাদের রেফারেন্স কাউন্টে কিছু যোগ করে না। অর্থাৎ এটি শেয়ার্ড অবজেক্টটি লক্ষ্য করতে ও অ্যাক্সেস করতে পারে, কিন্তু সেটি মোছে যাওয়া আটকায় না। শেয়ার্ড অবজেক্টের সব স্ট্রং রেফারেন্স ছেড়ে দেওয়া হলে অবজেক্টটি ধ্বংস হয়ে যায়, আর সেটি লক্ষ্য করা উইক পয়েন্টারগুলো স্বয়ংক্রিয়ভাবে nullptr-এ রিসেট হয়।

সাইক্লিক ওনারশিপ ভাঙতে std::weak_ptr-এর ব্যবহার দেখানো একটি ছোট উদাহরণ এখানে:

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::weak_ptr<Node> next;
};

// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;

// Release strong references to nodes
node1.reset();
node2.reset();

// Nodes are automatically destructed due to weak references

"Node constructed" দুইবার প্রিন্ট হয়, তারপর "Node destructed"-এর দুইটি লাইন আসে।

next যখন উইক পয়েন্টার নয়, বরং একটি std::shared_ptr, তখন এটি সাইক্লিক ওনারশিপের পরিস্থিতি তৈরি করে, ফলে মেমরি লিক ঘটে, কারণ দুটি নোডই একে অপরের প্রতি স্ট্রং রেফারেন্স ধরে রাখে, যা তাদের ধ্বংস হতে দেয় না।

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::shared_ptr<Node> next; // Change to shared_ptr
};

এই পরিস্থিতিতে node1 ও node2 উভয়ই std::shared_ptr-এর মাধ্যমে একে অপরের প্রতি স্ট্রং রেফারেন্স ধরে রাখে, ফলে একটি চক্র তৈরি হয়। প্রোগ্রাম শেষ হয়ে যখন reset() দিয়ে স্ট্রং রেফারেন্সগুলো ছেড়ে দেওয়ার চেষ্টা করা হয়, তখন দুটি নোডেরই রেফারেন্স কাউন্ট শূন্য থাকে না, কারণ প্রতিটি নোড অন্যটির প্রতি একটি স্ট্রং রেফারেন্স ধরে রাখে। তাই নোডগুলো ধ্বংস হয় না, ফলে মেমরি লিক ঘটে। "Node constructed" দুইবার প্রিন্ট হয়, কিন্তু তারপর "Node destructed"-এর কোনো লাইন আসে না।

ড্যাঙ্গলিং পয়েন্টার ও std::weak_ptr

কোনো পয়েন্টার মুছে ফেলা হওয়া একটি অবজেক্টের রেফারেন্স ধরে রাখলে ড্যাঙ্গলিং পয়েন্টার তৈরি হয়, আর পয়েন্টারটি ডিরেফারেন্স করা হলে আনডিফাইনড বিহেভিয়ার ঘটে। std::shared_ptr দিয়ে ম্যানেজ করা কোনো অবজেক্টের প্রতি মালিকানাহীন উইক রেফারেন্স দিয়ে std::weak_ptr ড্যাঙ্গলিং পয়েন্টার এড়াতে সাহায্য করে। std::weak_ptr দিয়ে ড্যাঙ্গলিং পয়েন্টার এড়ানোর উপায় হলো:

  1. একটি std::weak_ptr তৈরি করুন: সরাসরি একটি std::shared_ptr ধরে রাখার বদলে শেয়ার্ড অবজেক্টটির জন্য একটি std::weak_ptr তৈরি করুন।

  2. বৈধতা যাচাই করুন: std::weak_ptr ব্যবহার করার আগে expired() ফাংশন দিয়ে তার বৈধতা যাচাই করুন। সংশ্লিষ্ট শেয়ার্ড অবজেক্টটি মুছে ফেলা হয়ে থাকলে এই ফাংশন true রিটার্ন করে।

  3. std::weak_ptr লক করুন: শেয়ার্ড অবজেক্টটি নিরাপদে অ্যাক্সেস করতে lock() ফাংশনটি ব্যবহার করুন, যা অবজেক্টটি এখনও বৈধ থাকলে সেটির দিকে ইঙ্গিত করা একটি std::shared_ptr রিটার্ন করে। অবজেক্টটি মুছে ফেলা হয়ে থাকলে lock() একটি খালি std::shared_ptr রিটার্ন করে।

ড্যাঙ্গলিং পয়েন্টার এড়াতে std::weak_ptr-এর ব্যবহার দেখানো একটি কোড উদাহরণ এখানে:

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

এই উদাহরণে node1->next ও node2->next হলো std::weak_ptr অবজেক্ট। শেয়ার্ড অবজেক্টটি অ্যাক্সেস করার আগে expired() ফাংশন দিয়ে বৈধতা যাচাই করা হয়। এরপর নিরাপদে শেয়ার্ড অবজেক্টটি অ্যাক্সেস করতে lock() ব্যবহার করা হয়। এই পদ্ধতি নিশ্চিত করে যে std::weak_ptr নিয়ে কাজ করার সময় ড্যাঙ্গলিং পয়েন্টার এড়ানো যায়।

ব্যবহারের পরামর্শ

ডিফল্টভাবে স্মার্ট পয়েন্টার ব্যবহার করুন: একচেটিয়া মালিকানার জন্য std::unique_ptr আর ভাগ করা মালিকানার জন্য std::shared_ptr। র পয়েন্টার রাখুন মালিকানাহীন রেফারেন্সের জন্য বা পুরনো (লিগ্যাসি) কোডের সঙ্গে ইন্টারফেস করার জন্য। বেশিরভাগ ক্ষেত্রে একচেটিয়া মালিকানার জন্য std::unique_ptr-ই যথেষ্ট, কারণ এটি রেফারেন্স কাউন্টিংয়ের বাড়তি বোঝা ছাড়াই হালকা মেমরি ম্যানেজমেন্ট দেয়। std::shared_ptr কম ব্যবহার করা উচিত, কারণ সত্যিকারের শেয়ার্ড ওনারশিপ দরকার না হলে এটি বাড়তি বোঝা ও জটিলতা আনে। std::weak_ptr বিশেষভাবে সাইক্লিক ডিপেন্ডেন্সি ভাঙতে বা শেয়ার্ড অবজেক্ট লক্ষ্য করতে ব্যবহৃত হয়, তবে এটি সাধারণত বেশি ব্যবহার করা হয় না।

GitHub-এর মাধ্যমে সম্পাদনা করুন লিংকটি নতুন একটি উইন্ডো বা ট্যাবে খুলবে

স্মার্ট পয়েন্টার শিখুন