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

copy, move আর Rule of 0/3/5 — object কপি করার নিয়ম

Copy, Move and the Rule of 0/3/5 — How C++ Objects Are Copied

Taught in Bangla, this lesson explains copying and moving in C++: the six special member functions, the shallow-copy trap, deep copy with copy constructor and assignment, move semantics, why noexcept matters to vector, the Rule of Three, Five and Zero, and non-copyable types.

এই পর্বে যা শিখবেন

  • রিসোর্সের মালিক একটি object কপি করার মানে কী, আর shallow copy কেন double delete ঘটায়
  • ছয়টি special member function, আর compiler কখন কোনটি নিজে বানায় (Howard Hinnant-এর টেবিল)
  • Deep copy-র copy constructor ও copy assignment, self-assignment-এর ফাঁদ আর copy-and-swap কৌশল
  • Move constructor ও move assignment — pointer নিয়ে নেওয়া, source খালি রাখা, আর noexcept কেন জরুরি
  • Rule of Three, Five আর Zero; = delete দিয়ে কপি-অযোগ্য টাইপ, আর AI-এর লেখা boilerplate যাচাই

১ · কপি করা মানে কী? — দলিলের ফটোকপি আর চাবি হাতবদল

একটি int কপি করা সহজ। int b = a; লিখলে b-তে a-এর মান বসে, তারপর দুটি সম্পূর্ণ আলাদা। একটি বদলালে অন্যটির কিছু হয় না। C++-এর বেশিরভাগ টাইপ এভাবেই আচরণ করে — একে বলে value semantics. পর্ব ২০ আর ২১-এ আপনি যখন নিজের class বানিয়েছেন, তখনও Student b = a; লিখলে একটি স্বাধীন কপি পেয়েছেন, কারণ ভেতরে ছিল নাম আর নম্বরের মতো সাধারণ ডেটা।

কিন্তু এমন object-এর কথা ভাবুন, যে নিজে একটি resource-এর মালিক — ধরুন একটি ছবির class, যে heap-এ দশ লাখ pixel রেখেছে আর তার ঠিকানা রেখেছে একটি raw pointer-এ। এই object “কপি” করার মানে কী? দুটি সম্ভাব্য উত্তর আছে, আর দুটির ফল একদম আলাদা।

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

এই তিনটি সাড়াই C++-এ আছে, আর প্রতিটির একটি নাম আছে:

দলিলের ফটোকপি

শুধু pointer-এর মান, অর্থাৎ ঠিকানা কপি হয়। দুটি object একই heap ব্লক দেখায়। এটাই compiler-এর ডিফল্ট আচরণ, আর রিসোর্স-মালিক class-এ এটাই বাগ।

shallow copy

নতুন বাড়ি বানানো

নতুন মেমরি নিয়ে ভেতরের সব ডেটা কপি করা হয়। দুটি object সম্পূর্ণ স্বাধীন। নিরাপদ, কিন্তু সময় আর মেমরি লাগে।

deep copy

চাবি হাতবদল

নতুন object পুরোনোটির pointer নিয়ে নেয়, পুরোনোটি খালি হয়। কোনো ডেটা কপি হয় না, তাই প্রায় খরচহীন। পুরোনোটি যখন আর লাগবে না, তখনই এটা চলে।

move

এই পর্বের পুরো গল্প এই তিনটি সাড়াকে ঘিরে: কীভাবে compiler-কে বলবেন কোনটি চান, কখন সে নিজে থেকে কোনটি বেছে নেয়, আর কীভাবে এমন class লিখবেন যেখানে ভুলটা হওয়ার সুযোগই নেই।

মূল ধারণা

“কপি” শব্দের অর্থ ঠিক হয় ownership বা মালিকানা দিয়ে। যে object কোনো কিছুর মালিক নয়, তার কপি সহজ। যে object মালিক, তার কপির সময় ঠিক করতে হয়: মালিকানাও কি কপি হবে (নতুন রিসোর্স), নাকি হাতবদল হবে (move)? মালিকানা কখনো ভাগাভাগি হয়ে দুজনের হাতে একই রিসোর্স থাকা চলবে না — যদি না সেটি পর্ব ১৭-এর shared_ptr-এর মতো ইচ্ছাকৃত হয়।

২ · ছয়টি বিশেষ সদস্য — compiler যা নিজে লিখে দেয়

পর্ব ২১-এ আপনি constructor আর destructor দেখেছেন। এরা একটি বড় পরিবারের সদস্য, যার নাম special member function. এগুলো বিশেষ, কারণ আপনি না লিখলেও compiler দরকারমতো নিজে এদের ঘোষণা আর সংজ্ঞা লিখে দিতে পারে। একটি Image class-এর জন্য ছয়টির চেহারা:

নামস্বাক্ষরকখন চলে (উদাহরণ)
default constructorImage()Image a;
copy constructorImage(const Image&)Image b = a; — নতুন object, আরেকটি থেকে কপি
copy assignmentImage& operator=(const Image&)b = a; — আগে থেকে থাকা b-তে কপি
move constructorImage(Image&&)Image c = std::move(a);
move assignmentImage& operator=(Image&&)c = std::move(b);
destructor~Image()scope শেষে, বা delete করলে

একটি পার্থক্য আগেই পরিষ্কার করে নিন, কারণ পরীক্ষায় এটা বারবার আসে। constructor চলে নতুন object জন্মানোর সময়; assignment চলে এমন object-এ, যে আগে থেকেই বেঁচে আছে। Image b = a; লেখায় = চিহ্ন থাকলেও এটি assignment নয় — b এইমাত্র জন্মাচ্ছে, তাই copy constructor চলে। b = a; আলাদা লাইনে লিখলে b-এর আগে থেকেই নিজের pixel আছে, তাই copy assignment-কে সেগুলোর ব্যবস্থাও করতে হয়। এই বাড়তি দায়িত্বই assignment-কে কঠিন করে।

move-এর দুটি সদস্যের প্যারামিটারে && চিহ্ন — একে বলে rvalue reference. এর পুরো ব্যাখ্যা পর্ব ৩৬-এ। এখানে শুধু এটুকু জানলেই চলবে: && এমন object-এর সাথে যুক্ত হয়, যার ভেতরের জিনিস নিয়ে নেওয়া নিরাপদ — হয় সেটি একটি temporary object (যেমন ফাংশনের ফেরত দেওয়া মান, যার আর কোনো নাম নেই), নয়তো আপনি std::move লিখে স্পষ্ট অনুমতি দিয়েছেন।

compiler-এর বানানো সংস্করণ কী করে?

আপনি না লিখলে compiler-এর বানানো copy constructor করে memberwise copy: প্রতিটি data member-কে, ঘোষণার ক্রমে, তার নিজের কপির নিয়মে কপি করে। std::string member হলে string-এর নিজের copy constructor চলে (যা deep copy করে)। int হলে মান কপি হয়। আর raw pointer হলে? pointer-এর “মান” তো একটি ঠিকানা — তাই শুধু ঠিকানাটাই কপি হয়। compiler জানে না, আর জানার উপায়ও নেই, যে এই pointer একটি heap ব্লকের মালিক, নাকি শুধু অন্য কোথাও আঙুল দিয়ে দেখাচ্ছে। একইভাবে compiler-এর বানানো move constructor প্রতিটি member-কে move করে, আর destructor প্রতিটি member-এর destructor ডাকে — raw pointer-এর জন্য কিছুই না।

কোন অবস্থায় compiler কোন সদস্যটি নিজে বানায়, তার নিয়ম কিছুটা জটিল। পুরো টেবিলটি অংশ ৭-এ দেখবেন। আপাতত সবচেয়ে সাধারণ ক্ষেত্রটি মনে রাখুন: আপনি কোনো special member না লিখলে compiler ছয়টিই দরকারমতো বানিয়ে দেয়, আর সেগুলো memberwise কাজ করে।

৩ · shallow copy-র ফাঁদ — দুই pointer, এক বাড়ি

এবার একটি ভুল class দেখুন। Image heap-এ pixel রাখে, constructor-এ new[] করে আর destructor-এ delete[] — পর্ব ১৭-এর RAII মেনেই। শুধু copy-র কথা ভাবা হয়নি:

class Image {
    int n;          // pixel কতগুলো
    int* px;        // heap-এর pixel — এই object এর মালিক
public:
    explicit Image(int size) : n(size), px(new int[size]{}) {}
    ~Image() { delete[] px; }
    // copy constructor লিখিনি — compiler নিজে বানাবে: n কপি, px কপি (শুধু ঠিকানা!)
};

void makeBackup() {
    Image photo(1000000);
    Image backup = photo;   // backup.px == photo.px — একই heap ব্লক
}                           // প্রথমে backup-এর destructor, তারপর photo-র — একই ব্লক দুবার delete[]

কোডটি compile হয়, কোনো warning-ও আসে না। কিন্তু makeBackup শেষ হওয়ার মুহূর্তে একই মেমরি দুবার ফেরত দেওয়া হয় — একে বলে double delete, আর এটি undefined behavior. ধাপে ধাপে দেখুন কী ঘটে:

Image photo(1000000);একজন মালিক, একটি ব্লক
stack                      heap
photo { n = 1000000,
        px = @5000 } ───►  [ 1000000 pixel @5000 ]
Image backup = photo;compiler-এর copy: শুধু ঠিকানা কপি
stack                      heap
photo { n = 1000000,
        px = @5000 } ──────►  [ 1000000 pixel @5000 ]
                                ▲
backup { n = 1000000,           │
         px = @5000 } ────────┘   দুজন মালিক, ব্লক একটি
backup-এর pixel বদলালে?photo-ও বদলে যায়
backup.px[0] = 255;   // শুধু backup বদলাতে চেয়েছিলেন

photo.px[0]  → 255     // অথচ photo-ও বদলে গেছে!
"backup" আসলে কোনো backup নয় — একই ছবির দ্বিতীয় নাম
} // scope শেষ — আগে backup ধ্বংসব্লক ফেরত গেল
stack                      heap
photo { n = 1000000,
        px = @5000 } ──────►  [ ফেরত দেওয়া মেমরি @5000 ]
                                photo এখন dangling pointer ধরে আছে
