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

operator overloading — নিজের টাইপে +, == আর <<

Operator Overloading — Making Your Types Feel Built-in

Taught in Bangla, this lesson covers operator overloading in C++: operators as specially named functions, member versus non-member and hidden friends, building + from +=, the C++20 spaceship operator, printing with operator<< and std::formatter, [], () and ++, and conversion operators.

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

  • operator overloading কী, compiler কীভাবে a + b-কে একটি ফাংশন ডাকায় বদলায়, আর কখন overload করা উচিত — কখন একেবারেই নয়
  • member আর non-member operator-এর পার্থক্য, symmetry-র সমস্যা, আর hidden friend কৌশল
  • += থেকে + বানানো, C++20-এর operator<=> আর strong, weak, partial ordering-এর ধারণা
  • নিজের টাইপ std::cout আর std::println দিয়ে ছাপা, [], (), ++ আর explicit operator bool
  • AI-এর লেখা operator কোডের সাধারণ ভুল চেনা আর যাচাইয়ের চেকলিস্ট

১ · সমস্যাটা আগে — নিজের টাইপ কেন built-in-এর মতো হবে?

পর্ব ২০ থেকে ২২-এ আপনি নিজের class বানাতে শিখেছেন — constructor, destructor, copy আর move সহ। এখন ধরুন একটি চায়ের দোকানের হিসাবের সফটওয়্যার লিখছেন। টাকার হিসাব double-এ রাখা বিপজ্জনক, কারণ floating-point সংখ্যা 0.1-এর মতো দশমিক ঠিকঠাক ধরতে পারে না; লাখ লাখ লেনদেনে পয়সার গরমিল জমে যায়। তাই আপনি একটি Money class বানালেন, যা ভেতরে সব টাকা পয়সায়, পূর্ণসংখ্যা হিসেবে রাখে: ১৫ টাকা মানে 1500 পয়সা, ১২ টাকা ৫০ পয়সা মানে 1250.

class তো হলো, এবার ব্যবহার। দুই কাপ চা আর একটি শিঙাড়ার দাম যোগ করতে হলে, operator ছাড়া লিখতে হয় এরকম:

Money bill = add(add(tea, tea), samosa);          // ফাংশন দিয়ে
if (lessThan(bill, wallet)) pay(bill);

Money bill = tea + tea + samosa;                  // operator দিয়ে
if (bill < wallet) pay(bill);

দুটি একই কাজ করে, কিন্তু দ্বিতীয়টি পড়তে অনেক সহজ। প্রথমটিতে বন্ধনী গুনতে হয়, ভাবতে হয় add-এর প্রথম প্যারামিটার কোনটা। দ্বিতীয়টি দেখলেই স্কুলের অঙ্কের মতো বোঝা যায়। int বা double-এর সাথে আপনি এভাবেই লেখেন — তাহলে নিজের টাইপের সাথে কেন পারবেন না? C++-এর উত্তর: পারবেন। একই operator-কে নিজের টাইপের জন্য নতুন অর্থ দেওয়াকে বলে operator overloading.

bKash অ্যাপে “যোগ” মানে কী, সবাই জানে — দুটি টাকার পরিমাণ মিলিয়ে একটি পরিমাণ। কোনো নতুন ব্যবহারকারীকে শেখাতে হয় না। এখন কল্পনা করুন, অ্যাপের কোনো বোতামে “+” লেখা, কিন্তু চাপলে টাকা যোগ না হয়ে আপনার নম্বর ব্লক হয়ে যায়। ভয়ংকর, তাই না? চিহ্নটি পরিচিত বলেই মানুষ ধরে নেয় তার অর্থও পরিচিত। operator overloading-এর পুরো দর্শন এই একটি বাক্যে: পরিচিত চিহ্ন ব্যবহার করুন শুধু তখন, যখন অর্থটাও পরিচিত।

কোথায় অর্থ স্পষ্ট? যেখানে আপনার টাইপটি আসলে একটি “মান” (value semantics) — সংখ্যার মতো, যার যোগ-বিয়োগ-তুলনার একটি সর্বজনীন মানে আছে। টাকা, ভগ্নাংশ, বড় সংখ্যা (BigInt), জটিল সংখ্যা, 2D বা 3D vector, matrix, তারিখ ও সময়ের ব্যবধান। আবার যেখানে টাইপটি একটি পাত্র (container), সেখানে [] দিয়ে ভেতরের উপাদান ধরা স্পষ্ট — আপনি পর্ব ১৩-এ std::vector-এ এভাবেই v[i] লিখেছেন, আর সেটিও একটি overload করা operator.

অর্থ স্পষ্ট — overload করুন

money + money, point == point, date1 < date2, matrix * vector, playlist[3], std::cout << order. পাঠক দেখামাত্র বোঝেন কী হবে।

obvious meaning

অর্থ অস্পষ্ট — নাম দেওয়া ফাংশন লিখুন

student + student মানে কী — গ্রুপ? বিয়ে? নম্বর যোগ? order * 2 মানে দাম দ্বিগুণ, না পণ্য দ্বিগুণ? এখানে merge() বা withQuantity() অনেক পরিষ্কার।

use a name

অর্থ উল্টো — কখনোই নয়

- দিয়ে যোগ, == দিয়ে কিছু বদলানো, ! দিয়ে ফাইল মোছা। compiler আটকাবে না, কিন্তু প্রতিটি পাঠক ভুল বুঝবেন।

never surprise
মূল ধারণা

operator overloading কোনো নতুন ক্ষমতা দেয় না — যা a + b দিয়ে করবেন, তা add(a, b) দিয়েও করা যেত। এটি দেয় পড়ার সুবিধা. তাই এর একমাত্র পরীক্ষা: কোডটি কি পড়তে আরও সহজ হলো, নাকি পাঠককে এখন একটি গোপন নিয়ম মনে রাখতে হবে? এই নীতির নাম principle of least surprise.

২ · operator আসলে একটি ফাংশন — বিশেষ নাম সহ

পুরো বিষয়টার রহস্য ভাঙার জন্য একটি বাক্যই যথেষ্ট: C++-এ overload করা প্রতিটি operator একটি সাধারণ ফাংশন, যার নাম operator শব্দের পরে চিহ্নটি। +-এর ফাংশনের নাম operator+, তুলনার operator==, ছাপার operator<<. এর parameter আছে, return type আছে, এটি function overloading-এর সব নিয়ম মানে (পর্ব ৯) — শুধু ডাকার ভঙ্গিটা আলাদা। আপনি নাম লিখে ডাকেন না, চিহ্ন বসান; compiler নিজেই নামে বদলে নেয়।

Money operator+(Money a, const Money& b);   // একটি সাধারণ ফাংশনের ঘোষণা

Money x = tea + samosa;              // আপনি যা লেখেন
Money y = operator+(tea, samosa);    // compiler যা বোঝে — এটাও বৈধ C++

দ্বিতীয় লাইনটি কেউ লেখে না, কিন্তু লিখলেও চলবে। এটা জানলে অনেক ভয় কেটে যায়: operator overloading শেখা মানে নতুন ভাষা শেখা নয়, আপনার জানা ফাংশনেরই আরেকটি রূপ। tea + samosa দেখে compiler ঠিক কী কী করে, ধাপে ধাপে দেখুন:

  1. আপনি লেখেনtea + samosaদুটি Money
  2. প্রার্থী খোঁজাoperator+member, non-member, built-in
  3. সেরা মিলMoney, Moneyoverload resolution
  4. ফাংশন ডাকাoperator+(tea, samosa)সাধারণ call
  5. ফলMoney(2750)নতুন মান
sourceদুই পাশে দুটি Money object
Money tea(15);          // ভেতরে 1500 পয়সা
Money samosa(12, 50);   // ভেতরে 1250 পয়সা
Money x = tea + samosa;
candidate setকোথায় কোথায় খোঁজে
tea.operator+(samosa)      // ১. Money-র member? (নেই)
operator+(tea, samosa)     // ২. non-member? (আছে — Money-র friend)
int + int, double + double // ৩. built-in — Money-র সাথে মেলে না
overload resolutionপর্ব ৯-এর একই নিয়ম
Money operator+(Money, const Money&)   ✔ নিখুঁত মিল
(অন্য কোনো operator+ থাকলে এখানে তুলনা হতো;
 দুটি সমান ভালো হলে — ambiguous call error)
callএখন এটি নিছক একটি ফাংশন ডাকা
Money x = operator+(tea, samosa);
// a = tea-র কপি, b = samosa-র reference
resultদুই পাশের কেউ বদলায়নি
tea    → 1500 পয়সা   (অপরিবর্তিত)
samosa → 1250 পয়সা   (অপরিবর্তিত)
x      → 2750 পয়সা   (নতুন object)
  1. আপনি একটি expression লিখলেন। +-এর দুই পাশে দুটি Money. int হলে compiler সরাসরি যোগের machine instruction বসাত, কিন্তু Money-র জন্য কোনো built-in যোগ নেই।
  2. প্রার্থীর তালিকা। compiler তিন জায়গায় খোঁজে: বাঁ দিকের object-এর class-এ operator+ নামের member আছে কি না; বাইরে operator+ নামের non-member ফাংশন আছে কি না (এখানে ADL Money-র নিজের namespace আর class-এর friend-দেরও দেখে); আর built-in যোগগুলো।
  3. সেরা মিল বাছাই। এখন সাধারণ overload resolution — পর্ব ৯-এ যা শিখেছেন, হুবহু তাই। যে ফাংশনের প্যারামিটার আর্গুমেন্টের সাথে সবচেয়ে ভালো মেলে, সেটি জেতে। কোনোটি না মিললে error: no match for 'operator+'.
  4. এটা এখন শুধু একটি ফাংশন ডাকা। tea + samosa আক্ষরিক অর্থেই operator+(tea, samosa) হয়ে গেল। প্যারামিটার পাঠানো, কপি বা reference — সব সাধারণ ফাংশনের নিয়মে।
  5. ফল একটি নতুন মান। + দুই পাশের কাউকে বদলায় না, একটি নতুন Money ফেরত দেয় — ঠিক যেমন 3 + 4 লিখলে 3 বা 4 বদলে যায় না। built-in +-এর এই আচরণ নকল করাই ভালো overload-এর প্রথম শর্ত।

