اذهب إلى المحتوى

مفهوم الصحة الثباتية (Const Correctness) في كتابة شيفرات Cpp


محمد بغات

الصحة الثباتية (Const Correctness) أسلوب لتصميم الشيفرات، يعتمد على فكرة أنّه لا ينبغي أن تُتاح إمكانية تعديل نسخة معيّنة -على سبيل المثال الحق في الكتابة- إلّا للشيفرَات التي تحتاج إلى تعديل تلك النسخة، وبالمقابل، فإنّ أيّ شيفرة لا تحتاج إلى تعديل نُسخة ما لن تملك القدرة على تعديلها، أي سيكون لها حق القراءة فقط.

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

يتم ذلك عن طريق إعطاء التوابعِ مؤهلات (const CV-qualifiers)، وعن طريق وسم معاملات المؤشّرات / المراجع بالكلمة المفتاحية ‎const‎.

class ConstCorrectClass {
    int x;
    public:
        int getX() const {
            return x;
        } // الدالة ثابتة: أي أنها لن تعدل النسخة
    void setX(int i) {
        x = i;
    } // غير ثابتة: أي أنها ستعدل النسخة
};
// المعامل ثابت، أي أنّه لن يُعدّل
int const_correct_reader(const ConstCorrectClass& c) {
    return c.getX();
}
// المعامل غير ثابت: سيُعدَّل
void const_correct_writer(ConstCorrectClass& c) {
    c.setX(42);
}
const ConstCorrectClass invariant; // النسخة ثابتة: لن تُعدّل
ConstCorrectClass variant; // النسخة غير ثابتة: يمكن أن تُعدَّل
// …

// صحيح: استدعاء دالة ثابتة غير مُعدِّلة على نسخة ثابتة
const_correct_reader(invariant); 

// صحيح: استدعاء دالة ثابتة غير مُعدِّلة على نسخة قابة للتعديل
const_correct_reader(variant); 

// صحيح: استدعاء دالة ثابتة مُعدِّلة على نسخة قابلة للتعديل
const_correct_writer(variant); 

// خطأ: استدعاء دالة ثابتة مُعدِّلة على نسخة ثابتة
const_correct_writer(invariant); 

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

الصحة الثباتية في تصميم الأصناف

جميع الدوال التابعة في صنف صحيح ثباتيًا (const-correct class)، التي لا تُغيّر الحالة المنطقية، يكون مؤشّر ‎this‎ الخاص بها ثابتًا، للدلالة على أنّها لن تُعدّل الكائن، لكن هذا لا يمنع أن تكون هناك حقول متغيرة - ‎mutable‎ - والتي يمكن تعديلها حتى لو كانت الكائنات التي تنتمي إليها ثابتة.

عندما تعيد دالةٌ تأهيلها ثابت (‎const‎) مرجعًا، فينبغي أن يكون ذلك المرجع ثابتًا. يتيح هذا لها إمكانية أن تُستدعى على كلٍّ من النُسخ الثابتة، وكذلك النسخ غير المؤهّلة (non-cv-qualified)، إذ ستكون ‎const T*‎ قادرة على الارتباط بـ ‎T*‎ أو ‎const T*‎. هذا يسمح بدوره للدوالّ بأن تصرّح عن المُعاملات المُمرّة بالمرجع (passed-by-reference) على أنّها ثابتة عندما لا تحتاج إلى تعديل، وذلك دون التأثير على وظيفتها.

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

أولاً، لننظر إلى مؤهِّلات للمؤشّر ‎this‎ : لنفرض أن لدينا صنف Field، له الدالة التابعة ;(void insert_value(int

class ConstIncorrect {
    Field fld;
    public:
        ConstIncorrect(Field& f); // يعدِّل
   Field& getField(); // قد يعدِّل، كما تُكشَف الأعضاء كمراجع غير ثابتة وذلك لإتاحة التعديل غير 
                          //  المباشر
    void setField(Field& f); // يعدِّل
    void doSomething(int i); // قد يعدِّل
    void doNothing(); //  قد يعدِّل
};
ConstIncorrect::ConstIncorrect(Field& f): fld(f) {} //  يعدِّل
Field& ConstIncorrect::getField() {
    return fld;
} // لا يعدِّل
void ConstIncorrect::setField(Field& f) {
    fld = f;
} // يعدِّل
void ConstIncorrect::doSomething(int i) { //  يعدِّل
    fld.insert_value(i);
}
void ConstIncorrect::doNothing() {} // لا يعدِّل
class ConstCorrectCVQ {
    Field fld;
    public:
        ConstCorrectCVQ(Field& f); //  يعدِّل
    const Field& getField() const; // لا يعدِّل، تُكشف الأعضاء كمراجع ثابتة لمنع التعديل غير المباشر
    void setField(Field& f); //  يعدِّل
    void doSomething(int i); //  يعدِّل
    void doNothing() const; // لا يعدِّل
};
ConstCorrectCVQ::ConstCorrectCVQ(Field& f): fld(f) {}
Field& ConstCorrectCVQ::getField() const {
    return fld;
}
void ConstCorrectCVQ::setField(Field& f) {
    fld = f;
}
void ConstCorrectCVQ::doSomething(int i) {
    fld.insert_value(i);
}
void ConstCorrectCVQ::doNothing() const {}

هذا لن يعمل إذا لا يمكن استدعاء الدوال التابعة على نُسخ ConstIncorrect، نتابع …

void const_correct_func(const ConstIncorrect& c) {
    Field f = c.getField();
    c.do_nothing();
}

أما هذا فيعمل إذ يمكن استدعاء ()doNothing و ()getField على نسخ ConstCorrectCVQ

void const_correct_func(const ConstCorrectCVQ& c) {
    Field f = c.getField();
    c.do_nothing();
}

يمكننا بعد ذلك دمج هذا مع معاملات الدوال الصحيحة ثباتيا (‎Const Correct Function Parameters‎)، وسيجعل هذا الصنفَ صحيحًا ثباتيًا بشكل كامل.

class ConstCorrect {
    Field fld;
    public:
      ConstCorrect(const Field& f); // تعدّل النسخة، ولكن لا تعدّل المعامل
      const Field& getField() const; // لا تعدِّل، وتكشف العضو كمرجع ثابت لمنع التعديل غير المباشر
      void setField(const Field& f); // تعدّل النسخة، ولكن لا تعدّل المعامل
      void doSomething(int i); // passed by value تعدّل، لكن لا تعدّل المعامل - ممرر بالقيمة
      void doNothing() const; // لا تعدّل 
};
ConstCorrect::ConstCorrect(const Field& f) : fld(f) {}
Field& ConstCorrect::getField() const { return fld; }
void ConstCorrect::setField(const Field& f) { fld = f; }
void ConstCorrect::doSomething(int i) {
    fld.insert_value(i);
}
void ConstCorrect::doNothing() const {}

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

class ConstCorrectContainer {
    int arr[5];
    public:
      // معامل تسجيل يوفر إمكانية القراءة إن كانت النسخة ثابتة، أو إمكانية القراءة/الكتابة خلاف ذلك
      int& operator[](size_t index) { return arr[index]; }
    const int& operator[](size_t index) const { return arr[index]; }
// ...
};

يُستخدم هذا كثيرًا في المكتبة القياسية، إذ توفّر معظم الحاويات تحميلات زائدة لأخذ الثباتية في الحسبان.

معاملات الدوال الصحيحة ثباتيًا

إذا كانت دالّة ما صحيحة ثباتيًا، فإنّ جميع المُعاملات المُمرَّرة إليها بالمرجع ستكون ثابتة، ما لم تعدّلها الدالّة بشكل مباشر أو غير مباشر. يفيد هذا في منع المبرمج من إجراء تعديل غير مقصود، ويسمح للدالّة بأن تقبل النُسخ الثابتة، وكذلك النسخ غير المؤهّلة، وتجعل مؤشّر النسخة ‎this‎ من النوع ‎const T*‎ عندما تُستدعى دالة تابعة ما، حيث ‎T‎ يمثّل هنا نوع الصنف.

struct Example {
    void func() { std::cout << 3 << std::endl; }
    void func() const { std::cout << 5 << std::endl; }
};
void const_incorrect_function(Example& one, Example* two) {
    one.func();
    two->func();
}
void const_correct_function(const Example& one, const Example* two) {
    one.func();
    two->func();
}
int main() {
    Example a, b;
    const_incorrect_function(a, &b);
    const_correct_function(a, &b);
}

هذا هو خرج البرنامج

3
3
5
5

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

لاحظ أنّ الدوالّ غير الصحيحة ثباتيًا تتسبب في حدوث أخطاء تصريفية في حالة تمرير نُسخة ثابتة إليها.

// قراءة القيمة من المتجهة ثم حساب وإعادة قيمة
// إمساك القيمة المعادة لأجل تسريع البرنامج
template<typename T>
const T& bad_func(std::vector<T>& v, Helper<T>& h) {
    // إمساك القيم لإستخدامها مستقبلا
    // بعد حساب القيمة المعادة، تُخزّن ويُسجل فهرسها
    static std::vector<T> vals = {};
    int v_ind = h.get_index();            // v الفهرس الحالي في

سيكون السطر التالي مساويًا لـ 1- إن كان فهرس المخزن المؤقت غير مسجلًا …

  int vals_ind = h.get_cache_index(v_ind);        
  if (vals.size() && (vals_ind != -1) && (vals_ind < vals.size()) && !(h.needs_recalc())) {
        return vals[h.get_cache_index(v_ind)];
}
    T temp = v[v_ind];
    temp -= h.poll_device();
    temp *= h.obtain_random();
    temp += h.do_tedious_calculation(temp, v[h.get_last_handled_index()]);

    if (vals_ind != -1) {
        vals[vals_ind] = temp;
    } else {
        v.push_back(temp); // لا ينبغي الدخول إلى القيم
        vals_ind = vals.size() - 1;
        h.register_index(v_ind, vals_ind);
    }
    return vals[vals_ind];
}

// النسخة الصحيحة ثباتيا تماثل النسخة أعلاه، لذا سنتجاوز معظمها
template<typename T>
const T& good_func(const std::vector<T>& v, Helper<T>& h) {
// ...
    if (vals_ind != -1) {
    vals[vals_ind] = temp;
    } else {
        v.push_back(temp);        // Error: discards qualifiers.
        vals_ind = vals.size() - 1;
        h.register_index(v_ind, vals_ind);
    }
    return vals[vals_ind];
}

الصحة الثباتية كأداة للتوثيق

من فوائد مفهوم الصحة الثباتية (const correctness) هي أنّه يمكن استخدامها لتوثيق الشيفرات، عبر توفير ضمانات وتوجيهات للمبرمجين والمستخدمين الآخرين. هذه الضمانات يفرضها المُصرِّف بسبب الثباتية، مع غيابٍ للثباتية يوحي أن الشيفرة لا توفرها.

التوابع المؤهّلة ثباتيًا

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

يمكن استخدام هذا لأجل بناء افتراضات حول حالة الكائن بعد استدعاء دالة تابعة معيّنة عليه دون الحاجة إلى رؤية تعريف تلك الدالّة:

// ConstMemberFunctions.h
class ConstMemberFunctions {
    int val;
    mutable int cache;
    mutable bool state_changed;
public:
    // يعدّل المنشئ الحالة المنطقية، لذا لا ضرورة لأيّ افتراضات
    ConstMemberFunctions(int v = 0);

يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()squared_calc

    int calc() const;

يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()calc

    int squared_calc() const;

يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()calc أو ()squared_calc

    void bad_func() const;

يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()squared_calc أو ()calc

    void set_val(int v);
};

وفقًا لقواعد الصحة الثباتية، ستُفرض هذه الافتراضات في الواقع بواسطة المُصرِّف.

// ConstMemberFunctions.cpp
ConstMemberFunctions::ConstMemberFunctions(int v /* = 0*/)
: cache(0), val(v), state_changed(true) {}

// لقد كان افتراضنا صحيحا
int ConstMemberFunctions::calc() const {
    if (state_changed) {
        cache = 3 * val;
        state_changed = false;
}
        return cache;
}

// لقد كان افتراضنا صحيحا
int ConstMemberFunctions::squared_calc() const {
    return calc() * calc();
}

// لقد كان افتراضنا غير صحيح
// للمؤهّلات `this` فشل تصريف الدالة بسبب فقدان
void ConstMemberFunctions::bad_func() const {
    set_val(863);
}

// لقد كان افتراضنا صحيحا
void ConstMemberFunctions::set_val(int v) {
    if (v != val) {
        val = v;
        state_changed = true;
    }
}

مُعاملات الدالة الثابتة

  • يمكن أن نفترض أنّ الدوالّ التي تقبل مُعاملًا واحدًا ثابتًا أو أكثر تهدف إلى قراءة تلك المُعاملات، و:
  • أنّها لن تعدّل تلك المُعاملات، أو تستدعي عليها تابعًا يمكن أن يُعدّلها.
  • لن تمرّر تلك المُعاملات إلى أيّ دالّة أخرى من شأنها تعديلها و/أو استدعاء أي توابع أخرى من شأنها تعديلها.
  • بالمقابل، يمكن افتراض أنّ أي دالّة تقبل مُعاملًا واحدًا غير ثابت أو أكثر ستسعى لتعديل تلك المُعاملات، و:
  • قد تعدِّل أو لا تعدّل تلك المُعاملات، أو قد تستدعي عليها توابع يمكن أن تعدّلها.
  • قد تمرّر أو قد لا تمرّر تلك المُعاملات إلى دوال أخرى يمكن أن تعدّلها و / أو تستدعي عليها توابع قد تعدّلها.

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

فيما يلي، يمكن أن نفترض أن c لم تُعدَّل وأن ()c.set_val لم تُستدعى ولم تمرَّر إلى ()non_qualified_function_parameter، وإذا مُرِّرَت إلى ()one_const_one_not فستكون أول المعامِلات …

// function_parameter.h
void const_function_parameter(const ConstMemberFunctions& c);

يمكن أن نفترض أن c عُدِّلَت و/أو أن ()c.set_val استدعيَت، وقد تكون مرِّرَت إلى أي من تلك الدوال، وإن مُرِّرَت إلى ()one_const_one_not فقد تكون أيًا من المعامِلات …

void non_qualified_function_parameter(ConstMemberFunctions& c);

نستطيع افتراض أن: l لم تُعدَّل، وأن ()l.set_val لن تُستدعى. l قد تُمرَّر أو لا إلى ()const_function_parameter. تم تعديل r و/أو قد تستدعى ()r.set_val. قد تُمرَّر r أو لا إلى أي من الدوال السابقة. نتابع …

void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r);

يمكن أن نفترض أن c لم تُعدَّل وأن ()c.set_val لم تُستدعى، وأنما لم تُمرَّر إلى ()non_qualified_function_parameter، وإن مُرِّرَت إلى ()one_const_one_not فقد تكون أيًا من المعامِلات …

void bad_parameter(const ConstMemberFunctions& c);

وفقًا لقواعد الصحة الثباتية، ستُفرض هذه الافتراضات من قِبل المُصرِّف.

// function_parameter.cpp
// افتراضنا كان صحيحا
    void const_function_parameter(const ConstMemberFunctions& c) {
        std::cout << "With the current value, the output is: " << c.calc() << '\n'
        << "If squared, it's: " << c.squared_calc()
        << std::endl;
}

// افتراضنا كان صحيحا
void non_qualified_function_parameter(ConstMemberFunctions& c) {
    c.set_val(42);
    std::cout << "For the value 42, the output is: " << c.calc() << '\n'
        << "If squared, it's: " << c.squared_calc()
        << std::endl;
}

// افتراضنا كان صحيحا
// لاحظ أنّ الصحة الثباتية لا  تحصِّن التغليف من أن يُكسر، وإنما تمنع الحق في الكتابة
// إلا عند الحاجة إليها
void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r) {
    struct Machiavelli {
        int val;
        int unimportant;
        bool state_changed;
    };
    reinterpret_cast<Machiavelli&>(r).val = l.calc();
    reinterpret_cast<Machiavelli&>(r).state_changed = true;
    const_function_parameter(l);
    const_function_parameter(r);
}

افتراضنا فيما يلي كان خطأ، وتفشل الدالة في التصريف لأن this فقد مؤهلاتٍ في ()c.set_val، …

void bad_parameter(const ConstMemberFunctions& c) {
    c.set_val(18);
}

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

class DealBreaker : public ConstMemberFunctions {
    public:
        DealBreaker(int v = 0);
        // اسم غير مسموح به، لكنه ثابت
        void no_guarantees() const;
}
DealBreaker::DealBreaker(int v /* = 0 */) : ConstMemberFunctions(v) {}

// افتراضنا كان خاطئا
// الصحة الثباتية، وتجعل المصرف يعتقد أننا على علم بتبِعات ما نفعل const_cast تحذف
void DealBreaker::no_guarantees() const {
    const_cast<DealBreaker*>(this)->set_val(823);
}
// ...
const DealBreaker d(50);
d.no_guarantees(); // ثابتة، وقد تُعدّل d سلوك غير معرَّف، إذ أنّ

على أي حال، ولأن هذا يتطلب من المبرمج أن يخبر المصرِّف أنه ينوي تجاهل الثباتية (Constness)، ولعدم التماثل (Consistency) بين المصرِّفات، فمن الآمن افتراض أن الشيفرة الصحيحة ثباتيًا لن تتجاهل الصحة الثباتية أو التماثل بين المصرِّفات إلا إن حُدِّد خلاف ذلك.

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 128: Const Correctness من كتاب C++ Notes for Professionals


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...