كيف تبرمج في ++C كالمحترف - 35 نصيحة لا بد منها

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

بعض النصائح ملخصةٌ أيضاً من صفحات كتاب التفكير والإبداع في ++C:

.1 عندما تقوم بإنشاء صف ما اجعل أسماءك واضحة قدر الإمكان. يجب أن يكون هدفك جعل واجهة المبرمج الزبون بسيطة. حاول جعل أسماءك واضحة جداً بحيث تصبح التعليقات (comments) غير ضرورية. بعد ذلك استخدم التحميل الزائد للتوابع والمعاملات الافتراضية لإنشاء واجهة سهلة الاستخدام يمكن استيعابها بالحدس.

.2 تسمح المتحكمات بالوصول (access control) (منشئ الصف) بالتغيير قدر الإمكان في المستقبل دون إلحاق الضرر بالشيفرة الخاصة بالزبون والتي يتم داخلها استخدام الصف. في هذا الصدد حافظ على كل شيء خصوصي (private) قدر الإمكان واجعل فقط واجهة الصف عمومية (public) مستخدماً دائماً التوابع عوضاً عن المعطيات، اجعل المعطيات عمومية عندما تُجبر على ذلك فقط. إذا لم يكن مستخدمو الصف بحاجة للوصول إلى تابع ما فاجعله خصوصياً. إذا وجب على جزء من صفك أن يكون معرضاً للوراثة كمحمي (protected) فزود الزبون هنا بواجهة توابع عوضاً عن تعريض المعطيات الفعلية للخطر. بهذه الطريقة لن يكون لتغييرات التحقيق (implementation) إلا أثر بسيط على الصفوف المشتقة.

.3 اكتب شيفرة الاختبار أولاً (قبل كتابة الصف)، واحفظها مع الصف. قُم بأتمتة إجراء الاختبارات عن طريق ملف makefile أو أداة مشابهة. بهذه الطريقة يمكن التحقق آلياً من أي تعديلات عبر تنفيذ شيفرة الاختبار حيث يتم حالاً اكتشاف الأخطاء. بما أنك تعلم أنه لديك شبكة السلامة الخاصة ببيئة الاختبار التي تستخدمها ستكون أكثر جرأة على إجراء تغييرات بالغة الأهمية عندما تطرأ الحاجة لها. تذكر أن التحسينات الأعظم على اللغات تأتي من عمليات الاختبار المضمنة التي تزودها مثل: التحقق من الأنماط (type checking)، ومعالجة الاستثناءات (exception handling) وغيرها، ولكن هذه الميزات تقف عند حدودها. يجب عليك من أجل متابعة الطريق إلى إنشاء نظام متين تعبئة الاختبارات التي تتحقق من الميزات الخاصة بصفك أو برنامجك.

.4 اكتب شيفرة الاختبار أولاً (قبل كتابة الصف) لكي تتحقق من أن تصميم الصف كامل. إذا لم تتمكن من كتابة شيفرة الاختبار فلن تعلم الشكل الذي سيكون عليه الصف. بالإضافة إلى ذلك ستساعد عملية كتابة شيفرة الاختبار عادة على اكتشاف ميزات أو شروط إضافية تحتاجها في الصف – لا تظهر هذه الميزات أو الشروط دائماً خلال عملية التحليل والتصميم.

.5 اجعل صفوفك شديدة الصغر(atomic) قدر الإمكان، أي أعط كل صف غرضاً وحيداً واضحاً. إذا تطور تصميم صفوفك (أو نظامك) فأصبح معقداً كثيراً فقم بتجزئة الصفوف المعقدة إلى صفوف أبسط. الدليل الأكثر وضوحاً لذلك هو الحجم: فإذا كان صف ما كبيراً فمن المحتمل أنه يقوم بالكثير ويجب تجزئته.

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

.7 راقب تعليمات switch أو العبارات if-else المتسلسلة. يعد هذا بشكل نموذجي مؤشراً لشيفرة التحقق من الأنماط، وهذا يعني اختيار شيفرة يتم تنفيذها اعتماداً على نوع من معلومات النمط (ربما لا يكون النمط الدقيق واضحاً في البداية). يمكنك عادة استبدال هكذا نوع من الشيفرة بالوراثة وتعددية الأشكال، سيقوم استدعاء تابع متعدد الأشكال بالتحقق من الأنماط عوضاً عنك، وسيسمح لك بقابلية التمديد بشكل أكثر سهولة ووثوقية.

.8 انتبه للتغير (variance). ربما يكون لغرضين مختلفين دلالياً أفعال أو مهام متطابقة، وهناك تردد طبيعي في محاولة جعل أحدهما صفاً فرعياً (subclass) من الآخر فقط من أجل الاستفادة من الوراثة. يسمى هذا التنوع (Variance) ولكن ليس هناك عدالة حقيقية في أن تُجبر على وضع علاقة الأب-الابن عندما لا تكون هذه العلاقة موجودة، هناك حل أفضل وهو إنشاء صف أساسي عام يزود واجهة لكلا الصفين اللذان سيصبحان صفين مشتقين - يحتاج ذلك إلى مساحة إضافية قليلة، ولكن ما زال بإمكانك أن تستفيد من الوراثة ولربما تحصل على اكتشاف هام في التصميم.

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

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

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

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

.13 لا تستخدم الوراثة الخصوصية (private inheritance). بالرغم من أنها موجودة داخل اللغة وأنها تبدو من حين لآخر تملك بعض الوظيفية إلا أنها تؤدي عند جمعها مع تقنيات RTTI إلى العديد من حالات الغموض التي لا يمكن تجاهلها. قم بإنشاء غرض عضو خصوصي عوضاً عن استخدام الوراثة الخصوصية.

.14 إذا ارتبط صفان مع بعضهما بطريقة وظيفية ما (مثل الحاويات (containers) والمكررات (iterators)) فحاول جعل أحدهما صفاً عمومياً متشعباً داخل الآخر وصديقاً له، كما تفعل بالضبط مكتبة C++ المعيارية في المكررات داخل الحاويات (ترى أمثلة عن ذلك في الجزء الأخير من الفصل السادس عشر). لا يركز ذلك على الروابط بين الصفوف فقط، بل ويسمح بإعادة استخدام اسم الصف عبر تشعيبه (جعله صفاً متشعباً (nested)) داخل صف آخر. تقوم مكتبة C++ المعيارية بذلك عبر تعريف صف مكرر (iterator) متشعب داخل واجهة عامة. يوجد سبب آخر يدفعك إلى تشعيب صف وهو أنه جزء من التحقيق الخصوصي (private implementation). يعد التشعيب هنا مفيداً في تحقيق إخفاء التنفيذ (implementation hiding) عوضاً عن ارتباط الصفوف وفي منع تلوث فضاء الأسماء كما ذُكر سابقاً.

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

.16 بشكل طبيعي لا تدع المترجم ينشئ عوضاً عنك البواني أو الهوادم أو العملية "=". يجب على مصممي الصفوف أن يقولوا دائماً ما يجب أن يفعله الصف بالضبط وبذلك يتمكنوا من التحكم بشكل مطلق بالصف. إذا لم تنشئ باني نسخ أو عملية "=" فقم بالتصريح عنهم كخصوصيين (private). تذكر أنك إذا قمت بإنشاء أي باني فذلك يمنع إنشاء الباني الافتراضي.

.17 عندما تكتب باني نسخ لصف مشتق تذكر أن تستدعي باني نسخ الصف الأب بشكل صريح (أيضاً نسخ الأغراض الأعضاء) (انظر الفصل الرابع عشر). فإذا لم تفعل ذلك سيتم استدعاء الباني الافتراضي للصف الأساسي (أو الغرض العضو) ومن المحتمل أن لا يكون ذلك ما تريده. من أجل استدعاء باني نسخ الصف الأساسي مرر له الغرض المشتق الذي تنسخ منه:

