في المقال السابق ( لماذا عليك أن تقلل من الأعتماد على Preprocessor ؟) تحدثنا عن ضروره التقليل من استخدام الـ Preprocessor وخاصه عند تعريف الثوابت ، وذكرنا البديل الأفضل وهو أستخدام const .. مقالنا الليله يدعوك الى استخدام الqualifier المسمى const في أي مكان واي زمان لأنك هكذا ستكون في الـ Safe Side وستتعامل بشكل أكثر صرامه مع عملاء طبقاتك Your Client وسوف يساعدك المترجم في هذه المهمه .
الثوابت يمكن ان تكون في أي جزء في برنامجك ، داخل Block Scope مثلا الداله main ، أو Global Scope ، أو مثلا في داله سواء في القيم المرسله لها Argument أو حتى القيمه الراجعه منها return value .. يمكن أن يكون الثابت داخل الكلاس أو حتى داخل الـ Namespace .. أخيرا يمكن أن يكون مع المؤشرات ، وهنا يوجد له ثلاث استخدامات : مؤشر الى ثابت ، مؤشر ثابت الى متغير ، مؤشر ثابت الى ثابت …
نقوم بعمل Refresh بسيطه للأساسيات:
char arr[] = "wajdy Essam" ; char *p = arr ; // This is pointer to char char * const cp = arr ; // This is const Pointer to char const char * p = arr ; // This is Pointer to const char char const * p = arr ; // This is Pointer to const char const char * const p = arr ; // This is const Pointer to const char
لكي لا تقع في متاهه التفكير في الأمثله أعلاه ، دائما أنظر الى علامه * ، اذا كان بعدها على اليمين const فهذا يعني أن المؤشر هو ثابت (لا يؤشر الى شيء أخر) ، أما اذا قبلها (في اليسار) const فهذا يعني أن ما يؤشر اليه pointed to هذا المؤشر ثابت لا تتغير قيمته .. أما اذا ظهرت في الجهتين فمعناه أن الأثنين ثابتين ..
بالنسبه الى مؤشر الى ثابت يمكن أن تكون كلمه const بعد أسم المؤشر (قبل علامه * ) ، مثل :
const char * p = arr ; // This is Pointer to const char char const * p = arr ; // This is Pointer to const char
لذلك دائما أنظر الى علامه * وهي أوضح وأسهل طريقه لكي تعرف الثبوتيه 🙂 ..
استخدام الثوابت يعني أن قيمتها لن تتغير أثناء تنفيذ برنامجك … والمترجم يضمن لك ذلك أثناء ترجمه برنامجك .. حيث أن أي تغيير في أي بت في ذلك الثابت سوف يصرخ المترجم في وجهك ويخرج لك رساله بأنك تحاول أن تغير في ثابت .. ومن هنا كان تعريف الثوابت في سي++ يعرف بـ Bitwise Constness أي أنه لا يمكن تغيير قيمه أي بت في المتغير .
من أشهر أستخدامات الثوابت هي في القيم المرسله والراجعه في الداله .. بالنسبه للقيم المرسله فمعناه أنه الداله لن تغير من قيمه هذا (المتغير أو الكائن ) .. أما بالنسبه الى القيمه الراجعه فمعناه أن القيمه الفلانيه الراجعه من الداله لا يمكن أن تتغير (أي لن تكون في الجهه اليمنى في جمله الأسناد) ، وطبعا استخدام const في القيمه الراجعه أمر مهم ويضمن لك سلامه أستخدام الـ Client للطبقه الخاصه بك ولن توقعهم في مشاكل ما .. لنذكر مثال بسيط يبين الفكره :
int a,b,c ; // here put any value in this varaible a+b = c ;
السطر الأخير خاطئ ، والسبب أن الـ Left value دائما يجب أن تكون موقع location وليس قيمه …
هذا الكلام بالنسبه الى المتغيرات العاديه Primitive Data Type .. أما بالنسبه للكائنات فلن يكون أي خطأ هناك ، لأنه ناتج جمع كائن مع كائن(بعد اعاده تعرف المعامل +) سوف يكون كائن أخر ،وعمليه الأسناد تتم بشكل عادي (استدعاء لـ = operator) ، مثال بسيط :
MyObject a,b,c ; // here put Any Value in Objects a + b = c ;
العمليه صحيحه ولا يوجد خطأ بالترجمه … ولكنه بالتأكيد خطأ منطقى قام به العميل لطبقتك .. لذلك عليك عند تصميم الطبقه Class أن تراعي مثل هذه الأخطاء بوضع const في القيمه الراجعه وبالتالي لن تكون هذه القيمه أبدا هي LValue ..
ربما قد تقول يستحيل أن يقوم مبرمج بكتابه سطر مثل هذا :
a + b = c ;
حسنا ما رأيك بهذا :
if ( a + b = c ) // any statement
لقد وقع المبرمج القدير بالفخ ، وخطأ مثل هذا قد يصعب تصحيحه .. لذلك لم لا تدع هذه المهمه للمترجم .. فقط أعلن عن القيمه الراجعه بconst وريح نفسك …
أحد الأشهر الأستخدامات لـ const أيضا هو في داخل الداول في الكلاس const member function ، هذا يعني أن تلك الداله لن تغير في قيمه أي متغير معرف داخل الكلاس (ماعدا static variable) … أيضا هذه الدوال const لا يمكن أن يتم استدعائها الا من خلال الكائنات الثابته const object ..
سؤال / ماالفائده من const Member Function بشكل عملي ؟
جواب / لأنها تجعل الـ Interface أكثر وضوحا .. أضافه الى أنها يتم استدعائها بواسطه الكائنات الثابته const object
سؤال / ما الحاجه للكائنات الثابته const object ؟
جواب / استخدام const مع الكائنات لا توجد منه فائده كبيره … لكن Reference to const هو أكثر أستخداما حيث كما نعلم أن التمرير بالمرجع أفضل من التمرير بالقيمه عند التعامل مع الكائنات حيث لن يتم استدعاء الـCopy Constructor ولا Destructor عند الأنتهاء من الداله ..
لذلك دائما تمرير الكائنات يفضل بالمرجع :
- اذا أدرت تغيير قيمته أرسله بالمرجع فقط By reference
- اذا لم ترد تغيير قيمته أرسله By reference to const .
وبما أنك تريد ارسال reference to const فلن تستطيع استدعاء أي داله الا أن تكون const ..
تخيل أن لديك داله display للطباعه داخل كلاس A . و لكي تستخدم هذه الداله يجب عليك عمل كائن وتقوم باستدعائها ..
في حال أحتجت لداله تأخذ كائن من A ثابت ، هنا لن تستطيع استدعاء display لأنها غير const .. الحل هو باعاده تعريف الداله display في الكلاس A وجعلها const ، هكذا يكون لدينا دالتين بنفس الأسم ، أحدهما عاديه والأخرى ثابته ..
شاهد المثال :
#include <iostream> using namespace std ; class A { private : int x ; public : A(int v=5) : x(v) { } void display () { cout << x << endl; } }; void calculator (const A& a) { // simple statement here a.display(); } int main (int argc ,char** argv) { A a ; calculator(a); }
الأن ترجم البرنامج :وستجد الخطأ بأنك لا يمكن أن تستدعي display من خلال الثابت a .. الحل كما ذكرت باعاده تعريف الداله display ، ليكون لدينا :
void display () { cout << x << endl; } void display () const { cout << x << endl; }
في حال قمنا في الداله main بعمل كائن من A واستدعينا display.. في حال كان الكائن ثابت سوف يستدعي display الثابته ، واذا كان عادي سوف يستدعي الداله العاديه .. جرب المثال التالي وشاهد المخرجات :
#include <iostream> using namespace std ; class A { private : int x ; public : A(int v=5) : x(v) { } void display () { cout << "in Non-Const Function : " << x << endl; } void display () const { cout << "in Const Function : " << x << endl; } }; void calculator (const A& a) { // simple statement here a.display(); } int main (int argc ,char** argv) { A a ; calculator(a); A b(3); b.display(); const A c(4); c.display(); }
بعض الأحيان يكون الكائن ثابت ، والداله ثابته .. ولكن يمكنك تغيير القيمه ولن يشتكي المترجم .. والسبب لأنه لا يفكر ولا يعرف ماذا تفعل ، فقط هو يراعي الـ syntax ولا يفكر .. ففي حال أرجعت قيمه من داخل الكلاس بالمرجع ، يمكنك تغيير تلك القيمه التي داخل الكلاس عن طريق مؤشر .. وهو مايعرف بـ logical constness .. أنظر المثال التالي :
#include <iostream> using namespace std ; class Text { private : char* ch ; public : Text (char* c) { ch = new char[strlen(c)+1] ; strcpy(ch,c); } void display () const { cout << ch << endl; } char& operator[] (int position) const { return ch[position]; } }; int main (int argc , char** argv) { const Text t("My Name is Wajdy"); char* ptr = &t[0] ; *ptr = 'X' ; t.display(); return 0; }
لاحظ أننا أرجعنا Reference من المصفوفه ، وقمنا بتغيير هذه القيمه بواسطه مؤشر …
في بعض الحالات الشاذه ، قد تود أن تغير من قيمه متغير داخل الداله الثابته في الكلاس ، فقم باعلان المتغير بـ mutable وبالتالي لن يشتكي المترجم ان قمت بتغيير قيمه المتغير داخل تلك الداله ..
mutable int x ;
Avoiding Duplication in const and Non-const Member Functions
الى هنا ، عرفنا أنه يجب ان نقوم بعمل overload للداله لكي تتعامل مع الconst وnon-const .. لكن المشكله قد تكون في حال كانت الداله كبيره ، سوف يكون هناك تكرار كثير في الكود :
لاحظ :
const char& operator[](std::size_t position) const { ... // do bounds checking ... // log access data ... // verify data integrity return text[position]; } char& operator[](std::size_t position) { ... // do bounds checking ... // log access data ... // verify data integrity return text[position]; }
ما الحل في مشكله تكرار الكود Duplication هنا ؟
أسهل حل بوضع تلك الأجزاء التي تتكرر في داله (private) ونقوم باستدعاء هذه الداله داخل الدالتين ..
حل أخر وهو جعل الداله العاديه تستدعي الداله الثابته ، وذلك بتحويل الكائن الحالى الى ثابت ، واستدعاء الداله الثابته وعندما ترجع القيمه الثابته نقوم بازاحه الثابت cast const away .. كيف يمكن عمل ذلك ؟
const char& operator[](std::size_t position) const { ... // do bounds checking ... // log access data ... // verify data integrity return text[position]; } char& operator[](std::size_t position) { return const_cast<char&>( static_cast<const TextBlock&>(*this) [position] ); }
السطر الأخير وقد يبدوا غريب بعض الشيء .. نقوم بشرحه :
const_cast<char&>( تعني أن مابين القوسين سوف نزيح منه const ) ، اي حذف الـ const
static_cast<const TextBlock&>(*this) هنا اضافه const الى الكائن الحالي *this
[position] هنا سوف يتم استدعاء الداله ذات const
هذا هو الحل الأخر ، وبشكل عام عمليات Casting هي حلول سيئه في سي++ وعلينا أن نتجنبها بأي طريقه .. لكن بالمقابل تكرار الكود code duplication أمر لا يمكن السكوت عنه نهائيا .. لذلك اما أن تستخدم داله أخرى ، أو تستخدم الCasting ..
أخيرا ، أستخدام const أمر جميل حقا ، فهو يضمن لك بأن المترجم سوف ينبهك بأي عمليه خاطئه لا تود أن يقع فيها عميل طبقتك … لذلك حاول أن تكثر من أستخدامها في اي مكان .. سوف تكون حقا مسرور بذلك .
المصادر :
Effective c++
Thinking in C++