যা বদলানো যায়, আর যা যায় না

operator overload করলে আপনি শুধু অর্থ বদলান। ভাষার ব্যাকরণ একই থাকে। precedence (কে আগে হিসাব হবে), associativity (বাঁ থেকে না ডান থেকে), আর কতটি operand লাগবে — এগুলো পর্ব ৬-এর built-in নিয়মেই আটকে থাকে। তাই a + b * c সবসময় a + (b * c), আপনার টাইপ যা-ই হোক।

নিয়মমানে
অন্তত একটি operand নিজের টাইপint + int-এর অর্থ বদলানো যায় না; শুধু user-defined type (class, struct, enum) জড়িত থাকলে overload চলে
নতুন চিহ্ন বানানো যায় না** (ঘাত) বা <> নামে কোনো operator তৈরি করা যায় না
precedence ও associativity অপরিবর্তিত^-কে ঘাত বানালে a ^ 2 + 1 হয়ে যায় a ^ (2 + 1) — কারণ ^-এর precedence +-এর চেয়ে কম
কিছু operator একেবারেই overload হয় না::, ., .*, ?:, sizeof, alignof, typeid
কিছু operator শুধু member হতে পারে=, [], (), -> — এগুলো class-এর ভেতরেই লিখতে হয়

৩ · member না non-member? symmetry আর hidden friend

একটি operator দুইভাবে লেখা যায়। member function হিসেবে লিখলে বাঁ দিকের operand হয় *this (পর্ব ২০-এর this), আর প্যারামিটার থাকে একটি কম। non-member বা free function হিসেবে লিখলে দুটি operand-ই সাধারণ প্যারামিটার।

struct Vec2 {
    int x, y;
    Vec2 operator*(int k) const { return {x * k, y * k}; }   // member: v * 2 → v.operator*(2)
};
Vec2 operator*(int k, Vec2 v) { return v * k; }             // non-member: 2 * v → operator*(2, v)

একটি গেমের কথা ভাবুন। একটি চরিত্রের গতি speed, আর ২ সেকেন্ডে সে কতদূর যাবে — speed * 2. পদার্থবিজ্ঞানের বইয়ে লেখা থাকে 2 * speed. দুটির অর্থ এক, তাই দুটিই চলা উচিত। কিন্তু member দিয়ে শুধু প্রথমটি হয়: 2 * speed-এর বাঁ দিকে int, আর int-এর ভেতরে আপনি কোনো member যোগ করতে পারবেন না। বাঁ দিকের operand যখন আপনার টাইপ নয়, তখন non-member ছাড়া উপায় নেই। এই দুই দিকেই সমান আচরণের নাম symmetry.

একই সমস্যা আরও সূক্ষ্মভাবে আসে implicit conversion-এ। ধরুন একটি Fraction class-এ int থেকে রূপান্তর চলে (constructor explicit নয়)। member operator+ হলে half + 1 চলে (ডান দিকের 1 Fraction-এ রূপান্তরিত হয়), কিন্তু 1 + half চলে না — কারণ C++ member ডাকার জন্য বাঁ দিকের object-কে রূপান্তর করে না। non-member হলে দুটিই চলে। এমন অসম আচরণ ব্যবহারকারীকে বিভ্রান্ত করে।

Hidden friend — class-এর ভেতরে লেখা non-member

non-member ফাংশনের প্রায়ই class-এর private ডেটা লাগে — Money-র ভেতরের পয়সা তো private. পথ হলো friend: class নিজে ঘোষণা দেয়, “এই ফাংশনটি আমার private অংশ দেখতে পারবে।” আধুনিক C++-এ সবচেয়ে পছন্দের রূপ হলো ফাংশনটিকে class-এর ভেতরেই friend হিসেবে সংজ্ঞায়িত করা — একে বলে hidden friend:

class Money {
    long long poisha_;
public:
    friend Money operator+(Money a, const Money& b) {   // ভেতরে লেখা, তবু member নয়
        a.poisha_ += b.poisha_;
        return a;
    }
};

“hidden” কেন? কারণ এই ফাংশনটি সাধারণ নাম-খোঁজায় (name lookup) দেখা যায় না; compiler এটিকে খুঁজে পায় শুধু তখন, যখন অন্তত একটি আর্গুমেন্ট Money — ADL-এর মাধ্যমে। এর তিনটি সুবিধা: (১) প্রতিটি + লেখার সময় compiler-কে হাজারটা অপ্রাসঙ্গিক operator+ যাচাই করতে হয় না, তাই compile দ্রুত হয় আর error message ছোট হয়; (২) অপ্রত্যাশিত রূপান্তরে ভুল ফাংশন বাছাইয়ের ঝুঁকি কমে; (৩) operator-টি class-এর পাশেই থাকে, খুঁজতে হয় না। অনেক আধুনিক লাইব্রেরি, আর standard library-র নতুন কিছু অংশ, এই রীতি ব্যবহার করে।

operatorকোথায়কেন
= [] () ->member (বাধ্যতামূলক)ভাষার নিয়ম
+= -= *= /=, unary -, ++ --memberনিজের object বদলায়, বাঁ দিকটি সবসময় নিজের টাইপ
+ - * /non-member (hidden friend)symmetry: 2 * v আর v * 2
== <=>member বা hidden friendC++20 দুই দিক নিজেই উল্টে দেখে (অংশ ৫)
<< >> (stream)non-member (hidden friend)বাঁ দিকে std::ostream, যা আপনার class নয়

এবার একটি পুরো ছোট উদাহরণ — গেমের 2D vector, যেখানে 2 * speed আর speed * 3 দুটিই চলে। চালিয়ে দেখুন:

#include <print>

struct Vec2 {                                  // 2D vector for a game
    int x = 0, y = 0;
    Vec2& operator+=(Vec2 o) { x += o.x; y += o.y; return *this; }
    friend Vec2 operator+(Vec2 a, Vec2 b) { a += b; return a; }
    friend Vec2 operator*(int k, Vec2 v) { return {k * v.x, k * v.y}; }
    friend Vec2 operator*(Vec2 v, int k) { return k * v; }   // symmetry
    bool operator==(const Vec2&) const = default;
};

int main() {
    Vec2 pos{10, 5}, speed{3, -2};
    pos += 2 * speed;                          // 2 seconds of movement
    Vec2 far = pos + speed * 3;
    std::println("pos = ({}, {})", pos.x, pos.y);
    std::println("far = ({}, {})", far.x, far.y);
    std::println("pos == (16, 1)? {}", pos == Vec2{16, 1});
}

আউটপুট: pos = (16, 1), far = (25, -5) আর pos == (16, 1)? true. 2 * speed হলো (6, -4), তা যোগ করে pos (16, 1). তারপর speed * 3 = (9, -6), যোগ করে (25, -5). খেয়াল করুন তিনটি জিনিস: operator*(Vec2, int) নিজে হিসাব না করে উল্টো সংস্করণটিকে ডাকে — যুক্তি এক জায়গায়, ভুলের সুযোগ কম। + নিজে যোগ করে না, +=-কে ডাকে (পরের অংশে কারণ)। আর operator== নিজে লিখিনি, শুধু = default — compiler x আর y দুটিই তুলনা করে দিয়েছে (পর্ব ১২-এর defaulted comparison).

C জানলে

C-তে operator overloading নেই। দুটি struct-কে == দিয়ে তুলনাই করা যায় না — compile error; লিখতে হয় money_add(a, b) বা vec_equal(&a, &b)-এর মতো নামওয়ালা ফাংশন। অনেকে memcmp দিয়ে struct তুলনা করেন, যা padding-এর আবর্জনা বাইটের কারণে ভুল ফল দিতে পারে। C++-এর overload করা operator ভেতরে আসলে ঠিক সেই নামওয়ালা ফাংশনই, শুধু ডাকার রূপ সুন্দর। C-তে struct কীভাবে সাজানো থাকে, তা দেখতে C কোর্সের struct ও union পর্ব দেখুন।

৪ · গাণিতিক operator — আগে +=, তার ওপর +

built-in টাইপে a += b আর a = a + b একই ফল দেয়। আপনার টাইপেও তাই হওয়া উচিত — নইলে পাঠক বিভ্রান্ত হবেন। দুটিকে আলাদাভাবে লিখলে একদিন একটিতে ভুল ঠিক করবেন, অন্যটিতে ভুলে যাবেন, আর দুটি আলাদা ফল দেবে। তাই প্রচলিত রীতি: আসল হিসাব থাকবে compound assignment +=-এ, আর + হবে তার ওপর তিন লাইনের মোড়ক।

class Money {
    long long poisha_ = 0;
public:
    Money& operator+=(const Money& o) {          // member: নিজেকে বদলায়
        poisha_ += o.poisha_;
        return *this;                             // নিজের reference ফেরত
    }
    friend Money operator+(Money a, const Money& b) {   // a আগে থেকেই কপি
        a += b;
        return a;
    }
};

দুটি signature-ই ভেবেচিন্তে বানানো। += ফেরত দেয় Money& — নিজের reference, কপি নয়। কারণ built-in +=-ও তাই করে: (x += 2) *= 3 লিখলে দুটি কাজই x-এর ওপর হয়। আর +-এর প্রথম প্যারামিটার ইচ্ছা করে by value: কপি তো আমাদের লাগতই (নতুন ফল বানাতে), তাই সেটা প্যারামিটারেই নিয়ে নিলাম। ডাকার জায়গায় যদি একটি temporary object থাকে (যেমন tea + samosa + samosa-এর মাঝের ফল), তবে কোনো কপিই হয় না — C++17-এর নিয়মে সেই temporary সরাসরি প্যারামিটারেই তৈরি হয়। দ্বিতীয় প্যারামিটার শুধু পড়া হয়, তাই const reference.

একটি সূক্ষ্ম বিষয়: return a += b; এক লাইনে লেখা যেত, কিন্তু তখন ফেরত যায় একটি reference (a += b-এর ফল), আর compiler সেটিকে কপি করে — নিজে থেকে move করে না। আলাদা দুই লাইনে return a; লিখলে compiler জানে a একটি local প্যারামিটার, তাই ফেরত দেওয়ার সময় move করতে পারে। ছোট টাইপে পার্থক্য নেই, বড় টাইপে (BigInt, Matrix) আছে। পুরো প্রক্রিয়াটি ধাপে ধাপে দেখুন:

1Money& operator+=(const Money& o) {2    poisha_ += o.poisha_;3    return *this;4}5friend Money operator+(Money a, const Money& b) {6    a += b;7    return a;8}9Money bill = tea + samosa;
মান (পয়সায়)
tea
samosa
a (tea-র কপি)
bill
  1. ডাকা হলো +. লাইন ৯-এ tea + samosa — অংশ ২-এর মতো compiler একে operator+(tea, samosa) ডাকায় বদলে নিল।
  2. প্যারামিটার তৈরি। a by value, তাই tea-র একটি কপি তৈরি হলো (1500). b একটি const reference — samosa-র দিকে তাকিয়ে আছে, কোনো কপি নয়। আসল tea-কে আমরা ছুঁচ্ছিই না।
  3. + কাজটা +=-কে দিল। লাইন ৬ a += b আসলে a.operator+=(b). এখন +=-এর ভেতরে *this মানে a, আর o মানে samosa.
  4. আসল হিসাব একটিই জায়গায়। লাইন ২: a-এর পয়সা 1500 থেকে 2750 হলো। যোগের যুক্তি পুরো class-এ শুধু এই এক লাইনে — কাল যদি overflow যাচাই যোগ করতে চান, এখানেই করবেন, আর + আপনাআপনি তা পাবে।
  5. += নিজের reference ফেরত দিল। return *this; আর ফেরতের টাইপ Money& — তাই কোনো কপি নেই, আর (a += b) += c-এর মতো চেইনও a-এর ওপরই কাজ করবে।
  6. + নতুন মান ফেরত দিল। return a; — local প্যারামিটার বলে compiler এটি move করে bill-এ পাঠায়। bill = 2750. tea আর samosa আগের মতোই আছে, ঠিক built-in +-এর মতো।

বিয়োগ, গুণ, ভাগ — সব একই ছাঁচে: -= থেকে -, *= থেকে *. তবে প্রতিটি operator-এর জন্য নিজেকে জিজ্ঞেস করুন: এর অর্থ কি সত্যিই স্পষ্ট? Money - Money স্পষ্ট (ফেরত টাকা)। Money * int স্পষ্ট (তিন কাপ চা)। কিন্তু Money * Money — টাকা গুণ টাকা মানে “বর্গটাকা”, যার কোনো বাস্তব অর্থ নেই। তাই সেটি লিখবেনই না; কেউ ভুল করে লিখলে compile error-ই হবে আপনার সুরক্ষা। যে operator অর্থহীন, তা না থাকাটাই একটি ফিচার। একইভাবে unary - (যেমন -refund) স্পষ্ট, তাই সেটি দেওয়া যায়।

৫ · তুলনা — C++20-এর spaceship operator <=>

C++20-এর আগে Money-কে পুরোপুরি তুলনাযোগ্য করতে ছয়টি ফাংশন লিখতে হতো: ==, !=, <, <=, >, >=. সাধারণত == আর < আসল কাজ করত, বাকি চারটি তাদের ডাকত। ছয়টি ফাংশন, প্রতিটিতে ভুলের সুযোগ — কেউ একটিতে <-এর বদলে <= লিখলেই std::sort বা std::map অদ্ভুত আচরণ করত। আর class-এ নতুন সদস্য যোগ করলে ছয়টিই হালনাগাদ করতে হতো।

C++20 এনেছে three-way comparison operator <=>, যার ডাকনাম spaceship (দেখতে মহাকাশযানের মতো)। এটি একবারে তিনটি প্রশ্নের উত্তর দেয়: বাঁ দিকেরটি ছোট, সমান, না বড়? ফল একটি বিশেষ টাইপ, যাকে 0-এর সাথে তুলনা করা যায়: (a <=> b) < 0 মানে a ছোট। আর সবচেয়ে বড় কথা — এটি = default করা যায়:

#include <compare>

class Money {
    long long poisha_ = 0;
public:
    auto operator<=>(const Money&) const = default;   // এই এক লাইনেই ছয়টি তুলনা
};

= default মানে compiler সদস্যগুলোকে ঘোষণার ক্রমে একে একে তুলনা করবে — প্রথম যেখানে পার্থক্য, সেখানেই ফল ঠিক (অভিধানের মতো, lexicographic order). আর <=> default করলে compiler নিজে থেকে একটি defaulted operator==-ও ঘোষণা করে দেয়। এই এক লাইন থেকে ছয়টি তুলনা কীভাবে আসে, দেখুন:

auto operator<=>(...) const = default;আপনি লেখেন — শুধু এটুকু
==নিজে থেকে defaulted
!=!(a == b)
<(a <=> b) < 0
<=(a <=> b) <= 0
>(a <=> b) > 0
>=(a <=> b) >= 0
আপনার classএকটি ঘোষণা
auto operator<=>(const Money&) const = default;
// ফেরতের টাইপ: std::strong_ordering (long long সদস্যের জন্য)
==<=> default হলে == ফ্রি
bool operator==(const Money&) const = default;   // compiler নিজে যোগ করে
tea == samosa   →  1500 == 1250  →  false
!=rewritten — নতুন ফাংশন নয়
tea != samosa   →  !(tea == samosa)  →  true
<<=>-এর ফলকে 0-এর সাথে তুলনা
bill < paid   →  (bill <=> paid) < 0
             →  (4000 <=> 5000) হলো less  →  true
<= > >=একই কৌশল; দরকারে দুই দিক উল্টেও
bill >= paid  →  (bill <=> paid) >= 0  →  false
x < y         →  মিল না পেলে 0 < (y <=> x) — উল্টো প্রার্থী
ফলদুটি ফাংশন, ছয়টি তুলনা
== != < <= > >=   সব চলে
std::sort, std::map<Money, ...>, std::ranges::max — সব চলে
  1. আপনি লেখেন একটি লাইন। auto ফেরতের টাইপ compiler ঠিক করবে সদস্যদের দেখে। Money-র একমাত্র সদস্য long long, তাই টাইপ হবে std::strong_ordering.
  2. == বিনা খরচে। <=> default করলে compiler নিজে থেকে defaulted ==-ও ঘোষণা করে। এটি সদস্য একে একে == দিয়ে তুলনা করে, <=> দিয়ে নয় — কারণ সমতা প্রায়ই দ্রুত বের করা যায় (যেমন দুটি string-এর দৈর্ঘ্য আলাদা হলে আর পড়ারই দরকার নেই)।
  3. != হলো rewrite. C++20-এ a != b লিখলে compiler আলাদা কোনো ফাংশন খোঁজে না; সে এটিকে !(a == b) হিসেবে আবার লেখে। এই কৌশলের নাম rewritten candidate.
  4. < আসে <=> থেকে। bill < paid লিখলে compiler সেটিকে (bill <=> paid) < 0 বানায়। spaceship-এর ফল “less”, যা 0-এর চেয়ে ছোট হিসেবে গণ্য — তাই true.
  5. বাকি তিনটি একইভাবে — দরকারে উল্টে। <=, >, >= একই ছাঁচে। আরও আছে: x < y-এর জন্য সরাসরি মিল না পেলে compiler উল্টো দিক থেকেও চেষ্টা করে, 0 < (y <=> x). ফলে == বা <=> একদিকে লিখলেই দুই দিকে চলে — অংশ ৩-এর symmetry সমস্যা তুলনার ক্ষেত্রে নিজে থেকেই মিটে গেল।
  6. ফল: দুটি ফাংশন, ছয়টি তুলনা। আর এগুলো সঙ্গতিপূর্ণ হওয়ার নিশ্চয়তা আছে — < আর >= কখনো একসাথে true হবে না। এখন Money দিয়ে std::sort চলবে, std::map-এর key হবে (পর্ব ৩১), std::ranges::max কাজ করবে।

strong, weak, partial — তিন রকম ক্রম

<=>-এর ফেরতের টাইপ তিন রকম হতে পারে, আর পার্থক্যটা গণিতের চেয়ে দৈনন্দিন উদাহরণে সহজে বোঝা যায়:

টাইপ“সমান” মানেউদাহরণ
std::strong_orderingসত্যিই একই — একটির বদলে অন্যটি বসালে কোনো পার্থক্য নেইint, পয়সায় রাখা Money, রোল নম্বর
std::weak_orderingক্রমের হিসাবে সমান, কিন্তু আলাদা করে চেনা যায়বড়-ছোট হাতের অক্ষর না মেনে নাম সাজানো: “Dhaka” আর “dhaka” ক্রমে সমান, কিন্তু লেখা আলাদা
std::partial_orderingকিছু জোড়া তুলনাই করা যায় নাdouble: NaN কোনো কিছুর চেয়ে ছোটও না, বড়ও না, সমানও না

auto দিলে compiler সদস্যদের মধ্যে সবচেয়ে দুর্বল ক্রমটি বেছে নেয়। Money-তে double সদস্য রাখলে <=> হয়ে যেত partial — আরেকটি কারণ, টাকা কেন পূর্ণসংখ্যায় রাখবেন। আর নিজে হাতে <=> লিখতে হলে (যেমন শুধু কিছু সদস্য দিয়ে তুলনা), ফেরতের টাইপ স্পষ্ট করে লিখুন, যাতে পাঠক জানেন আপনি কোন প্রতিশ্রুতি দিচ্ছেন। spaceship নিয়ে আরও গভীর কথা পর্ব ৪২-এ।

defaulted তুলনা সব সদস্য তুলনা করে। যদি class-এ এমন সদস্য থাকে যা “মান”-এর অংশ নয় — যেমন একটি cache, শেষ ব্যবহারের সময়, বা একটি pointer — তবে = default ভুল অর্থ দেবে। দুটি একই অর্ডার শুধু cache আলাদা বলে “অসমান” হয়ে যাবে। এমন ক্ষেত্রে নিজে == আর <=> লিখুন, শুধু অর্থপূর্ণ সদস্য দিয়ে।

৬ · ছাপা — operator<< আর std::formatter

নিজের টাইপ std::cout << bill দিয়ে ছাপতে চাইলে << overload করতে হয়, যাকে stream-এর প্রসঙ্গে বলে insertion operator. এর বাঁ দিকে থাকে std::ostream (cout-এর টাইপ), যা standard library-র class — আপনি তাতে member যোগ করতে পারবেন না। তাই এটি সবসময় non-member, সাধারণত hidden friend:

friend std::ostream& operator<<(std::ostream& os, const Money& m) {
    return os << "Tk " << m.poisha_ / 100 << '.' << m.poisha_ % 100;   // (শূন্য-প্যাডিং পরে ঠিক করব)
}

signature-টি মুখস্থ রাখার মতো: প্রথম প্যারামিটার আর ফেরতের টাইপ দুটিই std::ostream&. stream কপি করা যায় না, আর কপি করার কোনো মানেও নেই — আমরা একই cout-এ লিখতে চাই। আর ফেরত দিতে হয় সেই একই stream, কারণ তা না হলে std::cout << "bill = " << bill << '\n'-এর মতো চেইন ভেঙে যায়। চেইনটি কীভাবে কাজ করে, দেখুন:

std::cout << "bill = " << bill << '\n'
শুরুএকটি std::ostream object
std::cout   // টাইপ std::ostream, স্ক্রিনের সাথে যুক্ত
প্রথম <<বাঁ থেকে ডানে (left-associative)
operator<<(std::cout, "bill = ")
// library-র ফাংশন; লিখে ফেরত দেয় std::cout-এর reference
দ্বিতীয় <<এবার আমাদের hidden friend
operator<<( operator<<(std::cout, "bill = "), bill )
// ভেতরেরটির ফল = std::cout&, তাই os = cout; আমরা লিখি "Tk 40.00", ফেরত os
তৃতীয় <<আবার library
operator<<( operator<<( operator<<(std::cout, "bill = "), bill ), '\n' )
// স্ক্রিনে: bill = Tk 40.00
যদি void ফেরত দিতাম?চেইন ভেঙে যায়
void operator<<(std::ostream& os, const Money& m);
std::cout << bill << '\n';
error: no match for 'operator<<' (operand types are 'void' and 'char')
  1. শুরুতে একটি stream. std::cout একটি std::ostream object. পর্ব ৭-এ দেখেছেন, << চেইন করা যায়; আর এটি বাঁ থেকে ডানে জোড়া লাগে (left-associative, পর্ব ৬), তাই সবচেয়ে বাঁয়ের অংশটি আগে হিসাব হবে।
  2. প্রথম ডাক — library-র ফাংশন। std::cout << "bill = " হলো operator<<(std::cout, "bill = "). লেখাটি ছাপা হলো, আর ফাংশনটি ফেরত দিল সেই cout-এরই reference.
  3. দ্বিতীয় ডাক — আপনার ফাংশন। আগের ধাপের ফল (cout) হয়ে গেল বাঁ দিকের operand. তাই আমাদের hidden friend ডাকা হলো os = cout আর m = bill দিয়ে। সে "Tk 40.00" লিখে আবার os ফেরত দিল।
  4. তৃতীয় ডাক। আবার ফল cout, তাই '\n' ছাপা হলো। পুরো লাইনটি আসলে তিনটি নেস্টেড ফাংশন ডাকা — প্রতিটি পরেরটিকে একই stream হাতে তুলে দেয়, যেন রিলে দৌড়ের ব্যাটন।
  5. ব্যাটন না দিলে দৌড় থামে। আপনার operator<< যদি void ফেরত দেয়, তবে cout << bill-এর ফল কিছুই না, আর তার সাথে << '\n' জোড়া যায় না — compile error. এটি AI-এর লেখা কোডে মাঝে মাঝে দেখা যায়।

এবার পুরো Money class একসাথে — যোগ, বিয়োগ, তুলনা আর ছাপা। শূন্য-প্যাডিং (12.05, 12.5 নয়) ঠিক করতে ভেতরে std::format-এর {:02} ব্যবহার করেছি। চালিয়ে দেখুন:

#include <format>
#include <iostream>
class Money {
    long long poisha_ = 0;                     // 1 taka = 100 poisha
public:
    explicit Money(long long taka, long long poisha = 0) : poisha_(taka * 100 + poisha) {}
    Money& operator+=(const Money& o) { poisha_ += o.poisha_; return *this; }
    Money& operator-=(const Money& o) { poisha_ -= o.poisha_; return *this; }
    friend Money operator+(Money a, const Money& b) { a += b; return a; }
    friend Money operator-(Money a, const Money& b) { a -= b; return a; }
    auto operator<=>(const Money&) const = default;
    friend std::ostream& operator<<(std::ostream& os, const Money& m) {
        return os << std::format("Tk {}.{:02}", m.poisha_ / 100, m.poisha_ % 100);
    }
};

int main() {
    Money tea(15), samosa(12, 50), paid(50);
    Money bill = tea + samosa + samosa;
    std::cout << "bill = " << bill << ", change = " << paid - bill << '\n';
    std::cout << std::boolalpha << (bill <= paid) << ' ' << (tea == samosa) << '\n';
}

আউটপুট: bill = Tk 40.00, change = Tk 10.00 আর তারপর true false. বিল 1500 + 1250 + 1250 = 4000 পয়সা, ৫০ টাকা দিলে ফেরত ১০ টাকা — পুরো হিসাব পূর্ণসংখ্যায়, তাই কোনো 39.999999-এর ঝামেলা নেই। paid - bill-এর চারপাশে বন্ধনী লাগেনি, কারণ --এর precedence <<-এর চেয়ে বেশি; কিন্তু bill <= paid-এ বন্ধনী লাগে, কারণ তুলনার precedence <<-এর চেয়ে কম — overload করলেও এই নিয়ম বদলায় না। constructor-এ explicit রেখেছি, যাতে bill == 40-এর মতো অস্পষ্ট লেখা compile-ই না হয় (৪০ টাকা, না ৪০ পয়সা?)। একটি সীমাবদ্ধতা খেয়াল করুন: ঋণাত্মক টাকা (যেমন −১.৫০) এই ছাপার নিয়মে ঠিক দেখাবে না — অনুশীলনে ভেবে দেখতে পারেন।

std::println-এর জন্য — std::formatter

পর্ব ৭-এ দেখেছেন, আধুনিক C++-এ ছাপার পছন্দের উপায় std::println (C++23 থেকে)। কিন্তু std::println("{}", bill) লিখলে compile error — format লাইব্রেরি << ব্যবহার করে না। তাকে শেখাতে হয় std::formatter-এর একটি template specialization দিয়ে (template নিয়ে বিস্তারিত পর্ব ২৮-২৯-এ; এখানে ছাঁচটি দেখে রাখুন):

template <>
struct std::formatter<Vec2> : std::formatter<std::string> {     // string-এর নিয়ম ধার নিই
    auto format(Vec2 v, std::format_context& ctx) const {
        return std::formatter<std::string>::format(std::format("({}, {})", v.x, v.y), ctx);
    }
};

std::println("pos = {}", pos);        // pos = (16, 1)
std::println("[{:>10}]", pos);       // [   (16, 1)] — প্রস্থ, alignment-ও চলে

std::formatter<std::string> থেকে উত্তরাধিকার নেওয়ায় (inheritance, পরের পর্ব) {:>10}-এর মতো প্রস্থ ও alignment-এর নিয়ম বিনা পরিশ্রমে পাওয়া গেল। কোনটি লিখবেন? নতুন কোডে std::formatter — এটি type-safe, দ্রুত, আর std::format, std::print, std::println সবখানে চলে। পুরোনো কোড বা লাইব্রেরির সাথে মেলাতে operator<<. বড় প্রজেক্টে প্রায়ই দুটোই থাকে, আর একটি অন্যটিকে ডাকে, যাতে ছাপার নিয়ম এক জায়গায় থাকে।

৭ · সূচক, ডাকা আর গোনা — [], () আর ++

operator[] — দুই রূপে

একটি বাসের টিকিট-ব্যবস্থায় Bus class আসনগুলোর যাত্রীর নাম রাখে। bus[3] দিয়ে ৩ নম্বর আসনের যাত্রী দেখা বা বদলানো স্পষ্ট — ঠিক vector-এর মতো। subscript operator সাধারণত দুটি রূপে লেখা হয়:

class Bus {
    std::vector<std::string> seats_;
public:
    std::string&       operator[](std::size_t i)       { return seats_[i]; }   // বদলানো যায়
    const std::string& operator[](std::size_t i) const { return seats_[i]; }   // শুধু পড়া
};

bus[3] = "Rahim";               // non-const রূপ: reference ফেরত বলে মান বসানো যায়
void show(const Bus& b) { std::println("{}", b[3]); }   // const রূপ

কেন দুটি? non-const রূপ reference ফেরত দেয়, তাই bus[3] = "Rahim" সরাসরি ভেতরের string বদলায় — কপি ফেরত দিলে বদল হতো কপিতে, আসল আসনে নয়। আর const Bus& হাতে থাকলে শুধু const ফাংশন ডাকা যায় (পর্ব ২০), তাই দ্বিতীয় রূপটি না থাকলে show ফাংশনটি compile হতো না। দুটি রূপের মধ্যে compiler নিজেই বেছে নেয় object const কি না দেখে।

আরেকটি বিষয়: std::vector-এর মতো আপনার []-ও সীমা যাচাই না করলে ভুল সূচকে undefined behavior. চাইলে একটি আলাদা at() দিন, যা যাচাই করে exception ছোড়ে — standard library-র রীতি মেনে।

operator() — matrix আর functor

একটি matrix-এ সারি আর কলাম — দুটি সূচক লাগে। C++23-এর আগে [] শুধু একটি আর্গুমেন্ট নিত, তাই matrix লাইব্রেরিগুলো (যেমন Eigen) বহু বছর ধরে function call operator () ব্যবহার করে আসছে:

class Matrix {
    int rows_, cols_;
    std::vector<int> data_;                          // সব মান এক সারিতে (row-major)
public:
    Matrix(int r, int c) : rows_(r), cols_(c), data_(r * c) {}
    int& operator()(int r, int c)       { return data_[r * cols_ + c]; }
    int  operator()(int r, int c) const { return data_[r * cols_ + c]; }
};

Matrix m(3, 4);
m(1, 2) = 7;              // সারি 1, কলাম 2

ভেতরে ডেটা একটি একটানা vector-এ row-major ক্রমে, যা cache-এর জন্য ভালো (পর্ব ১৩)। C++23 থেকে operator[]-ও একাধিক আর্গুমেন্ট নিতে পারে, তাই m[1, 2] লেখাও সম্ভব — std::mdspan এভাবেই কাজ করে। তবে () এখনো সবখানে চলে, আর পুরোনো compiler-এও।

()-এর আরও বড় ব্যবহার হলো function object বা functor — এমন object, যাকে ফাংশনের মতো ডাকা যায়, কিন্তু যার নিজের অবস্থা (state) থাকতে পারে। ধরুন একটি দোকানে নির্দিষ্ট দামের বেশি পণ্য গুনতে হবে, আর সীমাটা চলার সময় ঠিক হয়:

struct PriceAbove {
    Money limit;
    bool operator()(const Item& it) const { return it.price > limit; }
};

auto n = std::ranges::count_if(items, PriceAbove{Money(500)});   // ৫০০ টাকার বেশি কয়টি?

PriceAbove{Money(500)} একটি object, যে নিজের ভেতরে সীমাটি মনে রাখে, আর count_if প্রতিটি পণ্যের জন্য তাকে ডাকে। STL algorithm-গুলো (পর্ব ৩৩) এমন functor দিয়েই কাস্টমাইজ করা হয়। আর পর্ব ৩৪-এর lambda আসলে এই একই জিনিসের সংক্ষিপ্ত রূপ — compiler প্রতিটি lambda থেকে গোপনে এমন একটি class বানায়, যার ভেতরে একটি operator(). আজ যা শিখলেন, তা-ই lambda-র ভিত্তি।

++ — prefix আর postfix

++ আর ---এর দুটি রূপ (পর্ব ৬-এর increment ও decrement): ++c (আগে বাড়াও, নতুন মান দাও) আর c++ (পুরোনো মান দাও, তারপর বাড়াও)। দুটির নামই operator++, তাই আলাদা করতে C++ একটি পুরোনো কৌশল ব্যবহার করে — postfix রূপে একটি অব্যবহৃত int প্যারামিটার:

class TicketCounter {
    int next_ = 1;
public:
    TicketCounter& operator++() { ++next_; return *this; }        // prefix: ++c
    TicketCounter operator++(int) {                                // postfix: c++
        TicketCounter old = *this;                                 // পুরোনো অবস্থার কপি
        ++*this;                                                   // prefix-কেই ডাকি
        return old;                                                // কপি ফেরত — by value
    }
};

এখানেও আগের নীতি: আসল কাজ এক জায়গায় (prefix), postfix তার ওপর মোড়ক — ঠিক += আর +-এর মতো। আর লক্ষ করুন, postfix রূপকে একটি কপি বানাতে হয়। তাই নিজের টাইপে (আর iterator-এ) ফল ব্যবহার না করলে ++c লেখার অভ্যাস করুন। int-এ compiler পার্থক্য মুছে দেয়, বড় object-এ সবসময় পারে না।

৮ · রূপান্তর, explicit operator bool আর যা overload করবেন না

Conversion operator — সাবধানে

একটি class অন্য টাইপে রূপান্তরিত হওয়ার নিয়মও দিতে পারে, conversion operator দিয়ে: operator long long() const. এর কোনো ফেরতের টাইপ লেখা হয় না — নামটিই টাইপ। কিন্তু এটি খুব বিপজ্জনক হাতিয়ার। Money-তে এমন একটি রূপান্তর দিলে ঘটে এসব:

class Money { /* ... */ public: operator long long() const { return poisha_; } };

Money price(100);
auto x = price + 5;      // compile হয়! ফল long long 10005 — ৫ টাকা, না ৫ পয়সা? কেউ জানে না
if (price == 10000) {}   // compile হয়, পয়সায় তুলনা — পাঠক ভাববেন টাকায়
int n = price;           // নিঃশব্দে পয়সায় রূপান্তর

অংশ ৬-এ explicit constructor দিয়ে যে সুরক্ষা বানিয়েছিলাম, এক লাইনের implicit রূপান্তর তা ভেঙে দিল। নিয়ম তাই সহজ: রূপান্তর লাগলে নামওয়ালা ফাংশন দিন (price.inPoisha()), বা অন্তত explicit লিখুন, যাতে রূপান্তর শুধু স্পষ্ট static_cast-এ ঘটে।

explicit operator bool — “ঠিক আছে কি না” প্রশ্নের উত্তর

একটি ব্যতিক্রম খুব প্রচলিত: bool-এ রূপান্তর, যাতে if (obj) লেখা যায়। আপনি ইতিমধ্যে এটি ব্যবহার করেছেন — if (ptr) (পর্ব ১৭-এর unique_ptr), if (file), while (std::cin >> x). এগুলো সব explicit operator bool:

class Connection {
    bool open_ = false;
public:
    explicit operator bool() const { return open_; }
};

Connection c;
if (c) { /* ... */ }      // চলে — if-এর শর্তে explicit হলেও রূপান্তর হয়
if (!c && ready) {}      // চলে — !, &&, ||, ?: — সবই “শর্ত”
int n = c + 1;            // compile error — এটাই চাই

explicit থাকলেও if, while, !, &&, || আর ?:-এর শর্তে রূপান্তর ঘটে — একে বলে contextual conversion. কিন্তু হিসাবে বা অন্য ভ্যারিয়েবলে বসাতে গেলে আটকায়। কেন এত সতর্কতা? explicit না হলে bool নিজে int-এ উন্নীত হয়, তখন std::cin << x (তীর ভুল দিকে!) bool-কে int বানিয়ে bit shift হিসেবে compile হয়ে যেত। C++98-এ এই ঝুঁকি এড়াতে stream-গুলো operator void* আর নানা জটিল “safe bool” কৌশল ব্যবহার করত; C++11-এ explicit conversion আসার পর সমস্যাটির পরিষ্কার সমাধান হয়।

যা overload করবেন না

কিছু operator-এর built-in আচরণে এমন বিশেষ নিশ্চয়তা আছে, যা overload করলে হারিয়ে যায়। ফলে কোড দেখতে একরকম, চলে আরেকরকম:

operatorbuilt-in-এর নিশ্চয়তাoverload করলে যা হারায়
&& আর ||short-circuit: p && p->ok()-এ p খালি হলে ডান দিক চলেই নাoverload মানে ফাংশন ডাকা — ডাকার আগে দুটি আর্গুমেন্টই হিসাব হয়; ডান দিক সবসময় চলে
, (কমা)comma operator বাঁ দিক আগে, ডান দিক পরে চালায়পাঠক জানেন না কোনো একটি কমা আসলে আপনার ফাংশন; লুপ আর macro-তে অদ্ভুত বাগ
unary &address-of সবসময় আসল ঠিকানা দেয়generic কোড ঠিকানা পায় না; তাই library-কে std::addressof ব্যবহার করতে হয়
অপ্রচলিত অর্থে যেকোনো operatorপাঠকের পূর্বধারণা^ দিয়ে ঘাত (precedence ভুল), + দিয়ে তালিকা থেকে মোছা — সবই বিস্ময়

মজার ব্যাপার, standard library নিজেই কয়েকবার “অপ্রচলিত” অর্থ ব্যবহার করেছে, আর সেগুলো এখন প্রচলিত: << মূলত bit shift, কিন্তু stream-এ “পাঠাও”; std::filesystem::path-এ (C++17) / মানে ফোল্ডার জোড়া, যেমন dir / "report.txt"; আর C++20-এর ranges-এ | মানে পাইপলাইন (পর্ব ৩৫)। এগুলো টিকেছে কারণ চিহ্নের দৃশ্যমান অর্থটা স্বাভাবিক (তীর, ফোল্ডারের দাগ, Unix-এর pipe), আর পুরো লাইব্রেরিতে একইভাবে, সঙ্গতিপূর্ণভাবে ব্যবহৃত। আপনার নিজের কোডে এমন নতুন রীতি চালু করার আগে দুবার ভাবুন — standard library-র মতো লক্ষ পাঠক আর দশকের অভ্যাস আপনার পেছনে নেই।

C++-এর একেবারে শুরুর দিকেই, ১৯৮০-র দশকের প্রথমার্ধে, Bjarne Stroustrup ভাষায় operator overloading যোগ করেন। বড় অনুপ্রেরণা ছিল বৈজ্ঞানিক আর প্রকৌশল কোড: complex সংখ্যা, matrix, বড় সংখ্যা — যেগুলো Fortran-এর মতো ভাষায় স্বাভাবিক সূত্রের মতো লেখা যেত, কিন্তু C-তে ফাংশনের জঙ্গল হয়ে যেত। তুলনার ক্ষেত্রে দীর্ঘদিন ছয়টি ফাংশন হাতে লেখাই ছিল নিয়ম; standard library-র std::rel_ops কিছুটা সাহায্য করত, কিন্তু ব্যবহার ছিল অস্বস্তিকর (C++20-এ সেটি deprecated). শেষে Herb Sutter-এর প্রস্তাবে C++20-এ এল <=>; নামটি অবশ্য নতুন নয় — Perl-এর মতো ভাষায় এই চিহ্ন আগে থেকেই ছিল, আর “spaceship” ডাকনামও সেখান থেকে এসেছে।