Derived(const Derived& d) : Base(d) { // ...

.18 عندما تكتب عملية إسناد لصف مشتق تذكر أن تقوم بشكل صريح باستدعاء نسخة الصف الأساسي من عملية الإسناد (انظر الفصل الرابع عشر). فإذا لم تفعل ذلك لن يحدث شيء (ينطبق نفس الأمر على الأغراض الأعضاء). من أجل استدعاء عملية الإسناد التابعة للصف الأب استخدم اسم الصف الأب ومعامل تحديد مجال الرؤية.

Derived& operator=(const Derived& d) {

Base::operator=

.19 حافظ على مجالات الرؤية (scopes) أصغر ما يمكن بحيث يكون مجال رؤية ودورة حياة أغراضك أصغر ما يمكن. يقلص ذلك من فرصة استخدام غرض ما في السياق الخاطئ ويساعد على اكتشاف الأخطاء المختبئة (bug) والتي يعتبر من الصعب إيجادها. افترض مثلاً أنه لديك حاوية وقطعة من الشيفرة تتكرر على عناصرها، فإذا قمت بنسخ تلك الشيفرة لاستخدامها مع حاوية جديدة ربما ترى نفسك وبشكل خاطئ تستخدم حجم الحاوية القديمة كحد أعلى للحاوية الجديدة. على أي حال إذا أصبحت الحاوية القديمة خارج مجال الرؤية (out of scope) فسيُلتقط الخطأ في وقت الترجمة.

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

Class ostream;

هذه الطريقة تترك شيفرتك عرضة لخطر التغييرات في التمثيل (representation) (فمثلاً يمكن أن يكون ostream فعلياً عبارة عن typedef)، استخدم دائماً بدلاً من ذلك ملف الترويسة:

#include <iostream>

عند إنشاء صفوفك الخاصة وفي حال كون المكتبة كبيرة زود مستخدميك بشكل مختصر من ملف الترويسة مع توصيفات أنماط غير كاملة (أي تصريحات أسماء الصفوف) في الحالات التي يحتاجون بها لاستخدام المؤشرات فقط (يمكن لذلك أن يسرع عملية الترجمة).

.21 عند اختيار النمط المعاد من عملية محملة بشكل زائد، خذ بعين الاعتبار ما سيحدث إذا تم ربط التعابير مع بعضها بشكل متسلسل. قم بإعادة نسخة أو مرجع للقيمة (return *this) بحيث يمكن استخدامها في التعابير المتسلسلة (A=B=C). وعندما تعرف العملية "=" تذكر الحالة x=x.

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

.23 كن حذراً من المتحولات المؤقتة. عندما تقوم بتحسين الفعالية احذر من الإنشاء المؤقت خصوصاً مع التحميل الزائد للعمليات. إذا كانت البواني والهوادم معقدة فيمكن أن تكون تكلفة إنشاء وحذف المتحولات المؤقتة مرتفعة. عند إعادة قيمة من تابع حاول دائماً بناء الغرض "في مكانه" باستدعاء الباني في تعليمة الإعادة:

Return MyType(i,j);

بدلاً من:

MyType x(i,j);

Return x;

إن تعليمة الإرجاع السابقة (المسماة تحسين فعالية القيمة المعادة) تحذف استدعاء باني نسخ واستدعاء هادم.

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

.25 ارم الاستثناءات بالقيمة والتقطها بالمرجع. دع إدارة الذاكرة لآلية معالجة الاستثناءات. إذا رُميت مؤشرات إلى أغراض استثناءات تم إنشاؤها على الكومة، فيجب على الملتقط أن يعلم أنه عليه هدم الاستثناء، مما يعد ربطاً(coupling) سيئاً. إذا التقطت الاستثناءات بالقيمة فتسبب عمليات بناء وهدم إضافية، وأسوأ من ذلك ربما يتم تقطيع الأجزاء المشتقة من الغرض الاستثناء خلال عملية الترقية بالقيمة.

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

.27 لا تستخدم توابع الملف <cstdio> مثل التابع printf()، تعلم عوضاً عن ذلك استخدام قنوات الدخل و الخرج (iostreams) فهي آمنة وقابلة للتمديد من ناحية التنميط وأكثر قوة بشكل جلي. ستحصل على مكافآت استثمارك بشكل دوري. بشكل عام استخدم دائماً مكتبات لغة C++ بدلاً من مكتبات لغة C.

.28 لا تستخدم الشكل MyType a = b; لتعريف غرض ما، تعتبر هذه الميزة بالذات مصدراً كبيراً للإرباك لأنها تستدعي الباني بدلاً من العملية "=". من أجل الوضوح كُن محدداً دائماً واستخدم الشكل MyType a(b); بدلاً من ذلك. إن النتيجتين متطابقتان، ولكن لن يتم إرباك المبرمجين الآخرين.

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

.30 ابن نظامك باستخدام تصحيح الثوابت (const correctness). يسمح ذلك للمترجم باكتشاف الأخطاء التي كانت ستبقى مخفية وصعبة الإيجاد. تحتاج هذه الممارسة (practice) إلى بعض الالتزام، كما يجب استخدامها بشكل متناسق خلال صفوفك ولكنها تستحق ذلك.

.31 استخدم التحقق من الأخطاء الذي يزودك به المترجم. نَفِّذ جميع عمليات الترجمة مع تفعيل التحذيرات الكاملة، وعدل شيفرتك لحذف جميع التحذيرات. اكتب شيفرة تستغل أخطاء وتحذيرات زمن الترجمة بدلاً من تلك التي تسبب أخطاء في زمن التنفيذ (مثلاً لا تستخدم قوائم المعاملات المتغيرة والتي تلغي التحقق من الأنماط بأكمله). استخدم التابع assert() من أجل التنقيح، والاستثناءات من أجل أخطاء زمن التنفيذ.

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

.33 إذا كنت تستخدم توصيفات الاستثناء فقُم بتنصيب التابع unexpected() الخاص بك باستخدام التابع set_unexpected(). يجب على التابع unexpected() أن يسجل الخطأ ويعيد رمي الاستثناء الحالي. وبذلك إذا تم إبطال تابع موجود وبدأ يرمي استثناءات سيكون لديك سجل عن الجاني ويمكنك تعديل شيفرة الاستدعاء لمعالجة الاستثناء.

.34 انتبه للتحميل الزائد. لا يجب على تابع ما أن ينفذ شيفرة بشكل شرطي اعتماداً على قيمة معامل سواء أكان افتراضياً أم لا. في تلك الحالة يجب إنشاء تابعين محملين بشكل زائد بدلاً من ذلك.

.35 قُم بإخفاء مؤشراتك داخل صفوف حاويات. احضرهم خارجاً فقط عندما تقوم بتنفيذ عمليات عليهم. كانت المؤشرات دائماً مصدراً كبيراً للأخطاء. عندما تستخدم new حاول إسقاط المؤشر الناتج داخل حاوية. احرص على الحالة التي تمتلك فيها الحاوية مؤشراتها وبذلك فهي مسؤولة عن التحرير. أو غلف المؤشر داخل صف، أما إذا ما زلت تريد النظر إليه كمؤشر فقُم بالتحميل الزائد للعمليتين * و ->. إذا وجب عليك استخدام مؤشر حر فقم دائماً بتهيئه، ومن المفضل أن تكون التهيئة إلى عنوان غرض ما، ولكن في حالة الضرورة إلى الصفر. ضع قيمته إلى الصفر عندما تحذفه لمنع الحذف المتعدد غير المقصود.

فيما يلي كتب متميزة بلغات البرمجة: