تتيح الترجمة الشرطية لملف مصدري واحد أن يصير عدة برامج مختلفة. فـ المعالج الأولي يقرّر، قبل تشغيل المترجم، أي كتل النصّ تبقى - فالشيفرة داخل فرع غير محقّق ليست متخطّاة أثناء التشغيل فحسب، بل محذوفة. ولا تُحلَّل قطّ، فلا تسهم بشيء في الملف التنفيذي، بل يمكنها أن تحوي تراكيب يرفضها المترجم الحالي.
وهذا هو الفرق عن if العادية. فكلتاهما تختار بين مسارين؛ لكن واحدة منهما تختار قبل أن يوجد البرنامج.
#ifdef و#ifndef
أبسط سؤال هو هل الماكرو معرَّف أصلًا:
احذف سطر #define DEBUG فتختفي كل طباعة تنقيح من البناء - ليست معطّلة، بل زائلة. شغّله مرة كما هو، ثم احذف ذلك السطر وشغّله مجددًا.
و#ifndef هي النفي: أبقِ الكتلة فقط إن كان الماكرو غير معرَّف. وأشيع استخداماتها هو حارس التضمين الذي تغطّيه ملفات الترويسة، وهي أيضًا الطريقة التي توفّر بها قيمة افتراضية يمكن للمستدعي تجاوزها:
ويجب إغلاق كل #ifdef و#ifndef بـ #endif. وتوثيق المُغلِق في الملفات الطويلة - #endif /* DEBUG */ - يوفّر وقتًا حقيقيًا لاحقًا.
#if و#elif و#else
يأخذ #if تعبيرًا عدديًا ثابتًا ويُبقي الكتلة حين يكون غير صفري. وهذا ما يجعل مقارنات الإصدارات والمستويات ممكنة:
غيّر LOG_LEVEL إلى 0 أو 3 وأعد التشغيل لترى البرنامج نفسه يغيّر شكله.
ويقيّم المعالج الأولي التعبير، ما يعني أنه لا يستطيع استخدام سوى الثوابت الصحيحة وعوامل الحساب والمقارنة والماكروات التي تتوسّع إلى تلك. ولا يستطيع رؤية sizeof ولا قيم enum ولا المتغيّرات ولا أي شيء يحتاج المترجم. والماكرو غير المعرَّف داخل #if يُقيَّم إلى 0 بدل أن يكون خطأً، وهو أمر مريح ومفاجئ أحيانًا:
#if FEATURE_X /* FEATURE_X is never defined anywhere -> 0 -> block dropped */
وdefined(NAME) تحوّل سؤال #ifdef إلى شيء صالح للاستخدام داخل #if، فيمكنك دمج الفحوص:
#if defined(DEBUG) && !defined(NDEBUG)
/* بناء تنقيح، والتأكيدات مفعّلة */
#endif
#if defined(LINUX) || defined(BSD)
/* أي نظام شبيه بيونكس */
#endif
#if defined(X) و#ifdef X تعنيان الشيء نفسه؛ فالجأ إلى الأولى حين تحتاج && أو || أو !.
تعريف الماكرو من سطر الأوامر
القوة الحقيقية لهذه المفاتيح أن المصدر لا يحتاج إلى تغيير إطلاقًا. فـ gcc -D يعرّف ماكرو للترجمة كلها:
gcc -DDEBUG program.c -o program # DEBUG defined as 1
gcc -DLOG_LEVEL=3 program.c -o program # specific value
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app
و-DNAME وحدها تكافئ #define NAME 1. و-U NAME تلغي تعريف واحد، وهو ما يهمّ حين تعرّف ترويسة شيئًا تريده مطفأً.
فالترتيب المعتاد هو: المصدر يحوي قيمًا افتراضية محروسة بـ #ifndef، وأمر البناء يختار تهيئة.
/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1 /* quiet by default */
#endif
#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# development build
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev
# release build
gcc -DNDEBUG -O2 src/*.c -o app
وNDEBUG معياري: فتعريفه يعطّل كل assert() في البرنامج، لأن <assert.h> نفسها مكتوبة بالترجمة الشرطية. وذلك هو النمط مصغّرًا - ترويسة تُترجَم إلى أشياء مختلفة تبعًا لما عرّفه البناء.
مفاتيح المنصّات
تعرّف المترجمات مسبقًا ماكروات تحدّد النظام الهدف، فيستطيع ملف مصدري واحد استدعاء الواجهة الصحيحة على كلٍّ منها:
#if defined(_WIN32)
#include <windows.h>
#define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
#include <unistd.h>
#define CLEAR_SCREEN "clear"
#elif defined(__linux__)
#include <unistd.h>
#define CLEAR_SCREEN "clear"
#else
#error "Unsupported platform"
#endif
والشائعة منها: _WIN32 (معرَّف على ويندوز بنسختيه 32 و64 بتًا)، و_WIN64، و__linux__، و__APPLE__، و__unix__، و__ANDROID__. ولهوية المترجم مجموعتها الخاصة - __GNUC__ و__clang__ و_MSC_VER - وللمعمارية كذلك - __x86_64__ و__aarch64__.
وإليك نسخة قابلة للتشغيل تبلّغ عمّا بُنيت عليه:
و#error تستحقّ المعرفة بذاتها: فهي توقف الترجمة برسالتك. وإنهاء سلسلة منصّات بـ #else / #error "Unsupported platform" يحوّل بناءً خاطئًا صامتًا إلى فشل واضح في أعلى سجلّ البناء.
ولسرد كل ماكرو يعرّفه مترجمك مسبقًا:
gcc -dM -E - < /dev/null
ذلك الخرج هو الجواب الموثوق عن "ما الذي يمكنني فحصه على هذا الجهاز".
#if 0 لتعطيل الشيفرة
تعطيل كتلة بـ /* ... */ يفشل لحظة احتواء الكتلة على تعليق خاص بها، لأن تعليقات C لا تتداخل - فأول */ بالداخل تُنهي التعليق الخارجي ويصبح كل ما بعدها شيفرة شاردة. أما #if 0 فلا تعاني من ذلك:
#if 0
/* This whole region is removed, comments and all. */
legacy_init();
int n = old_calculation(42); /* even this comment is fine */
report(n);
#endif
اقلبها إلى #if 1 لإعادة الشيفرة. وتواصل المحرّرات تلوينها كـ C، ويمكنك تداخل #if 0 داخل #if آخر.
وهي أداة تنقيح لا نظام تخزين. فالشيفرة القابعة في #if 0 لا تُترجَم أبدًا، فتتعفّن بصمت - ولن تُبنى بحلول الوقت الذي يقلب فيه أحدهم المفتاح. استخدمها أثناء تضييق مشكلة، ثم احذف الكتلة ودع نظام التحكّم بالإصدارات يتذكّرها.
أين تسوء الترجمة الشرطية
ثلاثة أنماط فشل تفسّر معظم الألم.
الشيفرة التي لا تُترجَم أبدًا لا تُفحَص أبدًا. فالخطأ المطبعي داخل فرع #ifdef غير مفعّل غير مرئي حتى يبني أحدهم تلك التهيئة - ربما بعد أشهر، وربما في التكامل المستمر على منصّة لا تملكها. وإن كان للمشروع فروع منصّات مهمّة، فابنِها كلها بانتظام.
تشابك #ifdef مع تدفّق التحكّم يصبح غير مقروء بسرعة. فمثل هذا يصعب التفكير فيه:
if (ready) {
#ifdef FAST_PATH
fast_send(buf);
} else {
#endif
slow_send(buf);
}
فالأقواس المفتوحة في فرع والمغلقة في آخر مسموحة وفظيعة. فضّل الشروط التي تغلّف دوالًا كاملة، ثم اختر بينها:
#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif
if أثناء التشغيل أفضل غالبًا. فإن كان كلا الفرعين سيُترجَم في كل مكان، فإن if (debug_enabled) عادية تُبقي المسارين مفحوصَي الأنواع وقابلَين للاختبار وللتبديل دون إعادة بناء. احفظ المعالج الأولي لما يستطيعه وحده: الشيفرة التي لا تستطيع فعلًا أن تُترجَم على المنصّة الأخرى.
وللاطّلاع على النمط الذي يجمع كل هذا - ترويسة قيم افتراضية، و-D لكل بناء، وفروع منصّات ملفوفة بحرّاس تضمين - انظر ملفات الترويسة؛ ولـ #define نفسها، انظر الماكروات.
الأسئلة الشائعة
ما الترجمة الشرطية في لغة C؟
هي استخدام توجيهات المعالج الأولي لإبقاء كتل من الشيفرة المصدرية أو التخلّص منها قبل تشغيل المترجم. والشيفرة داخل #ifdef غير محقّق تُحذف من النصّ كليًا - فلا تُحلَّل ولا تُترجَم ولا تظهر في الملف التنفيذي أبدًا. وبها يدعم ملف مصدري واحد عدة منصّات أو أنواع بناء.
ما الفرق بين #ifdef و#if؟
يسأل #ifdef NAME عمّا إذا كان الماكرو موجودًا فقط، بصرف النظر عن قيمته. أما #if expression فيقيّم تعبيرًا عدديًا ثابتًا، فيستطيع مقارنة القيم: #if VERSION >= 3. استخدم #ifdef للرايات التي هي مجرّد موجودة أو غائبة، و#if حين تهمّ القيمة. و#if defined(NAME) يجمع الاثنين ويمكن وصله بـ && و||.
كيف أعرّف ماكرو من سطر الأوامر بـ gcc؟
استخدم -D: فـ gcc -DDEBUG program.c يعرّف DEBUG كأن الملف بدأ بـ #define DEBUG 1، و-DMAX=50 يمنحه قيمة محدّدة. وبهذا تشغّل أنظمة البناء الميزات دون تحرير المصدر، و-U NAME تلغي تعريف واحد.
لماذا نستخدم #if 0 بدل تعطيل الشيفرة بتعليق؟
لأن تعليقات /* ... */ لا تتداخل - فأول */ داخل المنطقة تُنهي التعليق مبكرًا ويصبح الباقي شيفرة معطوبة. أما #if 0 ... #endif فتزيل أي قدر من الشيفرة يحوي أي عدد من التعليقات، وتُبقي تلوين الصياغة حيًّا، وتُقلب بسهولة إلى #if 1.