سلسلة ++c للمحترفين الدرس 41: المعالجة الأولية Preprocessor في Cpp


محمد الميداوي

معالج C الأولي هو محلّل/مبدّل نصوص يُشغَّل قبل التصريف الفعلي للشيفرة، ويُستخدم لتوسيع وتيسير استخدام لغة C (وكذلك C++‎ لاحقًا)، ويمكن استخدامه من أجل:

  1. تضمين ملفّات أخرى باستخدام ‎#include‎.
  2. تعريف شيفرة جامعة (macro)، لاستبدال النص باستخدام ‎#define‎
  3. التصريف الشرطي باستخدام ‎#if‎ ‎#ifdef‎.
  4. توجيه شيفرة معيّنة لمنصّة أو مٌصرّف معيّن (امتداد للتصريف الشرطي)

قيود التضمين

قد تُضمّن ترويسة من قبل ترويسة أخرى. لذلك، فالملفّات المصدرية التي تتضمّن عدّة ترويسات قد تتضمّن بعض الترويسات أكثر من مرّة بشكل غير مباشر. وإن كانت إحدى الترويسات المُضمّنة أكثر من مرّة تحتوي على تعريفات، فإنّّ المٌصرّف -بعد المعالجة الأوّلية- سيرصد انتهاكًا لقاعدة التعريف الواحد (الفقرة 3.2 من المعيار C++‎ لعام 2003) ومن ثم يحدث خطأ في التصريف.

يمكن منع التضمين المتعدّد باستخدام دروع التضمين "include guards"، وتُعرَف أيضًا بدروع الترويسة، أو دروع الشيفرة الجامعة (macro guards). وتُنفَّذ هذه الدروع باستخدام مُوجِّهات المُعالج الأوّلي ‎#define‎ و ‎#ifndef‎ و ‎#endif‎. انظر:

// Foo.h
#ifndef FOO_H_INCLUDED
#define FOO_H_INCLUDED

class Foo // تعريف صنف
{
};

#endif

الميزة الرئيسية لاستخدام دروع التضمين أنّها تعمل مع جميع المُصرّفات المتوافقة مع المعايير والمُعالجات الأوّلية.

من ناحية أخرى، قد تخلق دروع التضمين بعض المشاكل للمطورين، لوجوب التأكد أنّ وحدات الشيفرة الجامعة فريدة في جميع الترويسات المُستخدمة في المشروع، خاصة إذا استَخدَمت ترويستين (أو أكثر) ‎FOO_H_INCLUDED‎ كدروع تضمين، فإنّّ أولى تلك الترويستين المُضمّنتين في وحدة تصريف ستمنع تضمين الترويسات الأخرى. هذه التحديات تظهر خاصّة إذا كان المشروع يستخدم مكتبات الطرف الثالث، والتي تحتوي على ترويسات تشترك في استخدام دروع تضمين.

من الضروري أيضًا التأكد من أنّ وحدات الشيفرة الجامعة المستخدمة في دروع التضمين لا تتعارض مع وحدات الشيفرة الجامعة الأخرى المُعرَّفة في الترويسة.

معظم تنفيذات C++‎ تدعم المُوجِّه ‎#pragma once‎ الذي يحرص على عدم تضمين الملفّ إلّا مرّة واحدة فقط في كل عمليّة تصريف، وهو مُوجِّه قياسي، لكنه ليس جزءًا من أيّ معيار من معايير ISO C++‎. مثلا:

// Foo.h
#pragma once
class Foo {};

في الشيفرة السابقة: بينما تُجنِّب ‎#pragma once‎ بعض المشاكل المرتبطة بدروع التضمين، فإنّ ‎#pragma‎ -حسب المعيار- هي بطبيعتها خُطَّاف خاص بالمٌصرّف (compiler-specific hook)، وستُتجاهل بصمت من قبل المٌصرّفات التي لا تدعمها. حمل المشاريع (أي porting) التي تستخدم ‎#pragma once‎ إلى المٌصرّفات التي لا تدعمها ليس بالأمر الهيّن.

هناك عدد من إرشادات التشفير ومعايير C++‎ التي توصي بعدم استخدام أيّ مُعالج أوّلي إلا من أجل تضمين ملفات الترويسة ‎#include‎، أو لأجل وضع دروع التضمين في الترويسات.

المنطق الشرطي والتعامل مع تعدد المنصات

باختصار، يتمحور منطق المعالجة الأوّلية الشرطية حول التحكم في جعل منطق الشيفرة متاحًا للتصريف أو غير متاح باستخدام تعاريف الشيفرات الجامعة.

هذه ثلاث حالات استخدام أساسية:

  • عدّة إصدارات من تطبيق (مثلًا: إصدار للتنقيح، وآخر للإطلاق، وآخر للاختبار، وآخر للتحسين)
  • التصريفات متعدّدة المنصات (cross-platform compiles) - نفس الشيفرة المصدرية، مع تصريفات لعدة منصات
  • استخدام نفس الشيفرة لأجل عدة إصدارات من التطبيق (مثلًا: إصدار Basic و Premium و Pro من البرنامج) - مع ميزات مختلفة قليلاً.

مثال أ: مقاربة متعددة المنصات لإزالة الملفّات:

#ifdef _WIN32
#include <windows.h >    // وبقية ملفات نظام ويندوز

#endif
#include <cstdio>
bool remove_file(const std::string &path)
{
#ifdef _WIN32
return DeleteFile(path.c_str());
#elif defined(_POSIX_VERSION) || defined(__unix__)
return (0 == remove(path.c_str()));
#elif defined(__APPLE__)

    //TODO: دالة مُخصّصة مع نافذة حوار للترخيص NSAPI التحقق مما إذا كان لـ
return (0 == remove(path.c_str()));
#else
#error "This platform is not supported"
#endif

}

مثال ب: إتاحة إمكانية التسجيل الإضافي لأجل التنقيح:

void s_PrintAppStateOnUserPrompt()
{
std::cout << "--------BEGIN-DUMP---------------\n"
<< AppState::Instance()->Settings().ToString() << "\n"
#if ( 1 == TESTING_MODE )  // الخصوصية: لا نريد  المعلومات الشخصية للمستخدم إلا عند الاختبار
<< ListToString(AppState::UndoStack()->GetActionNames())
<< AppState::Instance()->CrntDocument().Name()
<< AppState::Instance()->CrntDocument().SignatureSHA() << "\n"
#endif
<< "--------END-DUMP---------------\n"
}

مثال ج: توفير ميزة مدفوعة (premium feature) في منتج منفصل (ملاحظة: هذا المثال توضيحي. من الأفضل إتاحة الميزة دون الحاجة إلى إعادة تثبيت التطبيق)

void MainWindow::OnProcessButtonClick()
{
#ifndef _PREMIUM
CreatePurchaseDialog("Buy App Premium", "This feature is available for our App Premium users. Click the Buy button to purchase the Premium version at our website");
return;
#endif

            //… الميزات الفعلية هنا
        }

بعض الطرق الشائعة:

  • تعريف رمز في وقت الاستدعاء (invocation): يمكن استدعاء المُعالج الأوّلي برموز مسبقة (مع تهيئة اختيارية). على سبيل المثال (‎gcc -E‎ يعمل فقط على المُعالج الأوّلي):
gcc - E - DOPTIMISE_FOR_OS_X - DTESTING_MODE = 1 Sample.cpp

يُعالَج Sample.cpp كما لو أنّ ‎#define OPTIMISE_FOR_OS_X‎ و ‎#define TESTING_MODE 1‎ مُضافان إلى أعلى Sample.cpp.

  • التأكد أن شيفرة جامعة ما معرَّفة: إذا لم تكن الشيفرة الجامعة مُعرّفة وقورنت قيمتها أو تم التحقق منها، فسيفترض المعالج الأوّلي دائمًا أنّ القيمة ستساوي ‎0‎. وهناك عدّة طرق للتعامل مع هذا، إحداها هي افتراض أنّ الإعدادات الافتراضية تُمثَّل بالعدد 0، ويجب إجراء أيّ تغييرات لازمة بشكل صريح (على سبيل المثال ENABLEEXTRADEBUGGING = 0 افتراضيًا، مع الإسناد -DENABLEEXTRADEBUGGING = 1 لإعادة التعريف).

هناك طريقة أخرى، وهي جعل جميع التعاريف والقيم الافتراضية صريحة، ويمكن تحقيق ذلك بدمج المُوجِّهين ‎#ifndef‎ و ‎#error‎:

#ifndef (ENABLE_EXTRA_DEBUGGING)
// إن لم تكن مضافة سلفا DefaultDefines.h برجاء إضافة
# error "ENABLE_EXTRA_DEBUGGING is not defined"
#else
# if ( 1 == ENABLE_EXTRA_DEBUGGING )
//code
# endif
#endif

وحدات الشيفرة الجامعة التوليدية X-macros

هي تقنية اصطلاحية لتكرار توليد بنيات برمجية في وقت التصريف، وتتكون الشيفرات الجامعة التوليدية من جزأين: القائمة، وتنفيذ القائمة.

هذا مثال توضيحي:

#define LIST\
X(dog)\
X(cat)\
X(racoon)
// class Animal {
//    public:
//      void say();
// };

#define X(name) Animal name;
LIST
#undef X

int main()
{#define X(name) name.say();
    LIST
#undef X
    return 0;
}

هذا المثال سيُوسَّع من قبل المعالج الأوّلي إلى ما يلي:

Animal dog;
Animal cat;
Animal racoon;
int main()
{
    dog.say();
    cat.say();
    racoon.say();
    return 0;
}

عندما تصبح القوائم كبيرة (أكثر من 100 عنصر)، فإنّ هذه التقنية تساعد على تجنّب الإكثار من النسخ واللصق. وإذا أردت تجنّب التعريف غير الضروري لـ ‎X‎ قبل استخدام ‎LIST‎، فيمكنك تمرير اسم شيفرة جامعة كوسيط أيضًا:

#define LIST(MACRO)\
MACRO(dog)\
MACRO(cat)\
MACRO(racoon)

الآن، يمكنك تعريف الشيفرة الجامعة الذي يجب استخدامها عند توسيع القائمة، كما يلي:

#define FORWARD_DECLARE_ANIMAL(name) Animal name;
LIST(FORWARD_DECLARE_ANIMAL)

إذا وجب على كل استدعاء لـ ‎MACRO‎ أن يأخذ معاملات إضافية ثابتة بالنسبة للقائمة، فيمكن استخدام وحدات شيفرات جامعة متغيّرة (variadic macros):

// Visual studio
#define EXPAND(x) x
#define LIST(MACRO, ...)\
EXPAND(MACRO(dog, __VA_ARGS__))\
EXPAND(MACRO(cat, __VA_ARGS__))\
EXPAND(MACRO(racoon, __VA_ARGS__))

يُمرَّر الوسيط الأولى عبر ‎LIST‎، بينما تُوفّر الوسائط الأخرى عبر المستخدم عند استدعاء ‎LIST‎. مثلا، الشيفرة التالية:

#define FORWARD_DECLARE(name, type, prefix) type prefix## name;
LIST(FORWARD_DECLARE, Animal, anim_)
LIST(FORWARD_DECLARE, Object, obj_)

سوف تتوسع على النحو التالي:

Animal anim_dog;
Animal anim_cat;
Animal anim_racoon;
Object obj_dog;
Object obj_cat;
Object obj_racoon;

الشيفرات الجامعة Macros

تُصنّف وحدات الشيفرة الجامعة إلى مجموعتين رئيسيتين: وحدات الشيفرة الجامعة المشابهة للكائنات (object-like macros)، ووحدات الشيفرة الجامعة المشابهة للدوالّ (function-like macros). تُعامَل وحدات الشيفرة الجامعة كأداة لتعويض مقطع (token) معيّن من الشيفرة أثناء عملية التصريف، هذا يعني أنّه يمكن تجريد المقاطع الكبيرة (أو المكرّرة) من الشيفرة عبر شيفرة جامعة. انظر:

// هذه شيفرة جامعة تشبه الكائنات
#define    PI  3.14159265358979

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

#define    AREA(r) (PI *(r) *(r))
// يمكن استخدامها على النحو التالي
double pi_macro = PI;
double area_macro = AREA(4.6);

تستخدم مكتبة Qt هذه التقنية لإنشاء نظام وصفي للكائنات (meta-object system) عن طريق مطالبة المستخدم بتعريف الشيفرة الجامعة Q_OBJECT في رأس الصنف الذي يوسّع QObject. وتُكتب أسماء وحدات الشيفرة الجامعة في العادة بأحرف كبيرة لتسهيل تمييزها عن الشيفرات العادية. هذا ليس شرطًا لكنّ يستحسنه كثير من المبرمجين.

عند مصادفة شيفرة جامعة مشابهة للكائنات فإنها تُوسّع كعملية لصق-نسخ بسيطة، مع استبدال اسم الشيفرَة الجامعة بتعريفها. أمّا الشيفرات الجامعة المشابهة للدوال، فيُوسّع كلّ من اسمها ومعاملاتها.

double pi_squared = PI * PI;
double pi_squared = 3.14159265358979 * 3.14159265358979;
double area = AREA(5);
double area = (3.14159265358979 *(5) *(5))

وغالبًا ما تُوضع معاملات الشيفرة الجامعة الشبيهة بالدالّة (function-like macro parameters) بسبب هذا ضمن الأقواس، كما في ‎AREA()‎ أعلاه، وذلك لمنع أيّ أخطاء قد تحدث أثناء توسيع الشيفرة الجامعة، خاصة الأخطاء التي قد تنتج عن مُعاملات الشيفرة الجامعة التي تتألّف من عدة قيم.

#define BAD_AREA(r) PI *r *r
double bad_area = BAD_AREA(5 + 1.6);
// ما يراه المصرف:
double bad_area = 3.14159265358979 * 5 + 1.6 * 5 + 1.6;

double good_area = AREA(5 + 1.6);
// ما يراه المصرف:
double good_area = (3.14159265358979 *(5 + 1.6) *(5 + 1.6));

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

int oops = 5;
double incremental_damage = AREA(oops++);
// ما يراه المصرف:
double incremental_damage = (3.14159265358979*(oops++)*(oops++));

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

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

#define IF_BREAKER(Func) Func();
if (some_condition)
// Oops.
IF_BREAKER(some_func);
else
std::cout << "I am accidentally an orphan." << std::endl;

في هذا المثال، كسرت الفاصلة المنقوطة المزدوجة غير المقصودة كتلة ‎if...else‎، ومنعت المٌصرّف من مطابقة ‎else‎ بـ ‎if‎. ولكي نمنع هذا، تُحذَف الفاصلة المنقوطة من تعريف الشيفرة الجامعة، ممّا يتسبب في "ابتلاع" الفاصلة المنقوطة مباشرةً بعد أيّ استخدام لها.

#define IF_FIXER(Func) Func()
if (some_condition)
    IF_FIXER(some_func);
else
    std::cout << "Hooray!  I work again!" << std::endl;

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

#define DO_SOMETHING(Func, Param) Func(Param, 2)
// ...

some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42));

عادة، ينتهي تعريف الشيفرة الجامعة في نهاية السطر، فإذا احتاجت الشيفرة الجامعة إلى أن تمتدّ على عدّة أسطر، فيمكن استخدام شرطة عكسية \ في نهاية السطر للإشارة إلى ذلك. ويجب أن تكون هذه المشروطة العكسية هي الحرف الأخير من السطر، فذلك يُخطر المُعالج الأوّلي بوجوب ضمّ السطر التالي إلى السطر الحالي، ومعاملتهُما كسطرٍ واحد. يمكن استخدام هذا عدّة مرات على التوالي.

#define TEXT "I \
am \
many \
lines."
// ...
std::cout << TEXT << std::endl;    //   I am many lines.

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

#define CREATE_OUTPUT_AND_DELETE(Str) \
std::string* tmp = new std::string(Str); \
std::cout << *tmp << std::endl; \
delete tmp;
// ...
CREATE_OUTPUT_AND_DELETE("There's no real need for this to use 'new'.")

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

المقاربة الشائعة لهذا هي استخدام الحلقة do while 0، حيث تُوضع الشيفرة الجامعة في كتلة التكرار. لا تُتبع هذه الكتلة عادة بفاصلة منقوطة، ممّا يسمح لها بابتلاع واحدة.

#define DO_STUFF(Type, Param, ReturnVar) do {\
    Type temp(some_setup_values);\
    ReturnVar = temp.process(Param);\
} while (0)

int x;
DO_STUFF(MyClass, 41153.7, x);

int x;
do {
    MyClass temp(some_setup_values);
    x = temp.process(41153.7);
} while (0);

هناك أيضًا وحدات شيفرة جامعة متغايرة (variadic)، والتي تأخذ عددًا متغيّرا من الوسائط، ثم توسّعها جميعًا بدلاً من المُعامل الخاص ‎__VA_ARGS__‎.

#define VARIADIC(Param, ...) Param(__VA_ARGS__)
VARIADIC(printf, "%d", 8);
// ما يراه المصرِّف:
printf("%d", 8);

لاحظ أنّه يمكن وضع ‎__VA_ARGS__‎ أثناء التوسيع في أيّ مكان في التعريف، وسيُوسّع بشكل صحيح.

#define VARIADIC2(POne, PTwo, PThree, ...) POne(PThree, __VA_ARGS__, PTwo)
VARIADIC2(some_func, 3, 8, 6, 9);
some_func(8, 6, 9, 3);

في حالة المعامِل المتغير الذي ليس له وسائط، فإنّ المٌصرّفات تتعامل مع الفاصلة الزائدة بشكل مختلف، حيث تبتلع بعض المُصرّفات -مثل Visual Studio- الفاصلة بصمت بدون أيّ صيغة خاصّة، فيما ستطلّب منك مُصرّفات أخرى -مثل GCC- وضع ‎##‎ مباشرة قبل ‎__VA_ARGS__‎. ويكون من الحكمة بسبب هذا أن تضع تعريف وحدات الشيفرة الجامعة التي تأخذ عددًا متغيّرا من الوسائط في عبارة شرطية إذا كنت حريصًا على قابلية الشيفرة للعمل في أكثر من مكان. في المثال التالي: COMPILER هو شيفرة جامعة تحدد المصرِّف المستخدم.

#if COMPILER == "VS"
#define VARIADIC3(Name, Param, ...) Name(Param, __VA_ARGS__)
#elif COMPILER == "GCC"
#define VARIADIC3(Name, Param, ...) Name(Param, ##__VA_ARGS__)
#endif /* COMPILER */

الشيفرات الجامعة المُعرّفة مسبقًا (Predefined macros)

الشيفرة الجامعة المُعرّفة مسبقًا هي تلك التي يعرّفها المٌصرّف -على النقيض من تلك التي يعرّفها المستخدمون في الملف المصدري-، ويجب ألّا يُعاد تعريف تلك الشيفرات الجامعة أو إلغاء تعريفها من قِبل المستخدم.

الشيفرات الجامعة التالية مُعرّفة مسبقًا وفقًا لمعيار C++‎:

  • ‎__LINE__‎ - تحتوي على رقم السطر الذي تُسُتخدم فيه هذه الشيفرة الجامعة، ويمكن تغييرها عبر المُوجِّه ‎‎#line.
  • ‎__FILE__‎ - تحتوي على اسم الملفّ الذي تُستخدم فيه هذه الشيفرة الجامعة في، ويمكن تغييرها عبر المُوجِّه ‎‎#line.
  • ‎__DATE__‎ - تحتوي تاريخ تصريف الملف (بتنسيق "Mmm dd yyyy")، ويتم تنسيق Mmm كما لو كانت مُعادة من قبل ‎std::asctime()‎.
  • ‎__TIME__‎ - تحتوي زمن تصريف الملف وفق التنسيق "HH: MM: SS".
  • ‎__cplusplus‎ - تُعرَّف من قبل مصرّف C++‎ أثناء تصريف ملفات C++‎، وقيمتها هي الإصدار القياسي الذي يتوافق مع المٌصرّف، أي ‎199711L‎ لـ C++‎ 98 و C++‎ 03، و ‎201103L‎ لـ C++‎ 11، و ‎201402L‎ لمعيار C++‎ 14.

الإصدار ≥ C++‎ 11

  • ‎__STDC_HOSTED__‎ - تُعطى القيمة ‎1‎ إذا كان التطبيق مُستضافًا (hosted)، أو ‎0‎ إذا كان قائما بذاته (freestanding).

الإصدار ≥ C++‎ 17

  • ‎__STDCPP_DEFAULT_NEW_ALIGNMENT__‎ - تحتوي على size_t حرفية، والتي تمثّل المحاذاة المستخدمة لإجراء استدعاء للعامل ‎operator new‎ غير المدرِك للمحاذاة.

بالإضافة إلى ذلك، يُسمح للشيفرة الجامعة التالية أن تُعرَّف مسبقًا من قبل التنفيذات، لكنّها غير إلزامية:

  • ‎__STDC__‎ - يعتمد معناها على التنفيذ، ولا تٌعرَّف عادة إلّا عند تصريف ملف في لغة C للدلالة على الامتثال الكامل مع معيار C القياسي، أو قد لا تُنفّد إذا قرّر المٌصرّف عدم دعم هذه الشيفرة الجامعة.

الإصدار ≥ C++‎ 11

  • ‎__STDC_VERSION__‎ - يعتمد معناها على التنفيذ، وعادة ما تساوي قيمتها إصدارَ لغة C، على نحو مشابه لـ ‎__cplusplus‎ في إصدار C++‎، أو قد لا تُعرَّف، إذا قرر المٌصرّف عدم دعم هذه الشيفرة الجامعة.
  • ‎__STDC_MB_MIGHT_NEQ_WC__‎ - تُعطى القيمة ‎1‎، إذا كان ممكنًا لقيم الترميز الضيق لمجموعة المحارف الأساسية، ألا تساوي قيم نظرائها الواسعة، مثلًا في حال كان ‎(uintmax_t)'x' != (uintmax_t)L'x'‎.
  • ‎__STDC_ISO_10646__‎ - تُعرّف في حال كان wchar_t مُرمّزًا بترميز اليونيكود (Unicode)، وتُوسّع إلى عدد صحيح على شكل ‎yyyymmL‎ إشارةً إلى دعم آخر مُراجَعة لليونيكود.
  • ‎__STDCPP_STRICT_POINTER_SAFETY__‎ - تُعطى القيمة ‎1‎ إذا كان للتنفيذ نظام أمان صارم للمؤشّرات (strict pointer safety)
  • ‎__STDCPP_THREADS__‎ - تُعطى القيمة ‎1‎ إذا أمكن احتواء البرنامج على أكثر من خيط (thread) واحد للتنفيذ، ينطبق هذا على التنفيذ المستقل (freestanding implementation) أما التنفيذات المستضافة فيمكن أن يكون لها أكثر من خيط واحد.

ربما تجب الإشارة إلى ‎__func__‎، وهي ليست شيفرة جامعة ولكنّها متغيِّر محلّي لدالّة مُعرّفة مُسبقا (predefined function-local variable). يحتوي هذا المتغير على اسم الدالّة التي تُستخدَم فيها على شكل مصفوفة حروف ساكنة وفق تنسيق مُعرّف من قِبل التنفيذ.

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

توجد بعض تلك الشيفرات الجامعة للاستعلام عن دعم بعض الميزات فقط:

#ifdef __cplusplus   // C++ في حال صُرِّفت عبر مصرّف
extern "C"
{
    //C يجب أن تُزخرَف شيفرة 
    // هنا C ترويسة مكتبة
}
#endif

البعض الآخر مفيدة جدًا للتنقيح:

الإصدار ≥ C++‎ 11

bool success = doSomething( /*some arguments*/ );
if( !success ){
std::cerr << "ERROR: doSomething() failed on line " << __LINE__ - 2
<< " in function " << __func__ << "()"
<< " in file " << __FILE__
<< std::endl;
}

والبعض الآخر للتحكم في الإصدار:

int main(int argc, char *argv[])
{
    if( argc == 2 && std::string( argv[1] ) == "-v" ){
std::cout << "Hello World program\n"
            << "v 1.1\n"   // عليك تحديث هذا يدويا
            << "compiled: " << __DATE__ << ' ' << __TIME__    // هذا يُحدّث تلقائيا
            << std::endl;
    }
    else
    {
        std::cout << "Hello World!\n";
    }
}

عمليات المعالجة الأولية

يُستخدم المُعامل ‎#‎ أو مُعامل التنصيص (stringizing operator) لتحويل مُعامل شيفرة جامعة إلى سلسلة نصية، ولا يمكن استخدامه إلّا مع شيفرة جامعة ذات وسائط. انظر المثال التالي حيث يحول المعالج الأولي المعامِلَ x إلى السلسلة النصية المجردة x:

#define PRINT(x) printf(#x "\n")
PRINT(This line will be converted to string by preprocessor);
printf("This line will be converted to string by preprocessor"
    "\n");

يضمّ المُصرّف سلسلتين نصّيتين إلى بعضهما، وسيكون الوسيط ‎printf()‎ النهائي عبارة عن سلسلة نصية مجردة تنتهي بمحرف السطر الجديد. وسيتجاهل المُعالج الأوّلي المسافات البيضاء الموجودة قبل أو بعد وسيط الشيفرة الجامعة، لذلك ستطبع الشيفرة التالية نفس النتيجة.

PRINT(This line will be converted to string by preprocessor);

إذا تطلب مُعامل السلسلة النصية المجردة تسلسل تهريب (escape sequence)، كما هو الحال قبل علامة الاقتباس المزدوجة، فسيُدرَج تلقائيًا عبر المعالج الأوّلي.

PRINT(This "line" will be converted to "string" by preprocessor);
printf("This \"line\" will be converted to \"string\" by preprocessor""\n");

يُستخدم العامل ‎##‎ أو عامل لصق القِطع (Token pasting operator) لضمّ سلسلتين نصّيتين أو مقطعَين خاصين بشيفرة جامعة. انظر المثال التالي حيث يدمج المعالجُ الأولي المتغيرَ مع x:

#define PRINT(x) printf("variable" #x " = %d", variable##x)
int variableY = 15;
PRINT(Y);

printf("variable""Y"" = %d", variableY);

وسنحصل على الناتج التالي:

variableY = 15

‎‎ #pragma once

تدعم معظم تطبيقات C++‎ المُوجِّه ‎#pragma once‎ الذي يتحقّق من أنّ الملفّ لن يُضمَّن إلّا مرّة واحدة في كل عملية تصريف، وهو ليس جزءًا من أيّ معيار من معايير ISO C++‎.

على سبيل المثال:

// Foo.h
#pragma once
class Foo {};

بينما تتجنب ‎#pragma once‎ بعض المشاكل المرتبطة بدروع التضمين (include guards) التي ذكرناها آنفًا، فإنّ ‎#pragma‎ -حسب تعريفها في المعايير- هي خطّاف خاصّ بالمٌصرّف (compiler-specific hook)، وسيُتجاهَل بصمت من قبل المٌصرّفات التي لا تدعمه.

ويجب تعديل المشاريع التي تستخدم ‎#pragma once‎ لتكون متوافقة مع المعايير، وقد يؤدّي ‎#pragma once‎ إلى تسريع كبير لعملية التصريف في بعض المُصرِّفات -خاصّة تلك التي تستخدم الترويسات المُصرّفة مسبقًا-. وبالمثل، تسرّع بعض المُعالجات الأوّلية عملية التصريف من خلال التحقق من الترويسات المُتضمّنة التي تستخدم الدروع.

قد تزيد الفائدة من استخدام كل من ‎#pragma once‎ والدروع معًا أو تنقص بحسب التنفيذ ووقت التصريف. وقد كان يوصى بدمج ‎#pragma once‎ مع دُروع التضمين عند كتابة تطبيقات MFC على ويندوز، وكانت تُنشأ بواسطة الأدوات ‎add class‎ و ‎add dialog‎ و ‎add windows‎ الخاصة بـ Visual Studio، لذا من الشائع أن تجدها في تطبيقات C++‎ الموجّهة لويندوز.

رسائل خطأ المُعالج الأوّلي

يمكن توليد أخطاء التصريف باستخدام المعالج الأوّلي، هذا مفيد لعدد من الأسباب:

  • إخطار المستخدم في حال كان على منصّة أو مٌصرّف غير مدعوم.
  • إعادة خطأ في حال كان الإصدار أقل من gcc 3.0.0.
#if __GNUC__ < 3
#error "This code requires gcc > 3.0.0"
#endif

إعادة خطأ إن كان التصريف على حاسوب Apple.

#ifdef __APPLE__
#error "Apple products are not supported in this release"
#endif

هذا الدرس جزء من سلسلة مقالات عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 75: Preprocessor من كتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن