رفتن به محتوا

سربارگذاری عملگرها

نحوه صحیح سربارگذاری عملگرها را بیاموزید — عضو در مقابل غیر عضو، فرم‌های استاندارد، عملگرهای جریان، عملگر فضاپیمای ‎C++‎20 و دام‌های رایج.

فلسفه سربارگذاری عملگرها

‎C++‎ به شما اجازه می‌دهد نحوه کار عملگرها (+، ==، <<، [] و غیره) را برای انواع خود تعریف کنید. قانون طلایی: مانند اعداد صحیح عمل کنید. عملگرهای سربارگذاری‌شده باید همان‌طور که کاربران از انواع داخلی انتظار دارند رفتار کنند. اگر a + b مقدار a را تغییر دهد، انتظارات کاربر را نقض کرده‌اید.

سربارگذاری عملگرها چیزی است که انواعی مانند std::string، std::vector و std::complex را طبیعی به نظر می‌رساند. اما استفاده نامناسب، کد گیج‌کننده و غیرقابل نگهداری ایجاد می‌کند. هرگز عملگرها را طوری سربارگذاری نکنید که کار شگفت‌آوری انجام دهند.

عملگرهای عضو در مقابل غیر عضو

برخی عملگرها باید عضو باشند (=، []، ->، ()). بقیه بهتر است غیر عضو باشند (به‌ویژه عملگرهای دوتایی مانند + و ==) تا تبدیل‌های متقارن ممکن شود. الگوی استاندارد: += را به‌عنوان عضو و سپس + را به‌عنوان غیر عضو با استفاده از += پیاده‌سازی کنید.

operators.cpp
#include <iostream>

class Vec2 {
    double x_, y_;

public:
    Vec2(double x = 0, double y = 0) : x_{x}, y_{y} {}

    // += is a member: modifies *this
    Vec2& operator+=(const Vec2& rhs) {
        x_ += rhs.x_;
        y_ += rhs.y_;
        return *this;
    }

    // Unary minus (member)
    Vec2 operator-() const {
        return Vec2{-x_, -y_};
    }

    // Accessors for non-member operators
    double x() const { return x_; }
    double y() const { return y_; }

    // [] operator (must be member)
    double& operator[](int i) {
        return i == 0 ? x_ : y_;
    }
    const double& operator[](int i) const {
        return i == 0 ? x_ : y_;
    }
};

// + is a non-member using +=  (canonical form)
Vec2 operator+(Vec2 lhs, const Vec2& rhs) {  // lhs by value!
    lhs += rhs;
    return lhs;
}

// == as non-member (symmetric: allows conversions on both sides)
bool operator==(const Vec2& a, const Vec2& b) {
    return a.x() == b.x() && a.y() == b.y();
}

// Stream output operator (must be non-member)
std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    return os << '(' << v.x() << ", " << v.y() << ')';
}

int main() {
    Vec2 a{1.0, 2.0}, b{3.0, 4.0};
    Vec2 c = a + b;           // uses non-member operator+
    std::cout << c << '\n';   // (4, 6)
    std::cout << -c << '\n';  // (-4, -6)
    std::cout << c[0] << '\n'; // 4
    std::cout << std::boolalpha << (a == b) << '\n'; // false
    return 0;
}

عملگر فضاپیمای ‎C++‎20 (<=>)

‎C++‎20 عملگر مقایسه سه‌طرفه <=> (عملگر «فضاپیما») را معرفی می‌کند. یک تعریف واحد operator<=> تمام شش عملگر مقایسه (<، >، <=، >=، ==، !=) را به‌صورت خودکار تولید می‌کند. وقتی مقایسه عضو به عضو همان چیزی است که می‌خواهید از = default استفاده کنید.

spaceship.cpp
#include <iostream>
#include <compare>
#include <string>

class Version {
    int major_;
    int minor_;
    int patch_;

public:
    Version(int major, int minor, int patch)
        : major_{major}, minor_{minor}, patch_{patch} {}

    // Defaulted <=>: compares members in declaration order
    auto operator<=>(const Version&) const = default;

    friend std::ostream& operator<<(std::ostream& os, const Version& v) {
        return os << v.major_ << '.' << v.minor_ << '.' << v.patch_;
    }
};

class Student {
    std::string name_;
    double gpa_;

public:
    Student(const std::string& name, double gpa)
        : name_{name}, gpa_{gpa} {}

    // Custom <=>: compare only by GPA
    auto operator<=>(const Student& other) const {
        return gpa_ <=> other.gpa_;
    }

    // Need separate == if <=> is not defaulted
    bool operator==(const Student& other) const {
        return gpa_ == other.gpa_;
    }

    friend std::ostream& operator<<(std::ostream& os, const Student& s) {
        return os << s.name_ << " (GPA: " << s.gpa_ << ')';
    }
};

int main() {
    Version v1{2, 0, 0}, v2{1, 9, 5};
    std::cout << v1 << " > " << v2 << "? "
              << std::boolalpha << (v1 > v2) << '\n';  // true
    std::cout << (v1 == v2) << '\n';  // false

    Student alice{"Alice", 3.9};
    Student bob{"Bob", 3.7};
    std::cout << (alice > bob) << '\n';  // true (higher GPA)
    return 0;
}

operator() — اشیاء تابعی

سربارگذاری operator() یک فانکتور (شیء تابعی) ایجاد می‌کند. فانکتورها می‌توانند حالت داشته باشند که آن‌ها را انعطاف‌پذیرتر از اشاره‌گرهای تابع ساده می‌کند. آن‌ها به‌طور گسترده با الگوریتم‌های STL استفاده می‌شوند.

functor.cpp
#include <iostream>
#include <vector>
#include <algorithm>

// A functor that counts how many times it's called
class Counter {
    int count_ = 0;
    int threshold_;

public:
    explicit Counter(int threshold) : threshold_{threshold} {}

    bool operator()(int value) {
        ++count_;
        return value > threshold_;
    }

    int calls() const { return count_; }
};

int main() {
    std::vector<int> nums{1, 5, 3, 8, 2, 9, 4, 7, 6};

    Counter above_five{5};
    auto count = std::count_if(nums.begin(), nums.end(), above_five);
    std::cout << count << " elements above 5\n";  // 4

    // Functors can also be used directly
    Counter is_positive{0};
    std::cout << std::boolalpha;
    std::cout << is_positive(42) << '\n';   // true
    std::cout << is_positive(-1) << '\n';   // false
    std::cout << "Calls: " << is_positive.calls() << '\n';  // 2

    return 0;
}
دام

عملگرها را طوری سربارگذاری نکنید که کار شگفت‌آوری انجام دهند. operator+ باید جمع کند، نه تفریق. operator* روی یک Matrix باید ضرب ماتریسی انجام دهد، نه ضرب عنصر به عنصر (مگر اینکه دامنه شما آن را انتظار داشته باشد). برخی عملگرها قابل سربارگذاری نیستند: ::، .، .*، ?: و sizeof. همچنین، سربارگذاری && و || ارزیابی اتصال‌کوتاه را از بین می‌برد — تقریباً هرگز ایده خوبی نیست.

بهترین شیوه

از فرم‌های استاندارد پیروی کنید: تخصیص ترکیبی (+=، -=) را به‌عنوان عضو پیاده‌سازی کنید، سپس عملگرهای دوتایی (+، -) را به‌عنوان غیر عضو با استفاده از نسخه ترکیبی تعریف کنید. همیشه operator== را اگر operator< را پیاده‌سازی می‌کنید پیاده‌سازی کنید (یا بهتر، از <=> در ‎C++‎20 برای دریافت هر شش تا استفاده کنید). عملگرهای جریان (<<، >>) را به‌عنوان دوستان غیر عضو پیاده‌سازی کنید. از explicit روی عملگرهای تبدیل برای جلوگیری از تبدیل‌های ضمنی تصادفی استفاده کنید.

نکات کلیدی
  • از اصل «مانند اعداد صحیح عمل کنید» پیروی کنید — عملگرها باید همان‌طور که کاربران انتظار دارند رفتار کنند
  • تخصیص ترکیبی (+=) را به‌عنوان عضو و سپس + را به‌عنوان غیر عضو با استفاده از += پیاده‌سازی کنید
  • operator<=> در ‎C++‎20 تمام شش عملگر مقایسه را از یک تعریف واحد تولید می‌کند
  • سربارگذاری operator() فانکتورها ایجاد می‌کند — اشیائی که مانند توابع قابل فراخوانی هستند
  • && و || هنگام سربارگذاری ارزیابی اتصال‌کوتاه را از دست می‌دهند — از سربارگذاری آن‌ها اجتناب کنید
  • برخی عملگرها قابل سربارگذاری نیستند: ::، .، .*، ?:، sizeof

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. چرا `operator+` معمولاً به‌عنوان تابع غیر عضو پیاده‌سازی می‌شود؟

2. `auto operator<=>(const T&) const = default;` در ‎C++‎20 چه کاری انجام می‌دهد؟

3. چرا باید از سربارگذاری `operator&&` و `operator||` اجتناب کنید؟