سر ماكرو C++: لماذا نحتاج do-while(0) و if-else في الماكرو و3 أخطاء قاتلة

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

الماكرو في 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:

\n\n
#define LOG_ERROR(msg) if (g_logging) std::cerr << (msg)
\n\n

يبدو أن الأمر بسيط. لكن هنا بالتحديد تكمن الفخاخ التي تعثر بها آلاف المطورين. هذا السؤال على Stack Overflow حصد عشرات الآلاف من المشاهدات، واليوم سنشرح لماذا هذه البنى ليست خطأً بل ميزة، وما هي أخطاء ماكرو C++ التي تمنعها.

\n\n

لماذا الماكرو ليست دوال؟ المشكلة الرئيسية

\n\n

قبل أن نغوص في تفاصيل do-while(0)، دعنا نتذكر كيف يختلف الماكرو عن الدالة العادية. في C++، الماكرو هو استبدال نصي. يقوم المعالج المسبق ببساطة بنسخ جسم الماكرو إلى المكان الذي تم استدعاؤه فيه. لا فحوصات للأنواع، لا نطاقات رؤية - مجرد استبدال للنصوص.

\n\n

وبسبب هذا تحديداً تنشأ الأخطاء الكلاسيكية في ماكرو 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. بسبب أسبقية العوامل، تم تنفيذ الضرب قبل الجمع. الخطأ الأول - عدم وجود أقواس. لنصلحه:

\n\n
#define SQUARE(x) ((x) * (x))
\n\n

الآن يعمل. لكن هذه مجرد البداية. المشاكل الحقيقية تبدأ عندما يحتوي الماكرو على عدة عبارات أو بنى تحكم.

\n\n

اصطلاح do-while(0): درع ضد الكوارث النحوية

\n\n

لنعد إلى SAFE_DELETE الخاص بنا. لماذا لا نكتب الماكرو بشكل أبسط، بدون هذه الحلقة الغريبة؟

\n\n
#define SAFE_DELETE(ptr) delete (ptr); (ptr) = nullptr;
\n\n

يبدو منطقياً. لكن تخيل أنك تستخدمه في هذا السياق:

\n\n
if (ptr != nullptr)\n    SAFE_DELETE(ptr);\nelse\n    std::cout << "Pointer is null";
\n\n

بعد الاستبدال، سيحوله المعالج المسبق إلى:

\n\n
if (ptr != nullptr)\n    delete (ptr);\n    (ptr) = nullptr;\nelse\n    std::cout << "Pointer is null";
\n\nn

وها هو الخلل! else الآن لا ينتمي إلى if، لأنه ظهر بينهما عامل ثانٍ. سيصدر المترجم خطأ، لكن الفكرة واضحة: الماكرو "كسر" بنية التحكم. لهذه الحالات تحديداً نحتاج do-while(0). فهو يحول عدة عبارات إلى عبارة مركبة واحدة، محافظاً على المنطق:

\n\n
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

لنجرب استخدام

المدونات

توصيات الكتب