الماكرو في C++: جمال خطير أم شر لا بد منه؟
\n\nكل من كتب بلغة C++ ولو مرة واحدة، واجه الماكرو. من ناحية، هي أداة قوية للبرمجة الوصفية يمكنها تقليل كمية الكود النمطي. من ناحية أخرى، تشتهر الماكرو بمكرها. يكفي أن تفوت فاصلة منقوطة واحدة أو تضع الأقواس بشكل غير صحيح، ليتحول الكود إلى وحش لا يمكن التنبؤ به.
\n\nلكن هناك في عالم C++ فئة خاصة من الماكرو تبدو غريبة بشكل صريح. هل سبق لك أن رأيت مثل هذا الكود؟
\n\n#define SAFE_DELETE(ptr) do { delete (ptr); (ptr) = nullptr; } while(0)\n\nيبدو الأمر منطقياً: حذف المؤشر وتحديده بقيمة فارغة. لكن لماذا هذه do-while(0) التي لا معنى لها هنا؟ حلقة يتم تنفيذها مرة واحدة فقط بشكل مضمون؟ تبدو كزيادة غير ضرورية تصل إلى حد العبث. أو هذا المثال الآخر مع if-else:
#define LOG_ERROR(msg) if (g_logging) std::cerr << (msg)\n\nيبدو أن الأمر بسيط. لكن هنا بالتحديد تكمن الفخاخ التي تعثر بها آلاف المطورين. هذا السؤال على Stack Overflow حصد عشرات الآلاف من المشاهدات، واليوم سنشرح لماذا هذه البنى ليست خطأً بل ميزة، وما هي أخطاء ماكرو C++ التي تمنعها.
\n\nلماذا الماكرو ليست دوال؟ المشكلة الرئيسية
\n\nقبل أن نغوص في تفاصيل do-while(0)، دعنا نتذكر كيف يختلف الماكرو عن الدالة العادية. في C++، الماكرو هو استبدال نصي. يقوم المعالج المسبق ببساطة بنسخ جسم الماكرو إلى المكان الذي تم استدعاؤه فيه. لا فحوصات للأنواع، لا نطاقات رؤية - مجرد استبدال للنصوص.
وبسبب هذا تحديداً تنشأ الأخطاء الكلاسيكية في ماكرو C++. لننظر إلى مثال بسيط:
\n\n#define SQUARE(x) x * x\n\nint main() {\n int a = 5;\n int result = SQUARE(a + 1); // نتوقع 36، نحصل على 11\n std::cout << result; // سيطبع 11\n return 0;\n}\n\nماذا حدث؟ تم توسيع الماكرو إلى a + 1 * a + 1. بسبب أسبقية العوامل، تم تنفيذ الضرب قبل الجمع. الخطأ الأول - عدم وجود أقواس. لنصلحه:
#define SQUARE(x) ((x) * (x))\n\nالآن يعمل. لكن هذه مجرد البداية. المشاكل الحقيقية تبدأ عندما يحتوي الماكرو على عدة عبارات أو بنى تحكم.
\n\nاصطلاح do-while(0): درع ضد الكوارث النحوية
\n\nلنعد إلى SAFE_DELETE الخاص بنا. لماذا لا نكتب الماكرو بشكل أبسط، بدون هذه الحلقة الغريبة؟
#define SAFE_DELETE(ptr) delete (ptr); (ptr) = nullptr;\n\nيبدو منطقياً. لكن تخيل أنك تستخدمه في هذا السياق:
\n\nif (ptr != nullptr)\n SAFE_DELETE(ptr);\nelse\n std::cout << "Pointer is null";\n\nبعد الاستبدال، سيحوله المعالج المسبق إلى:
\n\nif (ptr != nullptr)\n delete (ptr);\n (ptr) = nullptr;\nelse\n std::cout << "Pointer is null";\n\nnوها هو الخلل! else الآن لا ينتمي إلى if، لأنه ظهر بينهما عامل ثانٍ. سيصدر المترجم خطأ، لكن الفكرة واضحة: الماكرو "كسر" بنية التحكم. لهذه الحالات تحديداً نحتاج do-while(0). فهو يحول عدة عبارات إلى عبارة مركبة واحدة، محافظاً على المنطق:
if (ptr != nullptr)\n do { delete (ptr); (ptr) = nullptr; } while(0);\nelse\n std::cout << "Pointer is null";\n\nالآن كل شيء يعمل بشكل صحيح. اصطلاح الماكرو do-while(0) هو أسلوب قياسي يضمن أن يتصرف الماكرو كعبارة واحدة.
\n\nلماذا do-while(0) تحديداً، وليس مجرد كتلة { }؟
\n\nسؤال جيد! كان يمكن استخدام الأقواس المعقوفة فقط:
\n\n#define SAFE_DELETE(ptr) { delete (ptr); (ptr) = nullptr; }\n\nلنجرب استخدام