Kodax

النسخ والنقل: Rule of 3/5/0 — OOP في C++

النسخ والنقل: قاعدة 3/5/0

النسخ الافتراضي

إذا لم تكتب شيئاً، يولّد المترجم مُنشئ نسخ وعامل إسناد ينسخان كل عضو بدوره. هذا ممتاز لأعضاء مثل int و string و vector، وكارثي للمؤشرات التي تملك ذاكرة:

#include <cstring>

class BadString {
    char* data;
public:
    BadString(const char* s) : data(new char[std::strlen(s) + 1]) { std::strcpy(data, s); }
    ~BadString() { delete[] data; }
    // لا مُنشئ نسخ ← النسخ الافتراضي ينسخ المؤشر فقط
};
// BadString a("hi");
// BadString b = a;   // b.data == a.data (نسخ سطحي)
// عند نهاية النطاق: delete[] لنفس الذاكرة مرتين ← سلوك غير معرّف (غالباً انهيار)

متى يُستدعى كل شيء؟

#include <string>
using std::string;

string a = "x";
string b = a;              // مُنشئ النسخ: كائن جديد من موجود
string c(a);               // مُنشئ النسخ أيضاً
// b = c;                  // عامل الإسناد: كائن موجود يأخذ قيمة آخر
// void f(string s); f(a); // مُنشئ النسخ عند التمرير بالقيمة

القاعدة: إذا كان الكائن يُنشأ الآن فهو مُنشئ؛ إذا كان موجوداً مسبقاً فهو إسناد.

قاعدة الثلاثة (Rule of Three)

إذا احتاج صنفك أحد هذه، فهو غالباً يحتاج الثلاثة:

1. المدمّر ~T()

2. مُنشئ النسخ T(const T&)

3. عامل إسناد النسخ T& operator=(const T&)

السبب: الحاجة لمدمّر تعني أن الصنف يملك مورداً، والمورد يحتاج نسخاً عميقاً (deep copy).

الإسناد الآمن: copy-and-swap

الطريقة الساذجة (delete ثم new) تترك الكائن مكسوراً إذا فشل new. الحل المعتمد:

#include <cstring>
#include <utility>

class Text {
    char* data;
public:
    Text(const char* s = "") : data(new char[std::strlen(s) + 1]) { std::strcpy(data, s); }
    ~Text() { delete[] data; }
    Text(const Text& o) : Text(o.data) {}                 // نسخ عميق (تفويض للمُنشئ)
    Text& operator=(Text other) {                         // لاحظ: بالقيمة ← النسخ يحدث هنا
        std::swap(data, other.data);                      // نبادل، والقديم يُحرَّر مع other
        return *this;
    }
    const char* c_str() const { return data; }
};

إذا فشل النسخ يرمي قبل أن نلمس الكائن، والإسناد الذاتي t = t يعمل تلقائياً.

قاعدة الخمسة (Rule of Five) — C++11

النسخ العميق مكلف. إذا كان المصدر مؤقتاً سيموت قريباً، لماذا لا "نسرق" مورده؟ هذا هو النقل:

4. مُنشئ النقل T(T&&) noexcept

5. عامل إسناد النقل T& operator=(T&&) noexcept

النقل ينقل المؤشر ويترك المصدر فارغاً لكن صالحاً (يمكن تدميره أو الإسناد له). اكتبه noexcept — وإلا لن يستخدمه vector عند التوسّع وسيلجأ للنسخ. ستتعمق في النقل في دورة C++ المتقدم.

قاعدة الصفر (Rule of Zero) — الأفضل

أفضل صنف هو الذي لا يحتاج أياً من الخمسة: اجعل الأعضاء أنواعاً تدير نفسها (string، vector، unique_ptr)، والمترجم سيولّد النسخ والنقل والتدمير الصحيحة:

#include <string>
#include <vector>
using namespace std;

class Playlist {          // لا مدمّر، لا نسخ يدوي — وكل شيء صحيح
    string name;
    vector<string> songs;
public:
    explicit Playlist(string n) : name(move(n)) {}
    void add(string s) { songs.push_back(move(s)); }
};

اكتب الخمسة فقط عندما تبني بنفسك صنفاً منخفض المستوى يدير مورداً.

default و delete

class Socket {
public:
    Socket() = default;
    Socket(const Socket&) = delete;             // غير قابل للنسخ
    Socket& operator=(const Socket&) = delete;
    Socket(Socket&&) = default;                 // لكن قابل للنقل
    Socket& operator=(Socket&&) = default;
};

= delete يحوّل خطأ وقت التشغيل (نسخ مورد لا يُنسخ) إلى خطأ ترجمة واضح.

تمرين

شغّل المثال الكامل وتتبّع كل سطر مطبوع: أي عملية استدعت أي دالة؟ ثم احذف مُنشئ النسخ من MyString، وتوقّع ما سيحدث عند MyString s2(s1); ولماذا.

#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
    char* data;
    size_t length;

public:
    // Constructor
    MyString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
        cout << "📦 Constructor: " << data << endl;
    }
    
    // Destructor
    ~MyString() {
        cout << "🗑️ Destructor: " << (data ? data : "null") << endl;
        delete[] data;
    }
    
    // Copy Constructor
    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
        cout << "📋 Copy Constructor: " << data << endl;
    }
    
    // Copy Assignment Operator
    MyString& operator=(const MyString& other) {
        cout << "📝 Copy Assignment" << endl;
        
        if (this == &other) return *this;  // self-assignment check
        
        delete[] data;  // تحرير القديم
        
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
        
        return *this;
    }
    
    // Move Constructor (C++11)
    MyString(MyString&& other) noexcept {
        data = other.data;
        length = other.length;
        
        other.data = nullptr;
        other.length = 0;
        
        cout << "🚀 Move Constructor: " << data << endl;
    }
    
    // Move Assignment Operator (C++11)
    MyString& operator=(MyString&& other) noexcept {
        cout << "🚀 Move Assignment" << endl;
        
        if (this == &other) return *this;
        
        delete[] data;
        
        data = other.data;
        length = other.length;
        
        other.data = nullptr;
        other.length = 0;
        
        return *this;
    }
    
    // Getters
    const char* c_str() const { return data; }
    size_t size() const { return length; }
    
    // طباعة
    friend ostream& operator<<(ostream& os, const MyString& s) {
        return os << s.data;
    }
};

MyString createString(const char* str) {
    return MyString(str);  // C++17: حذف نسخ مضمون — يُبنى الكائن مباشرة في مكانه دون نسخ أو نقل
}

int main() {
    cout << "=== إنشاء s1 ===" << endl;
    MyString s1("مرحبا");
    
    cout << "\n=== Copy Constructor ===" << endl;
    MyString s2(s1);  // Copy Constructor
    
    cout << "\n=== Copy Assignment ===" << endl;
    MyString s3;
    s3 = s1;  // Copy Assignment
    
    cout << "\n=== Move Constructor ===" << endl;
    MyString s4(move(s1));  // Move Constructor
    
    cout << "\n=== Move Assignment ===" << endl;
    MyString s5;
    s5 = createString("عالم");  // Move Assignment: الطرف الأيمن كائن مؤقت
    
    cout << "\n=== القيم ===" << endl;
    cout << "s2: " << s2 << endl;
    cout << "s3: " << s3 << endl;
    cout << "s4: " << s4 << endl;
    cout << "s5: " << s5 << endl;
    
    cout << "\n=== انتهاء البرنامج ===" << endl;
    
    return 0;
}
اختبر فهمك