c++ - ناطق - ما هي قاعدة الثلاثة؟




معنى كلمة title بالعربية (6)

ماذا يعني تقليد كائن ما ؟ ما هو مُنشئ النسخ ومُشغل تخصيص النسخة ؟ متى أحتاج إلى الإعلان عن نفسي؟ كيف يمكنني منع نسخ الكائنات الخاصة بي؟

https://code.i-harness.com


متى أحتاج إلى الإعلان عن نفسي؟

تنص قاعدة الثلاثة أنه إذا أعلنت أيًا من

  1. منشئ نسخة
  2. مشغل تخصيص نسخة
  3. المدمر

يجب عليك أن تعلن عن الثلاثة. نشأ عن الملاحظة أن الحاجة إلى الاستيلاء على معنى عملية النسخ تنبع دائما تقريبا من الطبقة التي تؤدي نوعا من إدارة الموارد ، وهذا يعني دائما تقريبا أن

  • أيا كانت إدارة الموارد التي يجري القيام بها في عملية نسخ واحدة ربما هناك حاجة إلى القيام به في عملية النسخ الأخرى و

  • كما سيشارك دستور الطبقة في إدارة الموارد (عادة ما يطلقها). كان المورد الكلاسيكي الذي يتم إدارته هو الذاكرة ، وهذا هو السبب في أن جميع فئات "ستاندرد للمكتبة" التي تدير الذاكرة (على سبيل المثال ، حاويات STL التي تقوم بإدارة الذاكرة الديناميكية) تعلن جميعها "الثلاثة الكبار": كل من عمليات النسخ والمدمرة.

نتيجة "القاعدة الثالثة" هي أن وجود مدمر مستخدم معلن يشير إلى أنه من غير المحتمل أن تكون نسخة العضو البسيطة الحكيمة مناسبة لعمليات النسخ في الفصل. وهذا ، بدوره ، يشير إلى أنه إذا صرحت إحدى الفئات بدمار ، فمن المحتمل ألا يتم إنشاء عمليات النسخ تلقائيًا ، لأنهم لن يفعلوا الشيء الصحيح. في الوقت الذي تم فيه تبني C ++ 98 ، لم تكن أهمية هذا الخط من الاستدلال موضع تقدير كامل ، لذا في C ++ 98 ، لم يكن لوجود مستخدم معلن مدمر أي تأثير على رغبة المجمعين في توليد عمليات النسخ. يستمر هذا الوضع في C ++ 11 ، ولكن فقط لأن تقييد الشروط التي يتم بموجبها إنشاء عمليات النسخ يؤدي إلى كسر الكثير من التعليمات البرمجية القديمة.

كيف يمكنني منع نسخ الكائنات الخاصة بي؟

إعلان منشئ نسخة ومشغل تخصيص نسخة كمعيّن وصول خاص.

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

في C ++ 11 فصاعدًا ، يمكنك أيضًا الإعلان عن حذف مُنشئ نسخة ومهمة

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

المقدمة

يعالج C ++ متغيرات أنواع المعرفة بواسطة دلالات القيمة . وهذا يعني أن الكائنات يتم نسخها ضمنيًا في سياقات مختلفة ، ويجب أن نفهم ما تعنيه "نسخ كائن ما" في الواقع.

دعونا ننظر في مثال بسيط:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(إذا كنت في حيرة من name(name), age(age) الجزء name(name), age(age) ، يسمى هذا قائمة مُبدئ عضو .)

وظائف العضو الخاص

ماذا يعني نسخ كائن person ؟ تُظهر الدالة main سيناريوهين نسخ متمايزين. person b(a); التهيئة person b(a); يتم تنفيذه بواسطة منشئ النسخة . مهمتها هي بناء كائن جديد يعتمد على حالة كائن موجود. يتم تنفيذ التخصيص b = a بواسطة مشغل تخصيص النسخة . وظيفتها بشكل عام أكثر تعقيدًا بعض الشيء ، لأن الكائن المستهدف بالفعل في حالة صالحة يجب التعامل معها.

بما أننا لم نعلن عن مُنشئ النسخ ولا عامل التعيين (ولا المدمر) أنفسنا ، فقد تم تعريفها ضمنيًا بالنسبة لنا. اقتبس من المعيار:

