الضرر الكلي لرمز C ++

حدد

توفر لغة C ++ إمكانيات هائلة للاستغناء عن وحدات الماكرو. لذلك دعونا نحاول استخدام وحدات الماكرو بأقل قدر ممكن!

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

BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT ) //{{AFX_MSG_MAP(efcDialog) ON_WM_CREATE() ON_WM_DESTROY() //}}AFX_MSG_MAP END_MESSAGE_MAP() 

هناك مثل وحدات الماكرو ، حسنا. لقد تم إنشاؤها بالفعل لتبسيط البرمجة.

أنا أتحدث عن وحدات الماكرو الأخرى التي يحاولون تجنب تنفيذ وظيفة كاملة أو محاولة تقليل حجم الوظيفة. النظر في العديد من الدوافع لتجنب مثل وحدات الماكرو.

المذكرة. تمت كتابة هذا النص كرسالة ضيف في مدونة Simplify C ++. قررت نشر النسخة الروسية من المقال هنا. في الواقع ، أنا أكتب هذه المذكرة من أجل تجنب سؤال من القراء غير المهتمين لماذا لم يتم وضع علامة على هذه المقالة بأنها "ترجمة" :). وهنا ، في الواقع ، منشور ضيف باللغة الإنجليزية: " Macro Evil in C ++ Code ".

أولاً: رمز الماكرو يجذب الأخطاء


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

لقد وصفت مرارًا مثل هذه الحالات في مقالاتي. على سبيل المثال ، استبدال دالة isspace بهذا الماكرو:

 #define isspace(c) ((c)==' ' || (c) == '\t') 

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

أو كيف تحب هذا الاختصار لكتابة الوظيفة std :: printf ؟

 #define sprintf std::printf 

أعتقد أن القارئ يخمن أنه ماكرو غير ناجح للغاية. تم العثور عليه ، بالمناسبة ، في مشروع StarEngine. اقرأ المزيد عن هذا هنا .

يمكن القول أن المبرمجين هم المسؤولون عن هذه الأخطاء ، وليس عن وحدات الماكرو. هذا هو الحال. بطبيعة الحال ، المبرمجون هم دائما المسؤولون عن الأخطاء :).

من المهم أن تتسبب وحدات الماكرو في حدوث أخطاء. اتضح أنه يجب استخدام وحدات الماكرو بدقة أكبر أم لا.

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

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

وحدات الماكرو مثل A2W إخفاء الشر. تبدو مثل الوظائف ، ولكن في الواقع ، لها آثار جانبية يصعب ملاحظتها.

لا يمكنني تجاوز محاولات مماثلة لتقليل الرمز باستخدام وحدات الماكرو:

 void initialize_sanitizer_builtins (void) { .... #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS) \ decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM, \ BUILT_IN_NORMAL, NAME, NULL_TREE); \ set_call_expr_flags (decl, ATTRS); \ set_builtin_decl (ENUM, decl, true); #include "sanitizer.def" if ((flag_sanitize & SANITIZE_OBJECT_SIZE) && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE)) DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size", BT_FN_SIZE_CONST_PTR_INT, ATTR_PURE_NOTHROW_LEAF_LIST) .... } 

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

ثانيًا: تصبح قراءة الكود أكثر تعقيدًا


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

وفقًا للأسطورة ، استثمرت شركة Apple في تطوير مشروع LLVM كبديل عن دول مجلس التعاون الخليجي نظرًا لتعقيد كود دول مجلس التعاون الخليجي بسبب وحدات الماكرو هذه. عندما أقرأ عنها ، لا أتذكر ، لذلك لن يكون هناك دليل.

ثالثًا: كتابة وحدات الماكرو أمر صعب


من السهل كتابة ماكرو سيء. التقيت بهم في كل مكان مع عواقب مماثلة. لكن كتابة ماكرو جيد وموثوق به غالبًا ما يكون أكثر صعوبة من كتابة وظيفة مماثلة.

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

 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]); 

بالطبع ، لمثل هذه الحالات ، تم اختراع الحلول البديلة منذ فترة طويلة ويمكن تنفيذ الماكرو بأمان:

 #define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; }) 

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

الرابعة: تصحيح الأخطاء معقد


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

خامسا: ايجابيات كاذبة للمحللين الساكنين


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

المشكلة في وحدات الماكرو هي أن المحللين ببساطة لا يمكنهم التمييز بين الشفرة الصعبة الصحيحة والكود الخاطئ. توضح المقالة حول التحقق من Chromium أحد وحدات الماكرو هذه.

ما يجب القيام به


دعونا لا نستخدم وحدات الماكرو في برامج C ++ ما لم يكن ذلك ضروريًا!

يوفر C ++ أدوات غنية مثل وظائف القالب ، والاستدلال التلقائي للكتابة (تلقائي ، Dectype) ، ووظائف constexpr.

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

قد يجادل البعض بأن هذا الرمز مع وظيفة أقل كفاءة. هذا هو مجرد "عذر".

يقوم المترجمون الآن بتضمين الكود ، حتى إذا لم تكتب الكلمة الأساسية المضمّنة .

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

سأشرح مع مثال. إليك خطأ ماكرو كلاسيكي اقترضته من رمز FreeBSD Kernel.

 #define ICB2400_VPOPT_WRITE_SIZE 20 #define ICB2400_VPINFO_PORT_OFF(chan) \ (ICB2400_VPINFO_OFF + \ sizeof (isp_icb_2400_vpinfo_t) + \ (chan * ICB2400_VPOPT_WRITE_SIZE)) // <= static void isp_fibre_init_2400(ispsoftc_t *isp) { .... if (ISP_CAP_VP0(isp)) off += ICB2400_VPINFO_PORT_OFF(chan); else off += ICB2400_VPINFO_PORT_OFF(chan - 1); // <= .... } 

يتم استخدام الوسيطة chan في ماكرو دون التفاف بين قوسين. نتيجة لذلك ، فإن التعبير ICB2400_VPOPT_WRITE_SIZE لا يضاعف التعبير (chan - 1) ، ولكن بضرب واحد فقط.

لن يظهر الخطأ إذا تمت كتابة دالة عادية بدلاً من ماكرو.

 size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

من المحتمل جدًا أن يؤدي برنامج التحويل البرمجي C و C ++ الحديث وظائف مضمّنة من تلقاء نفسه ، وسيكون الرمز فعالًا كما في حالة الماكرو.

في الوقت نفسه ، أصبحت الشفرة أكثر قابلية للقراءة ، وكذلك خالية من الأخطاء.

إذا كان من المعروف أن قيمة الإدخال ثابتة دائمًا ، فيمكنك إضافة constexpr والتأكد من حدوث جميع العمليات الحسابية في مرحلة الترجمة . تخيل أنه C ++ وأن تشان دائمًا ثابت. من المفيد أن تعلن عن الوظيفة ICB2400_VPINFO_PORT_OFF مثل هذا:

 constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

الربح!

آمل أن أتمكن من إقناعك. حظا سعيدا وعدد أقل من وحدات الماكرو في رمز!

Source: https://habr.com/ru/post/ar444612/


All Articles