backup (ধ্বংস হয়েছে, delete[] @5000 চলেছে)
তারপর photo ধ্বংসএকই ঠিকানা আবার delete[]
delete[] @5000   ← দ্বিতীয়বার!

double delete = undefined behavior:
  · কখনো সঙ্গে সঙ্গে crash ("double free detected")
  · কখনো চুপচাপ heap-এর হিসাব নষ্ট — অনেক পরে অন্য জায়গায় crash
  · কখনো কিছুই বোঝা যায় না — সবচেয়ে বিপজ্জনক
  1. একজন মালিক। photo তৈরির সময় constructor heap থেকে দশ লাখ pixel-এর জায়গা নিল (ধরুন ঠিকানা 5000) আর px-এ সেই ঠিকানা রাখল। এ পর্যন্ত সব ঠিক।
  2. compiler-এর copy — দলিলের ফটোকপি। backup-এর জন্য compiler-এর বানানো copy constructor চলল। সে n কপি করল, আর px কপি করল — অর্থাৎ ঠিকানা 5000. নতুন কোনো pixel তৈরি হয়নি। এটাই shallow copy.
  3. প্রথম উপসর্গ: আলাদা নয়। backup-এ কিছু বদলালে photo-ও বদলে যায়, কারণ দুজনে একই মেমরিতে লিখছে। যিনি ছবির backup রাখতে চেয়েছিলেন, তিনি পেয়েছেন একই ছবির দ্বিতীয় নাম।
  4. প্রথম destructor. scope শেষে local object উল্টো ক্রমে ধ্বংস হয়, তাই আগে backup. তার destructor ঠিক কাজই করল — delete[] px. কিন্তু এখন photo.px এমন মেমরি দেখাচ্ছে, যা আর তার নয় — একটি dangling pointer.
  5. দ্বিতীয় destructor — double delete. photo-র destructor-ও একই ঠিকানায় delete[] করল। এর ফল কী হবে, C++ standard কিছুই নিশ্চিত করে না। প্রতিটি destructor আলাদাভাবে দেখলে নির্ভুল — দোষ copy-র, যে দুজন মালিক বানিয়ে ফেলেছে।

এই পর্বে shallow copy-র কোডটিকে ইচ্ছা করেই চালানোর বোতাম দেওয়া হয়নি। undefined behavior-এর কোড চালিয়ে “দেখি কী হয়” পরীক্ষা করা অর্থহীন — আজ crash হলে কাল না-ও হতে পারে, এক compiler-এ এক রকম, অন্যটিতে আরেক রকম। নিজের কম্পিউটারে এমন বাগ খুঁজতে চাইলে -fsanitize=address দিয়ে compile করুন; AddressSanitizer double delete ধরলে স্পষ্ট বার্তা দিয়ে প্রোগ্রাম থামায়।

সমস্যার মূল কারণটা আরেকবার বলি: compiler জানে না px একটি মালিক pointer. তাই সমাধানও দুটি। হয় compiler-কে বলে দিন কপি কীভাবে হবে (নিজের copy constructor — পরের অংশ), নয়তো raw pointer-এর বদলে এমন member রাখুন, যে নিজেই নিজের কপি জানে (যেমন std::vector<int> — অংশ ৭-এর Rule of Zero).

C জানলে

C-তে struct assign করলে (b = a;) সব বাইট কপি হয় — এটাই C++-এর memberwise copy-র পূর্বপুরুষ। C-তেও pointer member থাকলে ঠিক এই shallow copy সমস্যা হয়, আর দুবার free করলে একই undefined behavior. পার্থক্য হলো, C++-এ আপনি copy-র নিয়ম নিজে লিখে দিতে পারেন, আর destructor থাকায় মোছার কাজটা নিজে থেকে হয় — তাই ভুল copy-র শাস্তিও নিশ্চিতভাবে আসে। struct কপির মূল নিয়ম দেখতে চাইলে C কোর্সের struct ও union পর্ব দেখুন।

৪ · deep copy — copy constructor আর copy assignment

সমাধানের প্রথম ধাপ: নিজের copy constructor লেখা, যা নতুন বাড়ি বানায়। নতুন object-এর জন্য নতুন heap ব্লক নিন, তারপর source-এর সব pixel সেখানে কপি করুন। এর নাম deep copy:

Image(const Image& other)
    : n(other.n), px(new int[other.n]) {           // ১. নিজের নতুন ব্লক
    std::copy(other.px, other.px + n, px);         // ২. ভেতরের ডেটা কপি
}

প্যারামিটার const Image& — reference, কারণ by value নিলে সেই প্যারামিটার বানাতেই আবার copy constructor লাগত (অসীম চক্র); আর const, কারণ কপি করার সময় source বদলানোর কোনো কারণ নেই। এবার একই ঘটনা আবার দেখুন:

Image backup = photo; // এবার নিজের copy constructorধাপ ১: নতুন ব্লক চাওয়া
photo  { n = 1000000, px = @5000 } ──►  [ photo-র pixel  @5000 ]
backup { n = 1000000, px = @9000 } ──►  [ খালি নতুন ব্লক @9000 ]
std::copy(other.px, other.px + n, px);ধাপ ২: ভেতরের ডেটা কপি
photo  ──►  [ 12 40 77 ... @5000 ]
                │ │  │
                ▼ ▼  ▼   দশ লাখ মান কপি হচ্ছে
backup ──►  [ 12 40 77 ... @9000 ]
backup.px[0] = 255;এখন সত্যিই আলাদা
photo  ──►  [ 12  40 77 ... @5000 ]   ← বদলায়নি
backup ──►  [ 255 40 77 ... @9000 ]
} // scope শেষপ্রত্যেকে নিজের ব্লক মোছে
backup-এর destructor → delete[] @9000   ✓
photo-র destructor   → delete[] @5000   ✓
প্রতিটি ব্লক ঠিক একবার ফেরত গেল — কোনো double delete নেই
  1. নতুন বাড়ির জমি। নিজের copy constructor-এর initializer list-এ new int[other.n] — backup নিজের জন্য সমান আকারের একটি নতুন ব্লক পেল, ঠিকানা আলাদা (ধরুন 9000).
  2. আসবাবপত্র কপি। std::copy photo-র প্রতিটি pixel নতুন ব্লকে কপি করল। দশ লাখ মান — এটাই deep copy-র দাম। বড় ছবিতে এতে সময় লাগে, আর মেমরিও দ্বিগুণ লাগে।
  3. সত্যিকারের স্বাধীনতা। এখন backup বদলালে photo বদলায় না। “backup” নামটা এবার সার্থক।
  4. পরিষ্কার বিদায়। দুটি destructor দুটি আলাদা ঠিকানা মোছে। প্রতিটি রিসোর্সের ঠিক একজন মালিক — RAII-এর মূল শর্ত আবার পূরণ হলো।

copy assignment — আগে থেকে থাকা object-এ কপি

copy constructor-এর কাজ সহজ, কারণ নতুন object-এর কিছু নেই। কিন্তু b = a;-এ b-এর আগে থেকেই নিজের pixel আছে। তাই copy assignment-কে তিনটি কাজ করতে হয়: নিজের পুরোনো ব্লক ফেরত দেওয়া, নতুন ব্লক নেওয়া, ডেটা কপি করা। প্রথম চেষ্টায় অনেকেই, আর অনেক AI টুলও, এভাবে লেখে:

Image& operator=(const Image& other) {      // ভুল সংস্করণ — দুটি বাগ
    delete[] px;                               // নিজের পুরোনো pixel মুছলাম
    n = other.n;
    px = new int[n];                           // বাগ ২: এখানে exception হলে?
    std::copy(other.px, other.px + n, px);     // বাগ ১: a = a হলে other.px তো এইমাত্র মোছা!
    return *this;
}

বাগ ১ — self-assignment. কেউ a = a; লিখবে কেন? সরাসরি কেউ লেখে না, কিন্তু ঘুরপথে প্রায়ই ঘটে: photos[i] = photos[j]; যখন i আর j সমান, বা দুটি reference একই object দেখায়। তখন other আর *this একই object. প্রথম লাইনেই নিজের pixel মুছে ফেলা হলো, তারপর std::copy সেই মোছা মেমরি থেকে পড়ছে — undefined behavior. পুরোনো সমাধান ছিল শুরুতে একটি পাহারা: if (this == &other) return *this; — এখানে this হলো বর্তমান object-এর ঠিকানা।

বাগ ২ — মাঝপথে ব্যর্থতা। new মেমরি না পেলে std::bad_alloc exception ছোড়ে। তখন পুরোনো ব্লক ইতিমধ্যে মোছা, কিন্তু px এখনো সেই মোছা ঠিকানা ধরে আছে। object টি ভাঙা অবস্থায় রয়ে গেল, আর পরে তার destructor আবার সেই ঠিকানা মুছবে। self-check এই বাগ সারায় না।

copy-and-swap — দুটি বাগের একটি সমাধান

অভিজ্ঞ C++ প্রোগ্রামাররা এখানে একটি কৌশল ব্যবহার করেন, যার নাম copy-and-swap. ধারণাটা হলো: যা ভাঙতে পারে (নতুন মেমরি নেওয়া, ডেটা কপি), তা আগে একটি আলাদা অস্থায়ী object-এ করুন। সেটা সফল হলে তবেই নিজের সাথে অদলবদল করুন — আর অদলবদল কখনো ব্যর্থ হয় না, কারণ শুধু দুটি int আর দুটি pointer বদলানো হচ্ছে।

void swap(Image& other) noexcept {             // শুধু pointer আর size অদলবদল
    std::swap(n, other.n);
    std::swap(px, other.px);
}
Image& operator=(const Image& other) {
    Image tmp(other);    // ১. আগে কপি (ব্যর্থ হলে exception — *this একদম অক্ষত)
    swap(tmp);           // ২. নতুন ডেটা আমার, পুরোনো ডেটা tmp-এর
    return *this;
}                        // ৩. tmp-এর destructor আমার পুরোনো pixel মুছে দেয়

খেয়াল করুন কী কী হলো। self-assignment-এ tmp একটি সম্পূর্ণ নতুন কপি, তাই কিছুই হারায় না — পাহারার দরকারই নেই। new ব্যর্থ হলে exception ওঠে প্রথম লাইনেই, যখন *this এখনো ছোঁয়া হয়নি। হয় পুরো assignment সফল, নয়তো কিছুই বদলায় না — এই প্রতিশ্রুতিকে বলে strong exception guarantee. আর পুরোনো pixel মোছার কোড আলাদা করে লিখতেই হয়নি — destructor-এর কাজই সেটা। std::swap এখানে শুধু int আর pointer অদলবদল করে, যা কখনো exception ছোড়ে না।

দামের কথা: copy-and-swap প্রতিবার নতুন ব্লক নেয়, এমনকি যখন b-এর পুরোনো ব্লক যথেষ্ট বড় আর সেখানেই কপি করা যেত। std::vector-এর মতো library টাইপ তাই নিজের assignment আরও যত্ন করে লেখে। সাধারণ application কোডে সরলতা আর নিরাপত্তার জন্য copy-and-swap খুব ভালো ডিফল্ট।

এ পর্যন্ত আমরা তিনটি জিনিস নিজে লিখলাম: destructor, copy constructor আর copy assignment. এটা কাকতালীয় নয়। যে class-কে নিজের destructor লিখতে হয়েছে, সে নিশ্চয়ই কোনো রিসোর্স নিজে সামলাচ্ছে — আর তখন compiler-এর memberwise copy প্রায় নিশ্চিতভাবেই ভুল। এই পর্যবেক্ষণটিকে বলে Rule of Three: তিনটির একটি লিখতে হলে, সম্ভবত তিনটিই লিখতে হবে। নিয়মটি ১৯৯০-এর দশক থেকে C++ সম্প্রদায়ে প্রচলিত।

৫ · move — কপি না করে চাবি হাতবদল

deep copy নিরাপদ, কিন্তু সবসময় দরকার নয়। ধরুন একটি ফাংশন ক্যামেরা থেকে একটি ছবি লোড করে ফেরত দিল, বা আপনি একটি ছবি একটি album-এর vector-এ ঢুকিয়ে দিচ্ছেন আর মূল ভ্যারিয়েবলটি আর ব্যবহার করবেন না। এসব ক্ষেত্রে দশ লাখ pixel কপি করে তারপর মূলটি মুছে ফেলা হলো নতুন বাড়ি বানিয়ে পুরোনোটা ভেঙে ফেলার মতো অপচয়। দরকার ছিল শুধু চাবি হাতবদল।

C++11 এজন্যই move semantics এনেছে। move constructor source-এর pointer নিজের কাছে নিয়ে নেয়, আর source-এর pointer nullptr করে দেয় — যাতে source-এর destructor পরে কিছু না মোছে:

Image(Image&& other) noexcept
    : n(std::exchange(other.n, 0)),              // other.n নাও, other.n = 0
      px(std::exchange(other.px, nullptr)) {}    // other.px নাও, other.px = nullptr

Image& operator=(Image&& other) noexcept {
    if (this != &other) {
        delete[] px;                             // নিজের পুরোনো pixel ফেরত
        n  = std::exchange(other.n, 0);
        px = std::exchange(other.px, nullptr);
    }
    return *this;
}

std::exchange (C++14 থেকে, <utility>-এ) এক লাইনে দুটি কাজ করে: প্রথম আর্গুমেন্টে নতুন মান বসায় আর পুরোনো মানটি ফেরত দেয়। move লেখার জন্য এটি একদম মাপমতো — “আপনার জিনিসটা আমাকে দিন, আর আপনি খালি হয়ে যান।” move assignment-এ self-check রাখা হয়েছে, কারণ a = std::move(a);-এ নিজের pixel মুছে তারপর সেই মোছা ঠিকানা নিজেকে দেওয়া হতো।

1Image a(1000000);2Image c = std::move(a);3  : n(std::exchange(other.n, 0))4  , px(std::exchange(other.px, nullptr)) {}5a = Image(10);   // খালি a-তে নতুন মান6}  // scope শেষ
কে কোন ব্লকের মালিক (ঠিকানা উদাহরণমাত্র)
a
c
@5000 ব্লক
  1. একজন মালিক। a heap-এ দশ লাখ pixel-এর মালিক। এবার c-তে ছবিটি দরকার, আর a-কে আর লাগবে না।
  2. std::move — অনুমতিপত্র। std::move(a) নিজে কিছুই সরায় না। সে শুধু a-কে একটি rvalue হিসেবে দেখায়, ফলে compiler copy constructor-এর বদলে move constructor বেছে নেয়। এটা আপনার স্বাক্ষর: “a-এর ভেতরের জিনিস নিয়ে যাও, আমি আর ব্যবহার করব না।”
  3. size নেওয়া। std::exchange(other.n, 0) — c-এর n হলো দশ লাখ, আর a-এর n শূন্য। দুটি int-এর কাজ, এক মুহূর্তে।
  4. চাবি হাতবদল। c-এর px এখন 5000, a-এর px nullptr. দশ লাখ pixel একটুও নড়েনি — শুধু একটি ঠিকানা এক object থেকে আরেকটিতে গেছে। ছবি যত বড়ই হোক, move-এর খরচ একই।
  5. খালি, কিন্তু বৈধ। move-এর পর a ধ্বংস হয়ে যায়নি — সে একটি বৈধ, খালি Image. তার destructor delete[] nullptr চালাবে, যা নিরাপদ এবং কিছুই করে না। চাইলে তাকে নতুন মানও দেওয়া যায়, যেমন এখানে a = Image(10);.
  6. প্রত্যেকে নিজেরটা মোছে। scope শেষে c মোছে 5000, a মোছে নতুন ব্লক 7000. পুরো যাত্রায় দশ লাখ pixel-এর ব্লক তৈরি হয়েছে একবার, মোছা হয়েছে একবার, কপি হয়নি একবারও।

এবার পাঁচটি সদস্য একসাথে চালিয়ে দেখুন। নিচের Image-এ প্রতিটি special member নিজের নাম প্রিন্ট করে। assignment-টি copy-and-swap-এর একটি সংক্ষিপ্ত রূপ — প্যারামিটার by value নেয়, ফলে একটিমাত্র ফাংশন copy assignment আর move assignment দুটোর কাজ করে (lvalue দিলে প্যারামিটার কপি হয়ে আসে, std::move করা object দিলে move হয়ে, আর temporary দিলে সরাসরি প্যারামিটারেই তৈরি হয়ে):

#include <algorithm>
#include <print>
#include <utility>

class Image {                                // pixel-er ekmatro malik
    int n = 0;  int* px = nullptr;
public:
    explicit Image(int size) : n(size), px(new int[size]{}) { std::println("ctor      n={}", n); }
    Image(const Image& o) : n(o.n), px(new int[o.n]) {           // deep copy
        std::copy(o.px, o.px + n, px);  std::println("copy ctor n={}", n); }
    Image(Image&& o) noexcept                                    // pointer-ta niye nao
        : n(std::exchange(o.n, 0)), px(std::exchange(o.px, nullptr)) { std::println("move ctor n={}, source n={}", n, o.n); }
    Image& operator=(Image o) noexcept {                         // copy-and-swap
        std::swap(n, o.n);  std::swap(px, o.px);  std::println("assign    n={}", n);  return *this; }
    ~Image() { std::println("dtor      n={}", n);  delete[] px; }
};
int main() {
    Image a(1000);
    Image b = a;                 // copy
    Image c = std::move(a);      // move
    b = Image(5);                // assign
}

আউটপুট লাইন ধরে মিলিয়ে নিন। ctor n=1000 — a জন্মাল। copy ctor n=1000 — b জন্মাল a থেকে, নিজের নতুন ব্লক নিয়ে। move ctor n=1000, source n=0 — c জন্মাল a-এর ব্লক নিয়ে, আর a-এর size এখন শূন্য। তারপর b = Image(5);: প্রথমে ctor n=5 — C++17-এর নিয়মে temporary টি সরাসরি প্যারামিটার o-তেই তৈরি হয়, মাঝে কোনো copy বা move চলে না (একে বলে guaranteed copy elision). তারপর assign n=5 — অদলবদল, এখন b-এর হাতে ৫টি pixel. তারপর dtor n=1000 — প্যারামিটার o ধ্বংস হলো, আর সাথে নিয়ে গেল b-এর পুরোনো হাজার pixel. শেষে main-এর object-রা উল্টো ক্রমে: dtor n=1000 (c), dtor n=5 (b), dtor n=0 (a — খালি, তাই কিছু মোছে না)। পুরো প্রোগ্রামে heap ব্লক তৈরি হয়েছে মোট তিনটি (a-এর, b-এর কপির আর ৫-এর ছবির), আর তিনটিই মুছেছে ঠিক একবার।

copy elision-এর কারণে আউটপুট যেন আপনাকে না ঠকায়। C++17 থেকে temporary (prvalue) দিয়ে object বানালে — Image x = Image(5); বা return Image(5); — কোনো copy বা move constructor চলবেই না, এটি নিশ্চিত। কিন্তু নাম আছে এমন local object ফেরত দিলে (Image img(5); return img;) compiler চাইলে copy বাদ দিতে পারে (NRVO), না-ও পারে; না পারলে move চলে। তাই special member-এ প্রিন্ট বসিয়ে পরীক্ষা করার সময় “কতবার move হলো” প্রশ্নের উত্তর compiler ও optimization-ভেদে বদলাতে পারে। উপরের উদাহরণটি এমনভাবে বানানো, যাতে প্রতিটি লাইন C++17-এর নিয়মে নিশ্চিত।

move-এর পরে source-এর কী অবস্থা?

নিজের class-এ আপনি ঠিক করেন। আমাদের Image-এ সরানো object নিশ্চিতভাবে খালি (n=0, px=nullptr). কিন্তু standard library-র টাইপের ক্ষেত্রে নিয়মটা সাবধানে পড়ুন: move-এর পর তারা থাকে “valid but unspecified” অবস্থায় — বৈধ, কিন্তু ঠিক কী মান তা বলা নেই। একে বলে moved-from state. এমন object ধ্বংস করা যায়, নতুন মান দেওয়া যায়, আর যে ফাংশনের কোনো পূর্বশর্ত নেই (যেমন size(), clear()) তা ডাকা যায়। কিন্তু “একটি moved-from std::string খালি” — এটা ধরে নেবেন না। ব্যতিক্রম হলো std::unique_ptr: move-এর পর সেটি nullptr হবে, standard তা নিশ্চিত করে (পর্ব ১৭)। সবচেয়ে নিরাপদ অভ্যাস: move করা object-এ নতুন মান না দিয়ে তা আর পড়বেন না।

৬ · noexcept — vector কেন move-এ ভরসা করতে চায়

উপরের move constructor-এ একটি শব্দ খেয়াল করেছেন: noexcept. এটা ঘোষণা করে, এই ফাংশন কোনো exception বাইরে ছুড়বে না। শুনতে সামান্য, কিন্তু এর অভাবে আপনার move অনেক জায়গায় কাজেই লাগে না। কারণটা বুঝতে std::vector-এর ভেতরে তাকান।

পর্ব ১৩-এ দেখেছেন, vector-এর জায়গা ফুরিয়ে গেলে সে একটি বড় নতুন ব্লক নেয়, পুরোনো সব উপাদান সেখানে নিয়ে যায়, তারপর পুরোনো ব্লক মোছে। C++98 থেকেই push_back প্রতিশ্রুতি দেয়: বড় হওয়ার সময় কিছু ব্যর্থ হলে vector আগের মতোই থাকবে — strong guarantee. পুরোনো উপাদানগুলো কপি করে নিলে এটা সহজ: মাঝপথে একটি কপি ব্যর্থ হলে নতুন ব্লক ফেলে দাও, পুরোনো ব্লক অক্ষতই আছে।

এখন উপাদানগুলো move করলে কী হয়? ধরুন ১০টির মধ্যে ৬টি সরানো হয়ে গেছে, আর সপ্তমটির move exception ছুড়ল। প্রথম ৬টির ডেটা এখন নতুন ব্লকে, পুরোনো ব্লকে তারা খালি। ফেরত সরাতে গেলে আবার move লাগবে — যা আবার ব্যর্থ হতে পারে। ভাঙা ডিম আর জোড়া লাগানো যায় না। তাই vector-এর নিয়ম: উপাদানের move constructor noexcept হলে move করব; না হলে, আর কপি করা গেলে, কপি করব। (ভেতরে সে std::move_if_noexcept নামের একটি সাহায্যকারী দিয়ে এই সিদ্ধান্ত নেয়।) নিচে দুটি টাইপ — একটির move-এ noexcept আছে, অন্যটিতে নেই:

#include <print>
#include <string_view>
#include <vector>

std::string_view used = "nothing";                 // shesh kon special member cholechhe
struct Safe {
    Safe() = default;
    Safe(const Safe&) { used = "copy ctor"; }
    Safe(Safe&&) noexcept { used = "move ctor"; }
};
struct Risky {
    Risky() = default;
    Risky(const Risky&) { used = "copy ctor"; }
    Risky(Risky&&) { used = "move ctor"; }          // noexcept nei
};
int main() {
    std::vector<Safe> s(1);  std::vector<Risky> r(1);
    while (s.size() < s.capacity()) s.emplace_back();   // jayga purota bhoro
    while (r.size() < r.capacity()) r.emplace_back();
    used = "nothing";  s.emplace_back();  std::println("vector<Safe>  grew using: {}", used);
    used = "nothing";  r.emplace_back();  std::println("vector<Risky> grew using: {}", used);
}

আউটপুট দুই লাইন: vector<Safe> grew using: move ctor আর vector<Risky> grew using: copy ctor. দুটি টাইপের move constructor হুবহু এক, পার্থক্য শুধু একটি শব্দে। তবু Risky-র বেলায় vector তার move-কে বিশ্বাস করল না, কপি বেছে নিল। এখানে কপি সস্তা, কিন্তু দশ লাখ pixel-এর Image হলে প্রতিবার vector বড় হওয়ার সময় সব ছবি deep copy হতো — শুধু একটি শব্দের অভাবে। প্রথমে প্রতিটি vector-কে তার পুরো capacity পর্যন্ত ভরা হয়েছে, যাতে পরের emplace_back-এ নতুন জায়গা নিতেই হয়; capacity কত, সেটা library-ভেদে আলাদা হতে পারে, তাই উদাহরণটি সংখ্যার ওপর নির্ভর করে না।

মূল নিয়ম

move constructor আর move assignment প্রায় সবসময় noexcept লিখুন। pointer আর int অদলবদল কখনো ব্যর্থ হয় না, তাই এটা সত্যি প্রতিশ্রুতি। সুখবর: compiler-এর বানানো বা = default করা move নিজে থেকেই noexcept হয়, যদি সব member-এর move noexcept হয় — std::string, std::vector, std::unique_ptr-এর move তাই। destructor-ও C++11 থেকে ডিফল্টভাবে noexcept. আর খেয়াল রাখুন: noexcept বলে exception ছুড়লে প্রোগ্রাম সরাসরি std::terminate — তাই মিথ্যা প্রতিশ্রুতি দেবেন না।

৭ · Rule of Three → Five → Zero

C++11 move আনার পর Rule of Three বড় হয়ে হলো Rule of Five: destructor, copy constructor, copy assignment, move constructor, move assignment — এর একটিও নিজে লিখলে বা delete করলে, পাঁচটির কথাই ভাবুন। “ভাবুন” মানে প্রতিটিকে হয় লিখুন, হয় = default, হয় = delete — কিন্তু চুপ করে compiler-এর হাতে ছেড়ে দেবেন না। কেন ছেড়ে দেওয়া বিপজ্জনক, সেটা বোঝা যায় compiler-এর নিয়মগুলো দেখলে।

compiler কখন কোনটি বানায় — Hinnant-এর টেবিল

move semantics-এর প্রধান নকশাকারদের একজন Howard Hinnant নিয়মগুলো একটি টেবিলে সাজিয়েছিলেন, যা এখন বহু জায়গায় উদ্ধৃত হয়। সারিতে: আপনি নিজে কোন সদস্যটি ঘোষণা করেছেন। ঘরে: বাকিগুলোর কী হয়। implicitly declared মানে compiler নিজে বানায়।

আপনি ঘোষণা করলেনdefault ctordestructorcopy ctorcopy assignmove ctormove assign
কিছুই নাবানায়বানায়বানায়বানায়বানায়বানায়
যেকোনো constructorনেইবানায়বানায়বানায়বানায়বানায়
destructorবানায়আপনারবানায় (deprecated)বানায় (deprecated)নেইনেই
copy ctorনেইবানায়আপনারবানায় (deprecated)নেইনেই
copy assignবানায়বানায়বানায় (deprecated)আপনারনেইনেই
move ctorনেইবানায়deleteddeletedআপনারনেই
move assignবানায়বানায়deleteddeletedনেইআপনার

টেবিলের তিনটি জিনিস মুখস্থ নয়, বুঝে রাখুন। এক, destructor বা copy-র যেকোনো একটি ঘোষণা করলেই move আর তৈরি হয় না। compiler-এর যুক্তি: “আপনি নিজে রিসোর্স সামলাচ্ছেন, আমি move কীভাবে করব আন্দাজ করব না।” দুই, move-এর কোনো একটি ঘোষণা করলে copy দুটি deleted হয় — অর্থাৎ ব্যবহার করলে compile error. তিন, “deprecated” মানে এখনো বানায়, শুধু পুরোনো কোড না ভাঙার জন্য; ভবিষ্যতে বাদ যেতে পারে, আর GCC-তে -Wextra দিলে এমন ক্ষেত্রে ব্যবহারে warning আসতে পারে।

নীরব ফাঁদ: destructor লিখলেই move হারায়

প্রথম নিয়মটির একটি অপ্রত্যাশিত ফল আছে। ধরুন একটি Album class-এ std::vector<Photo> আছে, যা নিজেই নিখুঁত move জানে। কেউ ডিবাগ করার জন্য এক লাইনের একটি destructor যোগ করলেন: ~Album() { std::println("album closed"); }. কোড compile হয়, সব ঠিক চলে। কিন্তু এখন Album-এর move constructor আর তৈরি হয় না। Album b = std::move(a); লিখলে কোনো error আসে না — কারণ copy constructor আছে, আর const Album& একটি rvalue-কেও নিতে পারে। ফলে move-এর বদলে চুপচাপ পুরো album deep copy হয়, হাজার ছবিসহ। কোনো warning নেই, শুধু প্রোগ্রাম ধীর। সমাধান: destructor লিখলে move দুটিকে স্পষ্ট করে ফিরিয়ে আনুন — Album(Album&&) noexcept = default; আর Album& operator=(Album&&) noexcept = default; — আর তখন copy দুটিও = default লিখুন, কারণ move ঘোষণা করায় সেগুলো deleted হয়ে গেছে। ঠিক এজন্যই Rule of Five বলে, পাঁচটির কথাই ভাবুন।

Rule of Zero — সবচেয়ে ভালো নিয়ম: একটিও লিখবেন না

এতক্ষণ যা লিখলাম, তা পড়ে মনে হতে পারে C++ class লেখা মানেই পাঁচটি জটিল ফাংশন। আসলে উল্টো। ২০১২ সালে R. Martinho Fernandes একটি ব্লগ লেখায় যুক্তি দেন: যে class-এর কাজ রিসোর্সের মালিকানা সামলানো, সে শুধু সেই কাজই করবে; বাকি সব class এমন মালিক-টাইপকে member হিসেবে রাখবে আর কোনো special member লিখবে না। এর নাম Rule of Zero. Standard library ইতিমধ্যে মালিক-টাইপগুলো দিয়ে রেখেছে: std::string, std::vector, std::unique_ptr. আমাদের Image এবং album এভাবে লিখলে:

struct Image {                          // Rule of Zero — একটিও special member নেই
    int width = 0, height = 0;
    std::vector<int> px;                // vector নিজেই deep copy, move, delete জানে
};
struct Photo  { std::string caption; Image image; };
class  Album  {
    std::string title;
    std::vector<Photo> photos;
public:
    void add(Photo p) { photos.push_back(std::move(p)); }
};

Album trip = loadAlbum("coxs-bazar");   // ফেরত আসা মান: move বা copy elision
Album backup = trip;                    // deep copy — সব ছবি, সব caption
Album shared = std::move(trip);         // move — শুধু কয়েকটি pointer হাতবদল

এখানে কোনো destructor নেই, কোনো copy constructor নেই, কোনো new নেই — অথচ সব কাজ ঠিকঠাক। compiler-এর বানানো copy প্রতিটি member কপি করে, আর vector ও string নিজেরা deep copy করে। compiler-এর বানানো move প্রতিটি member move করে, আর সেগুলো noexcept — তাই std::vector<Album> বড় হওয়ার সময়ও move-ই হয়। একটিও বাগ লেখার সুযোগ নেই, কারণ কোনো কোডই লেখা হয়নি। C++ Core Guidelines-ও একই কথা বলে: special member এড়াতে পারলে এড়িয়ে চলুন (নিয়ম C.20), আর একটি লিখলে বা delete করলে সবগুলোর কথা ভাবুন (নিয়ম C.21).

তাহলে কোন class-এর জন্য কোন নিয়ম? প্রতিটি ধাপে নিজেকে প্রশ্নটি করুন:

প্রশ্ন ১: class টি কি নিজে সরাসরি কোনো রিসোর্স ধরে?raw pointer-এর মালিকানা, C লাইব্রেরির handle, file descriptor
member-গুলো দেখুন:
  std::string name;            → নিজের রিসোর্স সামলায়, আপনার নয়
  std::vector<Photo> photos;   → নিজের রিসোর্স সামলায়
  std::unique_ptr<Engine> e;   → নিজের রিসোর্স সামলায়
  int* px;   // new[] করেছি    → এটা আপনার দায়িত্ব
উত্তর “না” → Rule of Zeroবেশিরভাগ class এখানেই শেষ
struct Student { std::string name; std::vector<int> marks; };

destructor লিখবেন না, copy/move লিখবেন না।
compiler-এর বানানো ছয়টিই সঠিক, দ্রুত, noexcept move সহ।
লক্ষ্য: আপনার class-গুলোর ৯০%-এর বেশি যেন এখানে পড়ে
উত্তর “হ্যাঁ” → আগে মোড়ানোর চেষ্টা করুনraw রিসোর্সকে একটি মালিক-টাইপে ভরুন
int* px;                 →  std::vector<int> px;
Engine* e;               →  std::unique_ptr<Engine> e;
FILE* f;  (fopen/fclose) →  std::unique_ptr<FILE, Closer> f;   // custom deleter, পর্ব ১৭

মোড়ানো গেলে → আবার Rule of Zero
মোড়ানো না গেলে → Rule of Fiveআপনি নিজেই একটি “মালিক-টাইপ” লিখছেন
~T();                              // ছাড়ে
T(const T&);  T& operator=(const T&);   // deep copy — অথবা = delete
T(T&&) noexcept;  T& operator=(T&&) noexcept;   // চুরি করে, source খালি

class টি শুধু এই রিসোর্সটিই সামলাবে, আর কিছু নয়।
(Rule of Three = শুধু প্রথম তিনটি; move নেই বলে rvalue থেকেও কপি হয় — ঠিক, কিন্তু ধীর)
  1. মালিকানা খুঁজুন। প্রতিটি member দেখে জিজ্ঞেস করুন: এর ফেরত দেওয়ার দায়িত্ব কি আমার class-এর? std::string, std::vector, smart pointer নিজের দায়িত্ব নিজে নেয়। শুধু আপনার হাতে new করা raw pointer বা C লাইব্রেরির handle আপনার দায়িত্ব।
  2. Rule of Zero — সেরা উত্তর। সরাসরি কোনো রিসোর্স না থাকলে একটিও special member লিখবেন না। সত্যিকারের প্রজেক্টে বেশিরভাগ class এই দলে — Student, Order, Album, Config. এখানে destructor লিখলে বরং ক্ষতি, কারণ move হারায়।
  3. আগে মুড়িয়ে দিন। raw রিসোর্স থাকলে প্রথম প্রশ্ন: একে কি vector বা unique_ptr দিয়ে মোড়ানো যায়? প্রায় সবসময় যায় — C লাইব্রেরির FILE*-ও custom deleter সহ unique_ptr-এ ঢোকে। মোড়ানো গেলেই আপনি আবার Rule of Zero-তে।
  4. শেষ উপায় — Rule of Five. নিজেই নতুন ধরনের মালিক-টাইপ লিখছেন (যেমন নিজের container, বা এমন handle যার জন্য কোনো তৈরি টাইপ নেই)? তাহলে পাঁচটির প্রতিটি সিদ্ধান্ত নিন — লিখুন, default করুন বা delete করুন — আর move-এ noexcept দিন। এমন class ছোট রাখুন: শুধু একটি রিসোর্স, আর কোনো ব্যবসার যুক্তি নয়।

৮ · = delete আর = default — কপি-অযোগ্য টাইপ বানানো

কিছু জিনিস কপি করার কোনো অর্থই হয় না। একটি ডেটাবেস সংযোগের কথা ভাবুন: সার্ভারের সাথে একটি খোলা নেটওয়ার্ক সংযোগ, যার পেছনে সার্ভারে একটি session চলছে। এই object “কপি” করলে কী হবে — একই সংযোগ দুজনে ব্যবহার করবে, আর দুজনেই একদিন বন্ধ করবে? নাকি গোপনে নতুন সংযোগ খুলবে, যা ধীর আর ব্যয়বহুল? দুটোই বিভ্রান্তিকর। সবচেয়ে সৎ উত্তর: এটা কপি করা যাবে না। কিন্তু মালিকানা হাতবদল (move) পুরোপুরি যুক্তিসঙ্গত — যেমন একটি connection pool থেকে সংযোগ একটি কাজের হাতে দেওয়া।

C++11 থেকে এর জন্য আছে = delete. যে ফাংশন delete করা হয়, তাকে ডাকার চেষ্টা করলেই compile error:

class DatabaseConnection {
    DbHandle* h = nullptr;                       // C লাইব্রেরির handle
public:
    explicit DatabaseConnection(const std::string& url) : h(db_open(url.c_str())) {}
    ~DatabaseConnection() { if (h) db_close(h); }

    DatabaseConnection(const DatabaseConnection&) = delete;              // কপি নিষেধ
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;

    DatabaseConnection(DatabaseConnection&& o) noexcept                  // হাতবদল চলে
        : h(std::exchange(o.h, nullptr)) {}
    DatabaseConnection& operator=(DatabaseConnection&& o) noexcept {
        if (this != &o) { if (h) db_close(h); h = std::exchange(o.h, nullptr); }
        return *this;
    }
};

DatabaseConnection a("db://shop");
DatabaseConnection b = a;              // error: use of deleted function
DatabaseConnection c = std::move(a);   // ঠিক আছে — a এখন খালি

এটাই একটি move-only type, ঠিক পর্ব ১৭-এর std::unique_ptr-এর মতো — আর আসলে unique_ptr নিজেও ঠিক এভাবেই তার copy delete করে রেখেছে। C++11-এর আগে প্রোগ্রামাররা copy constructor-কে private করে আর সংজ্ঞা না দিয়ে একই কাজ করতেন; error আসত, কিন্তু অস্পষ্ট বার্তায়, কখনো link করার সময়। = delete উদ্দেশ্যটা কোডেই লিখে রাখে।

আরও ভালো, Rule of Zero দিয়েও একই ফল পাওয়া যায়: handle-টিকে custom deleter সহ std::unique_ptr<DbHandle, Closer>-এ রাখুন। unique_ptr কপি-অযোগ্য, তাই compiler-এর বানানো copy নিজে থেকেই deleted হবে; move-এ unique_ptr move হবে, noexcept সহ; আর destructor-ও লিখতে হবে না। পাঁচটি ফাংশনের কাজ একটি member করে দিল।

উল্টো দিকে আছে = default: “compiler, আপনার স্বাভাবিক সংস্করণটিই বানিয়ে দিন।” এটি কাজে লাগে যখন কোনো নিয়মে সদস্যটি হারিয়ে যায় অথচ আপনি সেটা চান — যেমন অংশ ৭-এর Album-এ destructor রাখার পর move ফিরিয়ে আনা, বা অন্য constructor লেখার পর default constructor ফিরিয়ে আনা (Image() = default;). আর পর্ব ২৪-২৫-এ দেখবেন, inheritance-এর base class-এ virtual ~Shape() = default; লেখার চল আছে — তখনও এটি একটি ঘোষিত destructor, তাই move হারায়; দরকার হলে বাকিগুলোও = default করতে হয়।

পরীক্ষা ও ইন্টারভিউতে
  • Copy constructor আর copy assignment-এর পার্থক্য কী? — copy constructor নতুন object বানায় (T b = a;), assignment আগে থেকে থাকা object-এ কপি বসায় (b = a;) আর তাই পুরোনো রিসোর্সও সামলায়।
  • Copy constructor-এর প্যারামিটার by value হতে পারে না কেন? — by value নিতে হলে প্যারামিটারটি বানাতেই copy constructor লাগবে, যা আবার নিজেকে ডাকবে — অসীম চক্র; তাই const T&.
  • Shallow আর deep copy কী? — shallow copy শুধু pointer-এর ঠিকানা কপি করে (দুজনে একই মেমরি, double delete); deep copy নতুন মেমরি নিয়ে ভেতরের ডেটাসহ কপি করে।
  • Rule of Three/Five/Zero কী? — destructor, copy ctor, copy assign (C++11 থেকে move ctor, move assign সহ পাঁচটি) — একটি লাগলে সবগুলোর কথা ভাবুন; সবচেয়ে ভালো, মালিক-টাইপ member রেখে একটিও না লেখা।
  • Move constructor কেন noexcept হওয়া উচিত? — না হলে std::vector বড় হওয়ার সময় strong guarantee রক্ষায় move-এর বদলে কপি করে।

৯ · AI-এর যুগে — boilerplate নয়, মালিকানা যাচাই

“একটি C++ class লেখো” বললে AI টুল প্রায়ই পাঁচটি special member সহ লম্বা কোড দেয়। দেখে পেশাদার মনে হয়, কিন্তু এই কোডের প্রতিটি লাইন একটি সম্ভাব্য বাগ। অনলাইনে Rule of Five শেখানোর অসংখ্য টিউটোরিয়াল আছে, যেখানে উদাহরণের খাতিরে raw pointer ব্যবহার করা হয়েছে — AI সেই ছাঁচই নকল করে, এমনকি যখন একটি std::vector member-ই যথেষ্ট ছিল। সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
char* বা int* member আর হাতে লেখা পাঁচটি সদস্যবাগের অনেক সুযোগ, অথচ কোনো লাভ নেইstd::string/std::vector member, Rule of Zero
move constructor-এ noexcept নেইvector বড় হওয়ার সময় move না করে কপি করে — নীরবে ধীরT(T&&) noexcept
move-এর পর source-এর pointer nullptr করা হয়নিদুজনেই একই ব্লক মোছে — double deletestd::exchange(o.px, nullptr)
copy assignment শুরুতেই delete[], self-check বা copy-and-swap নেইa = a-তে মোছা মেমরি পড়া; new ব্যর্থ হলে ভাঙা objectcopy-and-swap
শুধু একটি destructor যোগ (logging-এর জন্য), বাকিগুলো নেইmove হারায়, প্রতিটি move আসলে deep copydestructor বাদ দিন, বা পাঁচটিই = default
copy constructor-এ কিছু member কপি করতে ভুলে যাওয়ানতুন member যোগ হলে কপিতে সেটি বাদ পড়েRule of Zero — compiler কোনো member ভোলে না
কপি-অযোগ্য রিসোর্সে (সংযোগ, lock) copy খোলা রাখাদুজন মালিক, দুবার বন্ধcopy = delete, বা unique_ptr member

AI-কে কোড লিখতে বলার সময় নিয়মগুলো আগেই দিয়ে দিন, আর তার প্রতিটি সিদ্ধান্তের কারণ চেয়ে নিন:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি ছবি-সম্পাদনা অ্যাপের জন্য তিনটি টাইপ লেখো: Image (width, height আর pixel-এর তালিকা), Album (নাম আর অনেকগুলো Image), আর DatabaseConnection (একটি C লাইব্রেরির db_open/db_close handle মোড়ায়)। নিয়ম: ১. Rule of Zero আগে: যেখানে std::string, std::vector বা std::unique_ptr দিয়ে কাজ চলে, সেখানে কোনো special member লিখবে না। কোথাও new বা delete লিখবে না। ২. DatabaseConnection কপি করা যাবে না, কিন্তু move করা যাবে। unique_ptr আর custom deleter দিয়ে করো; না পারলে Rule of Five মেনে লেখো, move-এ noexcept দিয়ে। ৩. কোনো class-এ destructor লিখলে বলো, তাতে কোন special member হারাল আর তুমি কীভাবে ফিরিয়ে আনলে। ৪. প্রতিটি টাইপের জন্য একটি টেবিল দাও: copy চলে কি না, move চলে কি না, move noexcept কি না — আর কেন। ৫. একটি ছোট main লেখো, যা static_assert(std::is_nothrow_move_constructible_v<Image>) আর static_assert(!std::is_copy_constructible_v<DatabaseConnection>) দিয়ে এগুলো যাচাই করে।

AI-এর লেখা class যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১raw pointer থাকলে compiler-এর বানানো copy ভুল কাজ করে — তবু C++ শুরু থেকেই প্রতিটি class-এ নিজে থেকে copy বানিয়ে দেয় কেন? ডিফল্টটা “কপি নিষেধ” হলো না কেন?

উত্তরের মূল কারণ C-এর সাথে সামঞ্জস্য। C++ জন্মেছিল “C with Classes” হিসেবে, আর Bjarne Stroustrup-এর একটি প্রধান লক্ষ্য ছিল যে বিদ্যমান C কোড প্রায় অপরিবর্তিত অবস্থায় C++ হিসেবে compile হবে। C-তে struct assign করা আর ফাংশনে by value পাঠানো বহু আগে থেকেই চালু, আর তার অর্থ সব বাইট কপি। C++-এ struct আর class আসলে একই জিনিস (শুধু ডিফল্ট access আলাদা)। তাই যদি class-এর ডিফল্ট “কপি নিষেধ” হতো, তাহলে প্রতিটি C struct-ও কপি-অযোগ্য হয়ে যেত, আর লাখো লাইন C কোড ভেঙে পড়ত।

দ্বিতীয় কারণ value semantics-এর দর্শন। Stroustrup চেয়েছিলেন নিজের বানানো টাইপ যেন int-এর মতোই আচরণ করে — কপি করা যায়, ফাংশনে পাঠানো যায়, ফেরত দেওয়া যায়। বেশিরভাগ class-এ, যেখানে member-গুলো নিজেরাই সঠিকভাবে কপি হয়, memberwise copy ঠিক এটাই দেয়। সমস্যা শুধু সেই অল্প class-এ, যারা নিজে raw রিসোর্স সামলায়। নকশাটা তাই ছিল: সাধারণ ক্ষেত্রে সুবিধা, বিশেষ ক্ষেত্রে প্রোগ্রামার নিজে নিয়ম লিখবে।

এই নকশার মূল্য পরের কয়েক দশক ধরে দিতে হয়েছে — Rule of Three-এর মতো নিয়ম জন্মেছে এই ফাঁক পূরণের জন্যই। C++11-এ standard কমিটি একটি আংশিক সংশোধন আনে: যে class-এ ব্যবহারকারী নিজে destructor বা copy-র একটি লিখেছেন, সেখানে বাকি copy-র স্বয়ংক্রিয় তৈরি deprecated ঘোষণা করা হয়। কিন্তু সেটি পুরোপুরি সরানো হয়নি, কারণ সরালে আবার পুরোনো কোড ভাঙে — এটাই backward compatibility-র চিরন্তন টানাপোড়েন। নতুন সদস্য move-এর ক্ষেত্রে কমিটি অবশ্য সাবধানী পথ নেয়: সন্দেহ থাকলে (destructor বা copy লেখা থাকলে) move বানানোই হয় না, কারণ নতুন বৈশিষ্ট্যে পুরোনো কোড ভাঙার ভয় ছিল না।

অন্য ভাষার সাথে তুলনা করলে নকশাটা আরও স্পষ্ট হয়। Rust-এ ডিফল্ট উল্টো: একটি টাইপ নিজে থেকে কপি হয় না, শুধু move হয়; কপি চাইলে স্পষ্ট করে Clone বা Copy চাইতে হয়। Rust-এর কোনো C-সামঞ্জস্যের বোঝা ছিল না, তাই নিরাপদ ডিফল্ট বেছে নিতে পেরেছে।

মূল উপলব্ধি: C++-এর অনেক “অদ্ভুত” নিয়মের পেছনে থাকে একই কারণ — পুরোনো কোড না ভাঙা। ডিফল্ট copy ছিল C-এর উত্তরাধিকার; Rule of Zero হলো আধুনিক C++-এর উপায়, যাতে সেই ডিফল্ট সবসময় ঠিক কাজ করে।

প্র ০২vector কেন throwing move-এ ভরসা করে না? strong guarantee আর noexcept-এর জন্ম-ইতিহাসটা কী?

C++98-এর std::vector একটি মূল্যবান প্রতিশ্রুতি দিত: push_back করার সময় কিছু ব্যর্থ হলে vector ঠিক আগের অবস্থায় থাকবে। এটি সম্ভব হয়েছিল কারণ তখন বড় হওয়ার সময় পুরোনো উপাদানগুলো নতুন ব্লকে কপি হতো। কপির সময় মূলগুলো ছোঁয়াই হয় না; মাঝপথে একটি কপি exception ছুড়লে নতুন ব্লকের তৈরি হওয়া কপিগুলো ধ্বংস করে ফেলে দিলেই হলো। exception-safety-র এই ধারণাগুলো — basic, strong আর nothrow guarantee — ১৯৯০-এর দশকের শেষভাগে Dave Abrahams-এর কাজের মাধ্যমে standard library-র নকশায় প্রতিষ্ঠিত হয়।

C++11-এর move আসার পর একটি সংকট দেখা দেয়। বড় হওয়ার সময় উপাদানগুলো move করলে অনেক দ্রুত হয়, কিন্তু move মূল উপাদানকে বদলে দেয় (খালি করে)। ধরুন ১০০টির মধ্যে ৫০টি সরানো হয়েছে, আর ৫১তম move exception ছুড়ল। আগের ৫০টি ফেরত আনতে আবার move লাগবে, যা আবারও ব্যর্থ হতে পারে। strong guarantee রাখার আর কোনো উপায় থাকে না। আর পুরোনো কোড, যা এই প্রতিশ্রুতির ওপর নির্ভর করে লেখা, নীরবে ভেঙে যেত।

এই সমস্যা নিয়ে ২০০৯ সালে Dave Abrahams, Rani Sharoni আর Doug Gregor একটি প্রস্তাব দেন, যার ফল হিসেবে C++11-এ noexcept keyword আর std::move_if_noexcept আসে। সমাধানটি সুন্দর: library compile-এর সময়েই জিজ্ঞেস করে, “এই টাইপের move কি ব্যর্থ না হওয়ার প্রতিশ্রুতি দিয়েছে?” হ্যাঁ হলে move — দ্রুত এবং নিরাপদ, কারণ মাঝপথে ব্যর্থতা অসম্ভব। না হলে, আর কপি করা গেলে, পুরোনো দিনের মতো কপি — ধীর কিন্তু নিরাপদ। আর টাইপটি যদি একেবারেই কপি করা না যায় (move-only), তখন throwing move-ই ব্যবহার হয়, আর standard মেনে নেয় যে তখন strong guarantee থাকবে না।

noexcept-এর আরেকটি দিক: এটি একটি চুক্তি, যা compiler চলার সময়ে কঠোরভাবে প্রয়োগ করে। noexcept ফাংশন থেকে exception বেরোতে চাইলে প্রোগ্রাম সরাসরি std::terminate দিয়ে বন্ধ হয়, catch পর্যন্ত যায় না। এই কঠোরতাই library-কে ভরসা দেয়। ফলে noexcept লাগানোর আগে নিশ্চিত হোন — pointer আর int অদলবদল করা move-এ সেটা সহজেই নিশ্চিত।

মূল উপলব্ধি: দ্রুততা আর নিরাপত্তার দ্বন্দ্বে C++ নিরাপত্তা বেছে নিয়েছে, কিন্তু আপনাকে একটি শব্দে দুটোই পাওয়ার সুযোগ দিয়েছে। noexcept হলো library-কে দেওয়া আপনার প্রতিশ্রুতি — আর সেই প্রতিশ্রুতির বিনিময়ে library আপনাকে দেয় গতি।

প্র ০৩Howard Hinnant-এর special member টেবিলের নিয়মগুলো এলোমেলো মনে হয়। এর পেছনের যুক্তিটা কী, আর তাঁর পরামর্শ কী?

Howard Hinnant C++11-এর move semantics প্রস্তাবের প্রধান লেখকদের একজন; ২০০২ সালে তিনি Peter Dimov আর Dave Abrahams-এর সাথে প্রথম প্রস্তাবটি দেন, আর পরে libc++ লাইব্রেরি তৈরিতেও নেতৃত্ব দেন। ২০১৪ সালের দিকে একটি আলোচিত বক্তৃতায় তিনি special member-এর নিয়মগুলো একটি টেবিলে দেখান, যা অংশ ৭-এ আপনি দেখেছেন। টেবিলটা প্রথম দেখায় এলোমেলো লাগে, কিন্তু এর পেছনে দুটি সরল নীতি আছে।

প্রথম নীতি: যে নিজে রিসোর্স সামলায়, তার জন্য compiler move আন্দাজ করবে না। আপনি destructor বা copy লিখেছেন মানে আপনার class-এ এমন কিছু আছে যা memberwise কাজ সঠিক নয়। সেখানে compiler যদি memberwise move বানাত, তাহলে সেটিও ভুল হতো — ঠিক Image-এর shallow copy-র মতো দুজনের হাতে একই pointer. তাই move বানানো হয় না, আর rvalue থেকেও কপি চলে — ধীর, কিন্তু সঠিক। নতুন বৈশিষ্ট্য পুরোনো সঠিক কোডকে ভাঙবে না, এটাই লক্ষ্য।

দ্বিতীয় নীতি: আপনি move লিখেছেন মানে আপনি C++11-এর নিয়ম জানেন, তাই copy-ও নিজে ঠিক করুন। move ঘোষণা করলে copy দুটি deleted হয়ে যায়। এতে move-only টাইপ বানানো সহজ হয়, আর ভুল করে memberwise copy থেকে যাওয়ার সুযোগ থাকে না। প্রথম নীতিতে পুরোনো কোড রক্ষা, দ্বিতীয়টিতে নতুন কোডে কঠোরতা।

আর “deprecated” ঘরগুলো? সেগুলো একটি আপস। যুক্তি অনুযায়ী destructor লেখা class-এ copy-ও বানানো উচিত ছিল না, কিন্তু C++98-এর লাখো class এর ওপর নির্ভর করে। তাই সেগুলো এখনো বানানো হয়, কিন্তু standard আনুষ্ঠানিকভাবে সতর্ক করে রেখেছে।

Hinnant-এর ব্যবহারিক পরামর্শ ছিল মোটামুটি এরকম: টেবিল মুখস্থ করার চেষ্টা না করে, special member নিয়ে কিছু করলে প্রতিটিকে স্পষ্ট করে ঘোষণা করুন — লিখুন, = default বা = delete — যাতে পাঠক আর compiler দুজনেই নিশ্চিত থাকে। আর একটি গুরুত্বপূর্ণ সতর্কতা: move সদস্যদের কখনো = delete করবেন না, যদি না সত্যিই চান যে rvalue থেকে কপিও না হোক — deleted move overload resolution-এ অংশ নেয়, ফলে temporary থেকে কপির পথও বন্ধ হয়ে যায়। move না চাইলে শুধু ঘোষণাই করবেন না।

মূল উপলব্ধি: টেবিলের প্রতিটি ঘর একটি প্রশ্নের উত্তর — “এখানে আন্দাজ করলে পুরোনো কোড কি ভাঙবে, নতুন কোডে কি বাগ ঢুকবে?” নিয়মগুলো মুখস্থের চেয়ে সহজ পথ হলো Rule of Zero, যেখানে টেবিলের প্রথম সারিই আপনার একমাত্র সারি।

প্র ০৪standard বলে moved-from object থাকে “valid but unspecified” অবস্থায়। এর মানে ঠিক কী, আর standard কেন “খালি” বলে নিশ্চিত করল না?

standard library-র নিয়মে, তার কোনো টাইপের object থেকে move করা হলে সেটি থাকে “valid but unspecified state”-এ। “valid” মানে object টির নিজস্ব invariant (যে শর্ত সবসময় সত্য থাকার কথা) এখনো রক্ষিত: একটি moved-from std::string এখনো একটি সঠিক string, তার size() একটি সঠিক সংখ্যা ফেরত দেবে, তাকে ধ্বংস করা নিরাপদ। “unspecified” মানে সেই মান কী, তা বলা নেই — খালিও হতে পারে, পুরোনো মানও থাকতে পারে।

কেন “খালি” নিশ্চিত করা হলো না? কারণ তাতে দক্ষতা হারানোর ঝুঁকি। একটি উদাহরণ দেখুন: বেশিরভাগ std::string বাস্তবায়নে ছোট লেখা heap-এ না রেখে object-এর ভেতরেই রাখা হয় (small string optimization). এমন string move করতে হলে আসলে অক্ষরগুলো কপিই করতে হয়, কারণ চুরি করার মতো কোনো heap pointer নেই। এখন standard যদি বাধ্য করত যে source-কে খালিও করতে হবে, তাহলে প্রতিটি move-এ একটি বাড়তি কাজ — যা অনেক ক্ষেত্রে দরকারই নেই, কারণ moved-from object টি সাধারণত পরের মুহূর্তেই ধ্বংস হবে। বাস্তবে প্রধান বাস্তবায়নগুলো moved-from string খালিই রাখে, কিন্তু standard বাস্তবায়নকারীদের স্বাধীনতা রেখেছে।

তাহলে moved-from object নিয়ে আপনি কী করতে পারেন? নিরাপদ তিনটি কাজ: ধ্বংস হতে দেওয়া, নতুন মান assign করা (s = "new";), আর এমন ফাংশন ডাকা যার কোনো পূর্বশর্ত নেই (s.clear(), s.size(), v.empty())। ভুল কাজ হলো ভেতরের মান সম্পর্কে কিছু ধরে নেওয়া, যেমন v.front() ডাকা, যার পূর্বশর্ত vector খালি নয়। কিছু টাইপ বাড়তি নিশ্চয়তা দেয়: std::unique_ptr আর std::shared_ptr move-এর পর নিশ্চিতভাবে খালি।

অন্য ভাষার দিকে তাকালে একটি বিকল্প নকশা দেখা যায় — Rust-এর “destructive move”: move-এর পর পুরোনো নাম ব্যবহারই করা যায় না, compiler তা ধরে ফেলে, আর পুরোনো object-এর destructor চলেই না। C++ এই পথে যায়নি, কারণ C++-এ move শুধু একটি সাধারণ ফাংশন-ডাক (move constructor), আর source object-এর জীবন তার scope অনুযায়ী চলে; ফলে তার destructor চলবেই, আর তাই তাকে বৈধ থাকতেই হবে। এই কারণেই আপনার নিজের move-এ source-কে nullptr করা বাধ্যতামূলক।

মূল উপলব্ধি: moved-from object একটি খালি বাক্স নয়, একটি “অজানা কিন্তু অক্ষত” বাক্স। একে ফেলে দিন বা নতুন জিনিস ভরুন — কিন্তু ভেতরে কী আছে, ধরে নেবেন না।

প্র ০৫Rule of Zero কীভাবে জন্মাল, আর এটি আসলে কোন বড় ডিজাইন-নীতির একটি রূপ?

C++11 আসার পর অনেক প্রোগ্রামার একটি অস্বস্তিতে পড়েন। Rule of Three ছিল তিনটি ফাংশন; এখন Rule of Five — পাঁচটি, আর প্রতিটিতে exception, self-assignment, noexcept-এর সূক্ষ্মতা। প্রতিটি নতুন class-এ এই boilerplate লেখা মানে প্রতিবার বাগের নতুন সুযোগ। ২০১২ সালে R. Martinho Fernandes তাঁর ব্লগে একটি ছোট লেখায় “Rule of Zero” নামটি দেন। তাঁর যুক্তি ছিল: যে class-এর আসল কাজ মালিকানা সামলানো নয়, সে কোনো special member লিখবে না; মালিকানার কাজ আলাদা, ছোট, বিশেষ class-এ থাকবে — আর standard library ইতিমধ্যে সেগুলো দিয়ে রেখেছে (std::unique_ptr, std::shared_ptr, container).

এর পেছনের বড় নীতিটি হলো single responsibility — প্রতিটি class-এর একটিই দায়িত্ব। একটি Album-এর দায়িত্ব ছবি সাজানো; মেমরি ফেরত দেওয়া তার দায়িত্ব নয়। মেমরি সামলানো vector-এর দায়িত্ব, আর vector সেটা হাজার হাজার প্রোগ্রামারের হাতে পরীক্ষিত কোডে করে। আপনার নিজের হাতে লেখা copy constructor কখনো ততটা পরীক্ষিত হবে না। পর্ব ২৭-এ এই নীতিকে SOLID-এর অংশ হিসেবে আবার দেখবেন।

Rule of Zero-র একটি নীরব কিন্তু বড় সুবিধা রক্ষণাবেক্ষণে। হাতে লেখা copy constructor-ওয়ালা class-এ ছয় মাস পর কেউ একটি নতুন member যোগ করলেন, কিন্তু copy constructor-এ সেটা যোগ করতে ভুলে গেলেন — কোনো error নেই, শুধু কপিতে সেই member চুপচাপ ডিফল্ট মান পায়। compiler-এর বানানো copy কখনো কোনো member ভোলে না।

নিয়মটি নিয়ে আলোচনাও হয়েছে। Scott Meyers ২০১৪ সালে একটি লেখায় একটি উদ্বেগ তোলেন: polymorphic base class-এ virtual destructor লাগে, আর একটি destructor ঘোষণা করলেই move হারায়; অথবা ডিবাগের জন্য কেউ destructor যোগ করলেন, আর অজান্তে move চলে গেল। তাঁর প্রস্তাব ছিল, এমন ক্ষেত্রে পাঁচটিকেই স্পষ্টভাবে = default ঘোষণা করা — যাকে কেউ কেউ “Rule of Five defaults” বলেন। দুটি অবস্থান আসলে সাংঘর্ষিক নয়: যতদূর সম্ভব কিছু লিখবেন না; কিছু লিখতে বাধ্য হলে সবগুলো স্পষ্ট করে লিখবেন। C++ Core Guidelines-এর C.20 আর C.21 নিয়ম ঠিক এই দুটি কথাই বলে।

মূল উপলব্ধি: সবচেয়ে নির্ভুল কোড হলো যে কোড লেখাই হয়নি। Rule of Zero কোনো C++-বিশেষ কৌশল নয় — এটা দায়িত্ব ভাগ করার পুরোনো প্রকৌশল-বুদ্ধি: রিসোর্স সামলানোর কঠিন কাজ একবার, এক জায়গায়, ভালোভাবে করুন, আর বাকি সবখানে সেটা পুনরায় ব্যবহার করুন।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. আউটপুট অনুমান: উপরের image_special_members.cpp-এর main-এর ভেতরটা বদলে লিখুন Image a(3); Image b(a); b = a; — পুরো আউটপুট কোন ক্রমে আসবে?

    সাতটি লাইন: ctor n=3 (a), copy ctor n=3 (b), তারপর b = a;-এর জন্য প্যারামিটার o by value, a একটি lvalue বলে কপি হয়ে আসে — আবার copy ctor n=3; তারপর assign n=3; তারপর o ধ্বংস হয়ে b-এর পুরোনো ব্লক নিয়ে যায় — dtor n=3. শেষে উল্টো ক্রমে dtor n=3 (b) আর dtor n=3 (a). লক্ষ করুন, lvalue থেকে assign করলে copy-and-swap-এ একটি copy constructor চলে — নতুন ব্লক নেওয়াটা সেখানেই হয়।

  2. কোন নিয়ম? প্রতিটির জন্য বলুন Rule of Zero, Five, নাকি copy delete — কারণসহ: (ক) struct Student { std::string name; std::vector<int> marks; }; (খ) একটি Matrix class, যে double* data = new double[r*c] রাখে (গ) একটি Logger, যে constructor-এ fopen আর destructor-এ fclose করে (ঘ) একটি Order, যার ভেতরে std::unique_ptr<Payment>.

    (ক) Rule of Zero — দুটি member-ই নিজের রিসোর্স সামলায়। (খ) সবচেয়ে ভালো: std::vector<double> data; করে Rule of Zero; raw pointer রাখতেই হলে Rule of Five, deep copy আর noexcept move সহ। (গ) একটি খোলা ফাইল কপি করার মানে স্পষ্ট নয় — std::unique_ptr<FILE, Closer> member দিন: copy নিজে থেকেই deleted, move চলে, destructor লাগে না (Rule of Zero, move-only). অথবা পুরো Logger-এর জায়গায় std::ofstream. (ঘ) Rule of Zero — Order নিজে থেকে move-only হবে; কপি চাইলে তখনই নিজে copy লিখে Payment-এর deep copy-র সিদ্ধান্ত নিতে হবে।

  3. বাগ শিকার: একটি AI এই move constructor দিয়েছে — Buffer(Buffer&& o) : size(o.size), data(o.data) {}, আর copy assignment — Buffer& operator=(const Buffer& o) { delete[] data; size = o.size; data = new char[size]; std::copy(o.data, o.data + size, data); return *this; }. destructor delete[] data করে। কয়টি সমস্যা খুঁজে পান?

    চারটি। (১) move-এর পর o.data nullptr করা হয়নি — দুজনেই একই ব্লক মুছবে, double delete. data(std::exchange(o.data, nullptr)) আর size(std::exchange(o.size, 0)) লিখুন। (২) move-এ noexcept নেই — vector-এ রাখলে move না করে কপি করবে। (৩) copy assignment-এ self-assignment-এ মোছা মেমরি থেকে পড়া হয়। (৪) new ব্যর্থ হলে data মোছা ঠিকানা ধরে থাকে — ভাঙা object, পরে আবার delete. (৩) আর (৪)-এর একসাথে সমাধান copy-and-swap. আর সবচেয়ে ভালো সমাধান: std::vector<char> member — পুরো কোডটাই মুছে ফেলা যায়।

  4. নিজে লিখুন (কোড): vector_noexcept.cpp-এ দুটি পরীক্ষা করুন, প্রতিবার আগে আউটপুট অনুমান করে: (ক) Safe-এর move constructor থেকে noexcept মুছে দিন। (খ) সেটা ফিরিয়ে দিয়ে এবার Risky-র copy constructor-কে Risky(const Risky&) = delete; করে দিন।

    (ক) এবার প্রথম লাইনও grew using: copy ctor — noexcept ছাড়া Safe-ও Risky-র মতো আচরণ করে। (খ) Risky এখন কপিই করা যায় না, তাই vector-এর আর কোনো বিকল্প নেই — সে throwing move-ই ব্যবহার করে: vector<Risky> grew using: move ctor. তখন exception হলে vector strong guarantee দিতে পারে না — standard এই ক্ষেত্রে ফল “unspecified” রেখেছে। শিক্ষা: noexcept শুধু গতির জন্য নয়, নিরাপত্তার প্রতিশ্রুতির জন্যও।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমcopy-and-swap কৌশলে লেখা copy assignment b = a; চলার সময় ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. a থেকে একটি অস্থায়ী কপি tmp বানানো হয় (নতুন মেমরি নেওয়া হয়)
  2. কপি সফল — b এখনো একদম অক্ষত
  3. b আর tmp-এর ভেতরের pointer ও size অদলবদল হয়
  4. b-এর রেফারেন্স (*this) ফেরত দেওয়া হয়
  5. tmp-এর destructor b-এর পুরোনো মেমরি মুছে দেয়

নিচের কোনটি C++-এর special member function নয়?

  • copy constructor
  • move assignment operator
  • destructor
  • operator==

C++-এ একটি class-এ শুধু একটি user-declared destructor আছে, আর কোনো special member লেখা নেই। T b = std::move(a); লিখলে কোনটি চলে?

  • compiler-এর বানানো move constructor
  • compiler-এর বানানো copy constructor
  • Compile error
  • কোনো constructor চলে না, std::move নিজেই সরিয়ে দেয়

C++-এ একটি class constructor-এ new int[100] করে raw pointer-এ রাখে আর destructor-এ delete[] করে, কিন্তু copy constructor লেখা নেই। { T a; T b = a; } চললে কী হয়?

  • b নিজের নতুন ১০০টি int পায়, সব ঠিক থাকে
  • Compile error — copy constructor নেই
  • দুজনে একই মেমরি ধরে, scope শেষে দুবার delete[] — undefined behavior
  • মেমরি কখনো ফেরত যায় না — memory leak

C++-এ একটি টাইপের move constructor noexcept নয়, কিন্তু copy constructor আছে। std::vector বড় হওয়ার সময় পুরোনো উপাদানগুলো সাধারণত কীভাবে নতুন জায়গায় নেয়?

  • কপি করে — যাতে ব্যর্থ হলে vector আগের অবস্থায় থাকে
  • সবসময় move করে, কারণ move দ্রুত
  • Compile error দেয়
  • memcpy দিয়ে বাইট কপি করে

C++-এ “Rule of Zero” বলতে কী বোঝায়?

  • কোনো class কপি করা যাবে না
  • std::string, std::vector, unique_ptr-এর মতো member রেখে কোনো special member নিজে না লেখা
  • সব special member = delete করা
  • একটি ফাঁকা destructor লিখে রাখা

C++-এ একটি class-এ Conn(const Conn&) = delete; আর Conn& operator=(const Conn&) = delete; লেখা আছে। এর ফল কী?

  • compiler নিজের memberwise copy বানিয়ে নেয়
  • কপি করলে চলার সময় exception ছোড়ে
  • Conn কপি করার চেষ্টা compile error — টাইপটি কপি-অযোগ্য
  • Conn-এর object তৈরি করাই যায় না

শব্দকোষ

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

মনে রাখুন

  1. ছয়টি special member: default ctor, copy ctor, copy assign, move ctor, move assign, destructor. compiler-এর সংস্করণ memberwise — raw pointer হলে শুধু ঠিকানা কপি, অর্থাৎ shallow copy আর double delete.
  2. Deep copy নতুন মেমরি নেয়; copy assignment-এ copy-and-swap দিন — self-assignment আর exception দুটোতেই নিরাপদ।
  3. Move চাবি হাতবদল করে: pointer নিয়ে নিন, source-কে nullptr করুন (std::exchange), আর noexcept দিন — নইলে vector কপি করে।
  4. Rule of Zero আগে, মোড়ানো না গেলে Rule of Five. শুধু একটি destructor লিখলেই move হারায়।
  5. = delete কপি-অযোগ্য টাইপ বানায় (সংযোগ, lock, unique_ptr); = default হারানো সদস্য ফিরিয়ে আনে। moved-from object-এ নতুন মান না দিয়ে তা পড়বেন না।

পরের পর্বে: copy assignment আসলে একটি অপারেটর — operator=. একইভাবে আপনার টাইপের জন্য +, ==, << বা []-এর অর্থও নিজে ঠিক করে দেওয়া যায়। কখন সেটা টাইপকে স্বাভাবিক করে তোলে, আর কখন বিভ্রান্তিকর — operator overloading.

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

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