পরীক্ষা ও ইন্টারভিউতে
  • কোন কোন operator overload করা যায় না? — ::, ., .*, ?:, sizeof, alignof, typeid; আর নতুন চিহ্ন বানানো বা precedence বদলানো যায় না।
  • prefix আর postfix ++ কীভাবে আলাদা করা হয়? — postfix রূপে একটি অব্যবহৃত int প্যারামিটার: T operator++(int), যা পুরোনো মানের কপি ফেরত দেয়; prefix ফেরত দেয় T&.
  • operator<< কেন member হতে পারে না? — বাঁ দিকের operand std::ostream, যা আপনার class নয়; তাই non-member (প্রায়ই friend), আর চেইনের জন্য std::ostream& ফেরত দেয়।
  • কোন operator অবশ্যই member? — =, [], (), ->.
  • <=> default করলে কী পাওয়া যায়? — সদস্যভিত্তিক তুলনা আর নিজে থেকে defaulted ==; ফলে ছয়টি তুলনাই চলে, ফেরতের টাইপ strong, weak বা partial ordering.

৯ · AI-এর যুগে — operator কোডের ভুল ধরা

AI টুল operator overloading-এর কোড খুব তাড়াতাড়ি লিখে দেয়, আর সেটি প্রায়ই compile হয়ে চলে। সমস্যা হলো, অনলাইনে বিশাল পরিমাণ C++98-যুগের টিউটোরিয়াল আছে, যেখানে ছয়টি তুলনা হাতে লেখা, += কপি ফেরত দেয়, আর member ফাংশনে const নেই। আবার “দেখতে চতুর” কোডের লোভে AI কখনো কখনো অদ্ভুত অর্থে operator বসায়। এগুলো ধরার জন্য আজকের প্রতিটি নিয়ম কাজে লাগবে:

AI যা করেসমস্যাযা হওয়া উচিত
Money operator+=(Money o) — কপি ফেরত(a += b) += c-এ c যায় একটি temporary-তে; a-তে নয়Money& operator+=(const Money&), ফেরত *this
ছয়টি তুলনা ফাংশন আলাদা করে লেখালম্বা, অসঙ্গতির ঝুঁকি, নতুন সদস্যে হালনাগাদ ভোলাauto operator<=>(const T&) const = default;
তুলনা member-এ const নেইconst Money& দিয়ে তুলনা compile হয় না, std::sort-এ অদ্ভুত errorসব তুলনা আর পড়ার operator const
+ আর +=-এ আলাদা যুক্তিএকটিতে বাগ সারালে অন্যটিতে থেকে যায়+ বানান += দিয়ে
operator* শুধু memberv * 2 চলে, 2 * v চলে নাnon-member (hidden friend) দুই রূপ
implicit operator double() বা operator bool()অর্থহীন হিসাব নিঃশব্দে compile হয়নামওয়ালা ফাংশন, বা explicit
অদ্ভুত অর্থ: cart + item দিয়ে মোছা, ^ দিয়ে ঘাতপাঠকের বিস্ময়, precedence-এর ভুলcart.remove(item), std::pow বা power()
টাকা double-এ0.1 + 0.2-এর মতো গরমিল, আর তুলনা partial orderingপয়সায় পূর্ণসংখ্যা

AI-কে operator লিখতে বলার সময় আধুনিক নিয়মগুলো আগেই বলে দিন, আর প্রতিটি operator-এর অর্থ ব্যাখ্যা করতে বলুন — যে operator এক বাক্যে ব্যাখ্যা করা যায় না, সেটি সম্ভবত থাকা উচিত নয়:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি Money class লেখো, যা ভেতরে টাকা পয়সায় long long হিসেবে রাখে (double নয়)। নিয়ম: ১. += আর -= member, ফেরত দেবে Money& (*this); + আর - hidden friend, যা += / -= দিয়ে বানানো। ২. তুলনার জন্য শুধু auto operator<=>(const Money&) const = default; — ছয়টি আলাদা ফাংশন লিখবে না। ৩. Money * long long আর long long * Money দুটোই চলবে; Money * Money থাকবে না। ৪. constructor explicit; কোনো implicit conversion operator থাকবে না। ৫. operator<< (std::ostream& ফেরত) আর std::formatter<Money> দুটোই দাও, যাতে std::println("{}", m) চলে; ছাপা হবে "Tk 12.05" রূপে, ঋণাত্মক টাকাও ঠিকভাবে। ৬. &&, ||, কমা বা unary & overload করবে না। কোডের পরে একটি তালিকা দাও: প্রতিটি operator, তার এক লাইনের অর্থ, আর কেন member বা non-member.

AI-এর operator কোড যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Java কেন operator overloading বাদ দিল, আর C++ কেন রাখল? কে ঠিক?

১৯৯০-এর দশকের মাঝামাঝি Java যখন নকশা হচ্ছিল, তার লক্ষ্য ছিল C++-এর চেনা চেহারা রেখে জটিলতা কমানো। James Gosling পরে বলেছেন, operator overloading বাদ দেওয়া ছিল অনেকটা ব্যক্তিগত সিদ্ধান্ত — তিনি C++-এ এর প্রচুর অপব্যবহার দেখেছিলেন। যুক্তিটা বোঝা যায়: বড় দলে কেউ একজন + দিয়ে অদ্ভুত কিছু করলে কোড পড়া প্রত্যেকের জন্য কঠিন হয়ে যায়, কারণ চিহ্ন দেখে আর বোঝা যায় না কী হচ্ছে। Java-তে a + b দেখলে আপনি নিশ্চিত, এটি সংখ্যার যোগ বা string জোড়া — আর কিছু নয়।

কিন্তু দামও দিতে হয়েছে। Java-তে টাকার হিসাবের জন্য BigDecimal ব্যবহার করলে লিখতে হয় total = price.multiply(qty).add(tax).subtract(discount) — যা একটি সাধারণ সূত্রের চেয়ে অনেক কঠিন পড়া। মজার বিষয়, Java নিজেই একটি ব্যতিক্রম রেখেছে: String-এর +. অর্থাৎ ভাষার নকশাকাররাও মেনেছেন, কিছু ক্ষেত্রে operator ছাড়া চলে না — শুধু সেই ক্ষমতা ব্যবহারকারীকে দেননি।

C++-এর দর্শন আলাদা: Stroustrup-এর নীতি ছিল, ব্যবহারকারীর বানানো টাইপ যেন built-in টাইপের সমান মর্যাদা পায়। std::complex, std::string, std::chrono::duration — এগুলো লাইব্রেরির টাইপ, অথচ int-এর মতোই স্বাভাবিকভাবে ব্যবহার করা যায়। ভাষা নিজে বিশেষ সুবিধা নেয় না; আপনার Money-ও একই সুবিধা পায়। অপব্যবহারের ঝুঁকি C++ মেনে নিয়েছে, আর সমাধান খুঁজেছে নির্দেশিকা আর অভ্যাসে — যেমন Core Guidelines-এর নিয়ম, প্রচলিত অর্থ মেনে operator overload করুন।

পরের প্রজন্মের ভাষাগুলোর সিদ্ধান্ত দেখলে বোঝা যায় পাল্লা কোন দিকে। C#, Python, Kotlin, Swift আর Rust — সবগুলোতেই কোনো না কোনো রূপে operator overloading আছে; Kotlin আর Rust এমনকি এটিকে নির্দিষ্ট নামের ফাংশনের (যেমন plus বা Add trait) সাথে বেঁধে দিয়েছে, যাতে অর্থ সীমিত থাকে। Go আবার Java-র পথে গেছে, সরলতার জন্য। অর্থাৎ দুটি দর্শনই টিকে আছে: “ক্ষমতা দাও, শৃঙ্খলা শেখাও” বনাম “ক্ষমতা সীমিত রাখো, ভুলের সুযোগ কমাও”।

মূল উপলব্ধি: operator overloading-এর বিতর্ক আসলে প্রকাশক্ষমতা বনাম পূর্বানুমেয়তার বিতর্ক। C++ প্রকাশক্ষমতা বেছে নিয়েছে, তাই পূর্বানুমেয়তার দায়িত্ব আপনার — প্রতিটি overload যেন পাঠকের প্রত্যাশা মেনে চলে।

প্র ০২iostream-এর জন্য bit shift-এর চিহ্ন << কেন বেছে নেওয়া হলো, আর এই নকশা নিয়ে সমালোচনা কী?

C-তে ছাপার উপায় ছিল printf, যা format string-এর %d, %s দেখে আর্গুমেন্ট পড়ে। এর বড় দুর্বলতা: compiler সাধারণত যাচাই করতে পারে না যে %d-এর জায়গায় সত্যিই একটি int দেওয়া হয়েছে, আর নিজের টাইপ (struct) ছাপার কোনো পথ নেই। C++-এর প্রথম দিকে Stroustrup চেয়েছিলেন type-safe আর বাড়ানো যায় এমন একটি ব্যবস্থা — যেখানে প্রতিটি টাইপ নিজের ছাপার নিয়ম দিতে পারবে। operator overloading এর স্বাভাবিক হাতিয়ার: প্রতিটি টাইপের জন্য আলাদা operator<<, আর overload resolution compile-এর সময়েই সঠিকটি বেছে নেবে।

কিন্তু কোন চিহ্ন? নতুন চিহ্ন বানানো যায় না, তাই বিদ্যমান চিহ্ন থেকে বাছতে হতো। = বা তুলনার চিহ্নের precedence আর অর্থ মানানসই ছিল না। << আর >>-এর দুটি সুবিধা ছিল: দেখতে তীরের মতো, তাই “ডেটা stream-এর দিকে যাচ্ছে” আর “stream থেকে আসছে” — দিকটা চোখে পড়ে; আর এদের precedence যথেষ্ট কম, তাই cout << a + b বন্ধনী ছাড়াই চলে। bit shift এমনিতেও সাধারণ ছাপার কোডে প্রায় আসে না, তাই সংঘর্ষ কম। পরে Jerry Schwarz-এর হাতে iostream লাইব্রেরি নতুন করে নকশা হয়, আর সেটিই C++98-এ standard হয়।

সমালোচনাও কম নয়। প্রথমত, ফরম্যাটিং: std::hex বা std::setprecision-এর মতো manipulator stream-এর অবস্থা স্থায়ীভাবে বদলে দেয়, তাই একটি ফাংশনে hex চালু করে ভুলে গেলে পরের সব ছাপা hex-এ আসে। দ্বিতীয়ত, অনুবাদ: cout << "You have " << n << " messages"-এর বাক্য টুকরো টুকরো, তাই অন্য ভাষায় অনুবাদে শব্দক্রম বদলানো কঠিন — বাংলায় সংখ্যাটা বাক্যের অন্য জায়গায় বসতে পারে। তৃতীয়ত, precedence-এর ফাঁদ: cout << a < b বা cout << (x & 1)-এ বন্ধনী ভুললে অদ্ভুত error. চতুর্থত, গতি: locale আর virtual ফাংশনের স্তরের কারণে iostream প্রায়ই printf-এর চেয়ে ধীর।

এই সমালোচনার জবাব হিসেবেই C++20-এ std::format আর C++23-এ std::print এসেছে — format string ফিরেছে, কিন্তু এবার type-safe আর compile-এর সময় যাচাই হয়।

মূল উপলব্ধি: << দেখায় যে একটি “অপ্রচলিত” overload-ও টিকে যেতে পারে, যদি তা সঙ্গতিপূর্ণ আর দৃশ্যত স্বাভাবিক হয়। কিন্তু তার দুর্বলতাগুলোও মনে করিয়ে দেয় — চিহ্নের সৌন্দর্য পুরো নকশার মান নিশ্চিত করে না।

প্র ০৩C++20-এর “rewritten comparison candidate” কী সমস্যা মেটাল, আর নতুন কী সমস্যা তৈরি করল?

C++20-এর আগে তুলনার তিনটি পুরোনো যন্ত্রণা ছিল। এক, ছয়টি ফাংশন হাতে লেখা। দুই, symmetry: money == 0 চালাতে চাইলে operator==(Money, int) আর operator==(int, Money) দুটোই লিখতে হতো, আর !=-এর জন্য আরও দুটি — একটি টাইপ-জোড়ার জন্যই চারটি ফাংশন। তিন, অসঙ্গতি: কেউ == বদলালেন কিন্তু != ভুলে গেলেন, এখন a == b আর a != b দুটোই true হতে পারে।

C++20-এর সমাধান: compiler তুলনার expression-কে নিজে নতুন করে লিখতে পারবে। a != b দেখলে সে !(a == b)-ও প্রার্থী হিসেবে বিবেচনা করে; a == b-এর জন্য উল্টো দিক b == a-ও দেখে; আর a < b-এর জন্য (a <=> b) < 0 আর 0 < (b <=> a). ফলে একটি operator==(Money, int) লিখলেই চারটি রূপ চলে। নকশার সময় আরেকটি গুরুত্বপূর্ণ সিদ্ধান্ত হয়েছিল: প্রথম প্রস্তাবে ==-ও <=> থেকে আসত, কিন্তু Barry Revzin-সহ কয়েকজন দেখান যে string বা vector-এর মতো টাইপে সমতা যাচাই অনেক দ্রুত করা যায় (দৈর্ঘ্য আলাদা হলেই উত্তর “না”), অথচ <=>-কে পুরো ক্রম বের করতে হয়। তাই চূড়ান্ত নিয়মে == আলাদা থাকল, আর != শুধু == থেকে আসে।

নতুন সমস্যা কোথায়? পুরোনো কোড। C++17-এ অনেকে অসম আকারের তুলনা লিখতেন — যেমন == member, যার প্যারামিটার const নয়, বা একই তুলনার দুটি কিছুটা আলাদা সংস্করণ। C++20-এ উল্টো প্রার্থী যোগ হওয়ায় হঠাৎ দুটি প্রার্থী সমান ভালো হয়ে গেল — ambiguous error, বা আরও খারাপ, ভিন্ন ফাংশন বাছাই। কিছু জনপ্রিয় লাইব্রেরি নতুন standard-এ compile হতে সমস্যায় পড়েছিল। পরে আরেকটি সংশোধনী (C++23-এ, পুরোনো standard-এর ওপরও প্রয়োগযোগ্য সংশোধন হিসেবে) নিয়মটা কিছুটা নরম করে, যাতে বেশিরভাগ পুরোনো কোড আবার স্বাভাবিকভাবে চলে। compiler-গুলো এখন এমন ক্ষেত্রে প্রায়ই সতর্কবার্তা দেয়।

শিক্ষাটা গভীর: ভাষার নিয়ম “স্মার্ট” করলে ব্যবহারকারীর কোড সরল হয়, কিন্তু compiler-এর সিদ্ধান্ত বোঝা কঠিন হয়। কেন এই ফাংশনটি বাছাই হলো, তা বুঝতে এখন rewritten আর reversed প্রার্থীর কথাও মাথায় রাখতে হয়।

মূল উপলব্ধি: নতুন কোডে <=> আর == একবার, সমানভাবে (const, একই টাইপের প্যারামিটার) লিখুন — তাহলে rewritten candidate আপনার বন্ধু। অসম, হাতে লেখা পুরোনো তুলনাগুলোই নতুন নিয়মে বিপদ বাধায়।

প্র ০৪overload করা && আর || কেন short-circuit হারায়? কোথাও কি এর বৈধ ব্যবহার আছে?

built-in && কোনো সাধারণ ফাংশন নয় — এটি একটি নিয়ন্ত্রণ-কাঠামো, অনেকটা if-এর মতো। p && p->isReady()-এ compiler এমন কোড বানায়: আগে p দেখো; মিথ্যা হলে ডান দিক ছোঁবেই না। এই নিশ্চয়তার ওপর লক্ষ লক্ষ লাইন কোড দাঁড়িয়ে আছে — null pointer যাচাই, সীমা যাচাই (i < n && v[i] > 0), ব্যয়বহুল হিসাব এড়ানো।

এখন অংশ ২-এর মূল কথা মনে করুন: overload করা operator একটি ফাংশন ডাকা। আর C++-এ যেকোনো ফাংশন ডাকার আগে তার সব আর্গুমেন্ট হিসাব করা হয়। operator&&(a, b) ডাকতে হলে b-ও আগে হিসাব হতে হবে — ফাংশনের ভেতরে ঢোকার আগেই। তাই আপনার overload যা-ই করুক, ডান দিক সবসময় চলে। কোড দেখতে if (p && p->ok())-এর মতো, কিন্তু আচরণ bool x = p; bool y = p->ok(); ...-এর মতো — p খালি হলে crash. সবচেয়ে বিপজ্জনক দিক হলো, পাঠক কোনো চিহ্নই দেখেন না যে এখানে নিয়ম বদলেছে। C++17 অবশ্য overload করা কিছু operator-এর জন্য আর্গুমেন্ট হিসাবের ক্রম বেঁধে দিয়েছে (বাঁ দিক আগে), কিন্তু “ডান দিক বাদ দেওয়া” কোনো ফাংশন ডাকার পক্ষে সম্ভব নয়। একই যুক্তিতে overload করা কমা operator-ও পাঠকের প্রত্যাশা ভাঙে।

তাহলে ভাষা অনুমতি দেয় কেন? কারণ কিছু বিশেষ লাইব্রেরি আছে, যেখানে && কিছু হিসাব করে না, বরং একটি “বর্ণনা” বানায়। যেমন expression template লাইব্রেরি (Boost.Proto, কিছু parser বা query লাইব্রেরি): age > 18 && city == "Dhaka" লিখলে সেটি তখনই সত্য-মিথ্যা বের করে না, বরং একটি গাছ বানায়, যা পরে ডেটাবেসের query-তে অনুবাদ হয়। সেখানে short-circuit-এর প্রশ্নই নেই, কারণ দুই দিকই শুধু বর্ণনা। এগুলো উচ্চস্তরের, বিশেষজ্ঞদের লাইব্রেরি, আর তাদের ব্যবহারকারীরা জানেন যে তারা একটি আলাদা ছোট ভাষার (DSL) ভেতরে আছেন।

বেশিরভাগ C++ কোডিং নির্দেশিকা তাই সাধারণ কোডে &&, || আর কমা overload না করার পরামর্শ দেয়। bool-এর মতো আচরণ চাইলে explicit operator bool দিন — তখন built-in && নিজেই কাজ করবে, short-circuit সহ।

মূল উপলব্ধি: কিছু operator শুধু হিসাব নয়, নিয়ন্ত্রণ-প্রবাহের প্রতিশ্রুতি। ফাংশন সেই প্রতিশ্রুতি রাখতে পারে না, তাই এই operator-দের overload করলে চিহ্নটি থাকে, কিন্তু প্রতিশ্রুতি হারায়।

প্র ০৫std::format/std::formatter বনাম operator<< — নিজের টাইপের জন্য কোনটি দেবেন, আর কেন C++ ছাপার পথ বদলাল?

std::format-এর জন্ম একটি জনপ্রিয় open-source লাইব্রেরি থেকে — Victor Zverovich-এর {fmt}. বহু বছর ধরে বাস্তব প্রজেক্টে ব্যবহৃত হওয়ার পর সেটি C++20-এ std::format হিসেবে standard-এ আসে, আর C++23-এ আসে std::print ও std::println. ধারণাটি Python-এর str.format-এর কাছাকাছি: "{} টাকা"-এর মতো একটি format string, যার {} জায়গাগুলোয় মান বসে।

এটি iostream-এর প্রায় সব দুর্বলতা মেটায়। format string একটি পূর্ণ বাক্য, তাই অনুবাদে শব্দক্রম বদলানো সহজ। কোনো স্থায়ী অবস্থা নেই — {:x} শুধু ওই একটি মানকে hex-এ দেখায়, পরের ছাপাকে নয়। format string আর আর্গুমেন্টের মিল compile-এর সময় যাচাই হয়, তাই printf-এর মতো ভুল টাইপের বিপদ নেই। আর বাস্তব মাপজোকে এটি প্রায়ই iostream-এর চেয়ে দ্রুত। নিজের টাইপের জন্য std::formatter specialization দিলে আপনার Money বা Vec2 প্রস্থ, alignment-এর নিয়মসহ সব জায়গায় চলে — এমনকি একটি std::vector<Money>-ও C++23-এর range formatting দিয়ে সরাসরি ছাপা যায়।

তাহলে operator<< কি মৃত? না। তিনটি কারণে এটি এখনো থাকবে। এক, বিপুল পরিমাণ বিদ্যমান কোড আর লাইব্রেরি stream ব্যবহার করে — logging লাইব্রেরি, test framework (অনেক test framework ব্যর্থ assertion-এর মান দেখাতে << খোঁজে), std::stringstream-ভিত্তিক কোড। দুই, ফাইলে লেখা বা custom stream-এ লেখার জন্য stream-এর পুরো ব্যবস্থা এখনো কাজের। তিন, পুরোনো compiler, যেখানে <format> সম্পূর্ণ নেই।

বাস্তব পরামর্শ: নতুন টাইপে আগে std::formatter দিন। যদি টাইপটি এমন কোডের সাথে মেশে যা stream চায়, তবে operator<<-ও দিন — কিন্তু ছাপার যুক্তি এক জায়গায় রাখুন; যেমন operator<<-এর ভেতরে os << std::format("{}", m). ঠিক এভাবেই এই পর্বের Money ভেতরে std::format ব্যবহার করেছে। দুটি জায়গায় আলাদা ছাপার নিয়ম থাকলে একদিন দুটি আলাদা ফল আসবে — + আর +=-এর সেই একই শিক্ষা।

মূল উপলব্ধি: ভাষা বিবর্তিত হয় বাস্তব ব্যবহারের অভিজ্ঞতা থেকে। << type-safety-র সমস্যা মিটিয়েছিল, কিন্তু অনুবাদ আর অবস্থার সমস্যা তৈরি করেছিল; std::format সেগুলো মেটায়। আপনার কাজ নতুনটিকে ডিফল্ট করা আর পুরোনোটির সাথে সেতু রাখা।

অনুশীলন

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

  1. ফল অনুমান: Money a(10), b(15); তারপর (a += b) += Money(1);. (ক) += যদি Money& ফেরত দেয়, তবে শেষে a কত টাকা? (খ) যদি += ভুল করে Money (কপি) ফেরত দেয়, তবে?

    (ক) ২৬ টাকা। প্রথম a += b a-কে ২৫ বানায় আর a-এরই reference ফেরত দেয়, তাই দ্বিতীয় += Money(1)-ও a-তে যায়। (খ) ২৫ টাকা। প্রথম += a-কে ২৫ বানায়, কিন্তু ফেরত দেয় একটি কপি; দ্বিতীয় += সেই temporary কপিতে ১ যোগ করে, যা লাইন শেষে মুছে যায়। কোনো error বা warning ছাড়াই ১ টাকা হারিয়ে গেল — এজন্যই += সবসময় reference ফেরত দেয়।

  2. overload করবেন কি না: কারণসহ বলুন — (ক) Date - Date, ফল দিনের সংখ্যা (খ) Student + Student (গ) Playlist[i], i-তম গান (ঘ) Matrix * Matrix (ঙ) !file দিয়ে ফাইল মুছে ফেলা।

    (ক) হ্যাঁ — দুটি তারিখের ব্যবধান সর্বজনীন ধারণা; তবে ফল Date নয়, দিনের সংখ্যা বা একটি duration টাইপ (std::chrono ঠিক এভাবেই করে)। আর Date + Date অর্থহীন, তাই সেটি দেবেন না। (খ) না — অর্থ অস্পষ্ট; makeGroup(a, b) লিখুন। (গ) হ্যাঁ — পাত্রের i-তম উপাদান, const আর non-const দুই রূপে। (ঘ) হ্যাঁ — গণিতে matrix গুণের সুনির্দিষ্ট অর্থ আছে; তবে আকার না মিললে কী হবে (exception?), তা নথিভুক্ত করুন। (ঙ) কখনোই না — ! মানে “না/খালি কি না”, যা কিছু বদলায় না; মুছে ফেলার মতো ধ্বংসাত্মক কাজ সবচেয়ে স্পষ্ট নামে হওয়া উচিত: file.remove().

  3. বাগ শিকার: একটি AI এই কোড দিয়েছে — struct Money { double taka; Money operator+=(Money o) { taka += o.taka; return *this; } bool operator==(Money o) { return taka == o.taka; } bool operator<(Money o) { return taka < o.taka; } /* আরও চারটি তুলনা */ operator double() { return taka; } };. কয়টি সমস্যা খুঁজে পান?

    অন্তত পাঁচটি। (১) টাকা double-এ — পয়সার গরমিল; long long পয়সায় রাখুন। (২) += কপি ফেরত দেয় — Money& হওয়া উচিত। (৩) তুলনা ফাংশনগুলো const নয় — const Money& দিয়ে তুলনা বা std::sort-এ compile error. (৪) ছয়টি তুলনা হাতে — auto operator<=>(const Money&) const = default; যথেষ্ট। (৫) implicit operator double() — price + 5, price * price নিঃশব্দে compile হয়; এটি মুছে নামওয়ালা ফাংশন দিন। বোনাস: double-এ == দিয়ে তুলনা নিজেই বিপজ্জনক (0.1 + 0.2 == 0.3 মিথ্যা)।

  4. নিজে লিখুন (কোড): উপরের money.cpp-এ একটি std::formatter<Money> যোগ করুন, যাতে std::println("bill = {}", bill); চলে। ইঙ্গিত: Money-র private পয়সা পড়তে একটি public long long poisha() const ফাংশন দিন, আর ছাপার যুক্তি যেন দুই জায়গায় না থাকে।

    একটি উপায়: class-এ long long poisha() const { return poisha_; } যোগ করুন। তারপর class-এর বাইরে template <> struct std::formatter<Money> : std::formatter<std::string> { auto format(const Money& m, std::format_context& ctx) const { return std::formatter<std::string>::format(std::format("Tk {}.{:02}", m.poisha() / 100, m.poisha() % 100), ctx); } }; (<print> আর <string> include করুন)। এবার class-এর ভেতরের hidden friend operator<< মুছে দিন, আর formatter-এর পরে, class-এর বাইরে লিখুন std::ostream& operator<<(std::ostream& os, const Money& m) { return os << std::format("{}", m); }. (class-এর ভেতরে রাখলে compile error আসে, কারণ সেখানে তখনো std::formatter<Money> ঘোষিত হয়নি।) তাহলে ছাপার নিয়ম শুধু formatter-এ, আর দুটি পথ সবসময় একই ফল দেবে।

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

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

ক্রমC++-এ std::cout << m << '\n'; চলে, যেখানে m একটি নিজের টাইপ আর তার operator<< একটি std::ostream& ফেরত দেয়। ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. compiler বাঁয়ের অংশ std::cout << m-কে operator<<(std::cout, m) ডাকায় বদলায়
  2. ফাংশনের ভেতরে os (মানে cout)-এ m-এর লেখা পাঠানো হয়
  3. ফাংশন os, অর্থাৎ cout-এর reference ফেরত দেয়
  4. ফেরত পাওয়া stream-কে বাঁ দিকে রেখে << '\n' ডাকা হয়
  5. newline ছাপা হয়, পুরো লাইন শেষ

C++-এ নিচের কোন operator overload করা যায় না?

  • +=
  • []
  • ?:
  • <<

C++-এ একটি Counter class-এর postfix increment (c++)-এর সঠিক ঘোষণা কোনটি?

  • Counter& operator++();
  • Counter operator++(int);
  • Counter& operator++(int);
  • Counter operator++post();

C++-এ struct Vec2 { int x, y; Vec2 operator*(int k) const; }; — শুধু এই member operator আছে। Vec2 v{1, 2}; হলে 2 * v লিখলে কী হয়?

  • compiler নিজে উল্টে v * 2 ডাকে
  • v-কে int বানিয়ে গুণ হয়
  • চলার সময় undefined behavior
  • Compile error — operator*(int, Vec2) নেই

একটি C++20 class-এ শুধু auto operator<=>(const P&) const = default; লেখা আছে, আর কোনো তুলনা ফাংশন নেই। কোন তুলনাগুলো ব্যবহার করা যাবে?

  • == != < <= > >= — ছয়টিই
  • শুধু < <= > >=, == চলে না
  • শুধু <=> নিজে
  • != ছাড়া বাকি পাঁচটি

একটি C++ Money class-এ operator+=-এর ফেরতের টাইপ প্রচলিত রীতিতে কী হওয়া উচিত?

  • Money (by value)
  • Money&, যা *this ফেরত দেয়
  • void
  • const Money&

C++-এ একটি class-এর জন্য operator&& overload করলে built-in &&-এর কোন বৈশিষ্ট্য হারিয়ে যায়?

  • ফল সবসময় bool হওয়া
  • precedence
  • short-circuit — ডান দিক সবসময় হিসাব হয়
  • দুটি operand নেওয়ার ক্ষমতা

শব্দকোষ

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

মনে রাখুন

  1. operator = বিশেষ নামের ফাংশন। a + b হয় operator+(a, b); precedence, associativity আর operand-সংখ্যা বদলায় না। অর্থ স্পষ্ট হলেই overload — নইলে নামওয়ালা ফাংশন।
  2. আগে += (member, ফেরত T&), তার ওপর + (hidden friend, প্রথম প্যারামিটার by value). বাঁ দিক নিজের টাইপ না হলে non-member ছাড়া উপায় নেই — 2 * v.
  3. তুলনা: auto operator<=>(const T&) const = default; — ছয়টি তুলনা, strong/weak/partial ordering; সদস্যরা সত্যিই “মান” কি না দেখে নিন।
  4. ছাপা: operator<< নেয় ও ফেরত দেয় std::ostream&; নতুন কোডে std::formatter, যাতে std::println চলে। ছাপার যুক্তি এক জায়গায়।
  5. [] দুই রূপে, () দিয়ে functor, prefix ++ আগে; bool-এ রূপান্তর explicit; &&, ||, কমা, unary & নয়।

পরের পর্বে: এতদিন প্রতিটি class একা দাঁড়িয়ে ছিল। কিন্তু একটি SavingsAccount কি একটি Account-এরই বিশেষ রূপ নয়? inheritance — একটি class থেকে আরেকটি গড়া, আর কখন সেটি ভালো নকশা, কখন ফাঁদ।

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

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