[...] منشئ نسخة ومشغّل تخصيص النّسخة ، [...] و destructor هما عضوان خاصان. [ ملاحظة : سيعلن التنفيذ ضمنيًا وظائف هذه العضو لبعض أنواع الفئات عندما لا يعلن البرنامج صراحةً عن هذه الأنواع. سيحدد التنفيذ ضمنيًا إذا تم استخدامها. [...] ملاحظة نهاية [n3126.pdf section 12 §1]

بشكل افتراضي ، فإن نسخ كائن يعني نسخ أعضائها:

ينفّذ مُنشئ النسخة المعرّف ضمنيًا لفئة غير الإتحادية X نسخة مؤقّتة من عناصره الفرعية. [n3126.pdf section 12.8 §16]

ينفّذ عامل تخصيص النسخة المحدد ضمنيًا لفئة غير الإتحادية X تعيين نسخة ذات عضوية من عناصره الفرعية. [n3126.pdf section 12.8 §30]

تعريفات ضمنية

وظائف العضو الخاص المعرّفة ضمنيًا person تبدو كالتالي:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

إن نسخ الأعضاء هو بالضبط ما نريده في هذه الحالة: يتم نسخ name age ، حتى نحصل على كائن مستقل مستقل بذاته. دائمًا ما يكون destructor المعرّف ضمنيًا فارغًا. هذا أيضا جيد في هذه الحالة لأننا لم نحصل على أي موارد في المنشئ. يتم استدعاء ضمائر الأعضاء ضمنيًا بعد إنهاء person المدمر:

بعد تنفيذ جسم المدمر وتدمير أي أجسام تلقائية يتم تخصيصها داخل الجسم ، يقوم أحد المدمرين للفئة X بالاتصال بالدمار للأعضاء [...] المباشرة لـ [n3126.pdf 12.4 §6]

إدارة الموارد

إذاً متى يجب أن نعلن عن وظائف الأعضاء الخاصة هذه بوضوح؟ عندما يدير فصلنا موردًا ، أي عندما يكون عنصر من الطبقة مسؤولًا عن هذا المورد. يعني ذلك عادة أن يتم الحصول على المورد في المنشئ (أو تمريره إلى المُنشئ) وإطلاقه في destructor.

دعونا نعود في الوقت المناسب ل C ++ القياسية. لم يكن هناك شيء مثل std::string ، وكان المبرمجون في الحب مع المؤشرات. قد يبدو صف person كما يلي:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

حتى اليوم ، لا يزال الناس يكتبون فصولاً في هذا الأسلوب ويخوضون المشاكل: " دفعت شخصًا إلى متجه وأصبحت الآن أواجه أخطاءً جنونية في الذاكرة! " تذكر أنه افتراضيًا ، يعني نسخ كائن ما نسخ أعضائه ، ولكن نسخ عضو name مجرد نسخ مؤشر ، وليس صفيف الحرف الذي يشير إليه! هذا له العديد من الآثار غير السارة:

  1. يمكن ملاحظة التغييرات عبر a b .
  2. بمجرد أن يتم إتلاف b ، فإن a.name مؤشر متدلي.
  3. إذا تم إتلاف ، فحذف المؤشر المتدلي يؤدي إلى سلوك غير معروف .
  4. نظرًا لأن المهمة لا تأخذ بعين الاعتبار name المشار إليه قبل المهمة ، عاجلاً أم آجلاً ، ستحصل على تسرب للذاكرة في كل مكان.

تعريفات واضحة

نظرًا لأن النسخ على مستوى الأعضاء ليس له التأثير المطلوب ، فيجب علينا تحديد مُنشئ النسخ ومُشغل تخصيص النسخة بشكل واضح لعمل نسخ عميقة من مصفوفة الأحرف:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

لاحظ الفرق بين التهيئة والالتزام: يجب علينا هدم الحالة القديمة قبل التعيين name لمنع تسرب الذاكرة. أيضا ، لدينا لحماية ضد التعيين الذاتي للنموذج x = x . بدون هذا الفحص ، delete[] name الصفيف الذي يحتوي على السلسلة المصدر ، لأنه عندما تكتب x = x ، يحتوي كلا من this->name و that.name على نفس المؤشر.

سلامة الاستثناء

لسوء الحظ ، سيفشل هذا الحل إذا كان new char[...] يطرح استثناء بسبب استنفاد الذاكرة. أحد الحلول الممكنة هو إدخال متغير محلي وإعادة ترتيب العبارات:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

هذا أيضًا يعتني بالتخصيص الذاتي دون فحص صريح. الحل الأكثر قوة لهذه المشكلة هو لغة النسخ والتقليب ، لكنني لن أخوض في تفاصيل سلامة الاستثناء هنا. لقد أشرت فقط إلى استثناءات لجعل النقطة التالية: كتابة الطبقات التي تدير الموارد أمر صعب.

الموارد غير القابلة للنسخ

لا يمكن نسخ بعض الموارد أو لا يجب نسخها ، مثل مقابض الملفات أو كائنات المزامنة. في هذه الحالة ، ما عليك سوى أن تعلن أن مُنشئ النسخة ومشغّل تخصيص النُسخة هما عاملان خاصان دون إعطاء تعريف:

private:

    person(const person& that);
    person& operator=(const person& that);

بدلاً من ذلك ، يمكنك أن ترث من boost::noncopyable أو تعلن أنها محذوفة (C ++ 0x):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

حكم الثلاثة

تحتاج أحيانًا إلى تطبيق فصل يدير موردًا. (لا تدير أبدًا موارد متعددة في فصل واحد ، فلن يؤدي ذلك إلا إلى الشعور بالألم). في هذه الحالة ، تذكر قاعدة الثلاثة :

إذا كنت بحاجة إلى الإعلان صراحة عن المُدمِر أو مُنشئ النسخ أو مُعيِّن تعيين النسخة بنفسك ، فربما تحتاج إلى الإعلان صراحة عن كل ثلاثة منهم.

(لسوء الحظ ، لا يتم تطبيق هذه "القاعدة" من قبل معيار C ++ أو أي مترجم أعلم به.)

النصيحة

في معظم الأوقات ، لا تحتاج إلى إدارة مورد بنفسك ، لأن هناك بالفعل طبقة مثل std::string بالفعل. ما عليك سوى مقارنة الرمز البسيط باستخدام عضو من std::string إلى العنوان المعقد والمعروف بالخطأ باستخدام char* ويجب أن تكون مقتنعاً. وطالما بقيت بعيدًا عن أعضاء المؤشر الخام ، فمن غير المحتمل أن تهتم قاعدة الثلاثة برمزك الخاص.


العديد من الإجابات الموجودة تلمس بالفعل منشئ نسخة ، مشغل التعيين و destructor. ومع ذلك ، في مرحلة ما بعد C ++ 11 ، قد يؤدي تطبيق التحريك الدلالية إلى توسيع هذا الرقم إلى ما بعد 3.

قدم مؤخرا مايكل كلايس محاضرة تلمس هذا الموضوع: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


قاعدة ثلاثة في C ++ هي مبدأ أساسي في تصميم وتطوير ثلاثة متطلبات أنه إذا كان هناك تعريف واضح في إحدى وظائف الأعضاء التالية ، فيجب على المبرمج أن يحدد وظيفتي الأعضاء الآخرين معًا. وهي وظائف العضو الثلاثة التالية لا غنى عنها: destructor ، منشئ نسخة ، مشغل مهمة نسخ.

نسخ منشئ في C ++ هو منشئ خاص. يتم استخدامه لإنشاء كائن جديد ، وهو كائن جديد مكافئ لنسخة من كائن موجود.

عامل تشغيل نسخ المهمة هو عامل تخصيص خاص يستخدم عادةً لتحديد كائن موجود للآخرين من نفس نوع الكائن.

هناك أمثلة سريعة:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

ماذا يعني تقليد كائن ما؟ هناك بعض الطرق التي يمكنك من خلالها نسخ الأشياء - دعنا نتحدث عن نوعين من المرجح أن تشير إليهما - نسخة عميقة ونسخة ضحلة.

بما أننا في لغة موجهة للكائنات (أو على الأقل نفترض ذلك) ، لنفترض أن لديك جزءًا من الذاكرة المخصصة. نظرًا لأنها لغة OO ، يمكننا بسهولة الرجوع إلى أجزاء الذاكرة التي نخصصها نظرًا لأنها في الغالب متغيرات بدائية (ints أو chars أو bytes) أو فئات حددناها مصنوعة من أنواعنا وأولوياتنا. لنفترض أن لدينا فئة من السيارات على النحو التالي:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

والنسخة العميقة هي إذا أعلننا عن كائن ثم أنشأنا نسخة منفصلة تمامًا من الكائن ... فنحن في نهاية الأمر نحصل على كائنين في مجموعتين من الذاكرة تمامًا.

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

الآن دعونا نفعل شيئا غريبا. دعنا نقول أن car2 إما مبرمجة خاطئة أو المقصود عمدا لمشاركة الذاكرة الفعلية التي يتم car1 من. (عادة ما يكون من الخطأ القيام بذلك ، وفي الفصول الدراسية عادة ما تكون هي الباقة التي يتم مناقشتها في إطار.) تدعي أنه في أي وقت تسأل فيه عن car2 ، فأنت بالفعل تحلّل مؤشرًا إلى مساحة ذاكرة car1 ... وهذا أكثر أو أقل ما هي نسخة ضحلة هو.

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

لذا بغض النظر عن اللغة التي تكتب بها ، كن حذرًا جدًا بشأن ما تعنيه عندما يتعلق الأمر بنسخ الكائنات نظرًا لأن معظم الوقت تريد نسخة عميقة.

ما هو مُنشئ النسخ ومُشغل تخصيص النسخة؟ لقد استخدمتها بالفعل أعلاه. يتم استدعاء مُنشئ النسخة عند كتابة تعليمة برمجية مثل Car car2 = car1; أساسا إذا قمت بتعريف متغير وتعيينه في سطر واحد ، وهذا هو عندما يتم استدعاء المنشئ نسخة. مشغّل المهمة هو ما يحدث عندما تستخدم علامة مساوية - car2 = car1; . لا يتم الإعلان عن car2 في نفس العبارة. من المحتمل أن تكون قطعتا الرمز التي تكتبها لهذه العمليات متشابهة للغاية. في الواقع ، يحتوي نمط التصميم النموذجي على وظيفة أخرى تتصل بها لتعيين كل شيء بمجرد اقتناعك بأن النسخة الأولية / المهمة مشروعة - إذا نظرت إلى الشفرة المميتة التي كتبتها ، فإن الوظائف متطابقة تقريبًا.

متى أحتاج إلى الإعلان عن نفسي؟ إذا كنت لا تقوم بكتابة الكود المطلوب مشاركته أو لإنتاجه بطريقة ما ، فإنك تحتاج فقط إلى الإعلان عنه عندما تحتاج إليه. يجب أن تكون على دراية بما تقوم به لغة برنامجك إذا اخترت استخدامه "عن طريق الصدفة" ولم تقم بإنشاء واحدة - أي أنك تحصل على برنامج التحويل البرمجي الافتراضي. نادراً ما أستخدم نسخ المنشئات على سبيل المثال ، لكن تجاوزات عامل التخصيص شائعة جدًا. هل تعلم أنه بإمكانك تجاوز ما الإضافة أو الطرح أو ما إلى ذلك؟

كيف يمكنني منع نسخ الكائنات الخاصة بي؟ تجاوز كل الطرق التي يسمح لك بتخصيصها للذاكرة الخاصة بك مع وظيفة خاصة هو بداية معقولة. إذا كنت لا تريد حقا أن يقوم الناس بنسخها ، يمكنك جعلها عامة وتنبيه المبرمج عن طريق رمي استثناء وكذلك عدم نسخ الكائن.


قاعدة الثلاثة هي قاعدة أساسية لـ C ++ ، تقول أساساً

إذا كان صفك يحتاج إلى أي من

  • منشئ نسخة ،
  • مشغل الاحالة ،
  • أو مدمر ،

تعريف explictly ، فمن المرجح أن تحتاج كل ثلاثة منهم .

تكمن الأسباب وراء ذلك في أن الثلاثة يستخدمون عادةً لإدارة مورد ، وإذا كانت صفك يدير موردًا ، فعادة ما يحتاج إلى إدارة النسخ بالإضافة إلى تحريره.

إذا لم يكن هناك دلالة جيدة لنسخ المصدر الذي تديره فصلك ، فاحرص على حظر النسخ عن طريق التصريح (وليس defining ) بأن مُنشئ النسخ ومشغل المهمة كخاص.

(لاحظ أن الإصدار الجديد القادم من معيار C ++ (وهو C ++ 11) يضيف دلالات نقل إلى C ++ ، والتي من المحتمل أن تغير قاعدة الثلاثة .ومع ذلك ، لا أعرف سوى القليل عن هذا الأمر لكتابة قسم C ++ 11 حول قاعدة الثلاثة.)





rule-of-three