اذهب إلى المحتوى

التعبيرات الثابتة constexpr في Cpp


محمد بغات

‎constexpr‎ هي كلمة مفتاحية يمكن استخدامها مع متغيّر لجعل قيمته تعبيرًا ثابتًا (constant expression)، أو دالةً لأجل استخدامها في التعبيرات الثابتة، أو (منذ C++‎ 17) تعليمة if حتى يُصرَّف فرع واحد فقط من فروعها.

المصادقة عبر الدالة static_assert

تقتضي المصادقات (Assertations) وجوب التحقق من شرط معيّن، وإطلاق خطأ إن كانت خطأً (false)، ويحدث هذا في وقت التصريف بالنسبة لـ ‎static_assert()‎.

template<typename T>
T mul10(const T t)
{
    static_assert( std::is_integral<T>::value, "mul10() only works for integral types" );
    return (t << 3) + (t << 1);
}

المعاملات التي تقبلها الدالة ‎static_assert()‎:

المعامِل التفاصيل
bool_constexpr التعبير المراد التحقق منه
message الرسالة المُراد طباعتها عندما تساوي bool_constexpr القيمة false

المعامل الأوّل إلزامي ويمثّل الشرط، وهو تعبير منطقي ثابت constexpr. كذلك يمكن أن تقبل الدالة أيضًا مُعاملًا ثانيًا، والذي يمثّل الرسالة، وهي سلسلة نصية مجردة. صار المعامِل الثاني اختياريًا ابتداءً من C++‎ 17، أما قبل ذلك كان إلزاميًا.

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

template < typename T >
    T mul10(const T t) {
        static_assert(std::is_integral < T > ::value);
        return (t << 3) + (t << 1);
    }

تُستخدم المصادقات في حال:

  • لزِم التحقق في وقت التصريف من نوع معيّن في تعبير ثابت constexpr.
  • احتاجت دالّة القالب إلى التحقق من خاصّيات معيّنة من النوع المُمرّر إليها.
  • إذا أردت كتابة حالات اختبارية لما يلي:
  • الدوال الوصفية للقوالب template metafunctions
  • دوال التعبيرات الثابتة constexpr functions
  • شيفرة وصفية جامعة macro metaprogramming
  • إن كانت بعض التعريفات مطلوبة (على سبيل المثال، إصدار C++‎)
  • نقل الشيفرات القديمة (Porting legacy code)، والمصادقة (assertation) على ‎sizeof(T)‎ (على سبيل المثال، ‎32-bit int)
  • إن كانت بعض ميزات المصرّف مطلوبة لعمل البرنامج (التحزيم - packing - أو تحسين الأصناف الأساسية الفارغة، وما إلى ذلك)

لاحظ أنّ ‎static_assert()‎ لا تشارك في قاعدة SFINAE: إذا كانت التحميلات الزائدة/ التخصيصات الإضافية ممكنة، فلا ينبغي استخدامها بدلًا من تقنيات قوالب البرمجة الوصفية - template metaprogramming - (مثل ‎std::enable_if<>‎)، وقد تُستخدَم -مع لزوم التحقق منها- في شيفرة القالب في حال إيجاد ([التحميل الزائد](رابط الفصل 35) / التخصيص) المتوقع، وفي مثل هذه الحالات قد تُوفِّر رسالة خطأ أو أكثر تكون أوضح مما لو كنا اعتمدنا على قاعدة SFINAE.

متغيّرات التعبيرات الثابتة (constexpr variables)

إن صُرِّخ عن متغيّر بالكلمة المفتاحية ‎constexpr‎، فسيكون ثابتًا (‎const‎) ضمنيًا، وسيكون من الممكن استخدام قيمته كتعبير ثابت.

المقارنة مع define

يمكن استخدام ‎constexpr‎ كبديل آمن نوعيًا (type-safe) للتعبيرات التي تعتمد على ‎#define‎ في وقت التصريف، وعند استخدام ‎constexpr‎، سيُستبدَل التعبير المُقيَّم في وقت التصريف بنتيجته. انظر المثال التالي:

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

int main() {
    constexpr int N = 10 + 2;
    cout << N;
}

سينتج عن المثال أعلاه الشيفرة التالية:

cout << 12;

ستختلف الشيفرة الجامعة التي تعتمد على المعالج الأولى في وقت التصريف (pre-processor based compile-time macro)، انظر المثال التالي:

#define N 10 + 2
int main() {
    cout << N;
}

هذا سيُنتِج الشيفرة التالية:

cout << 10 + 2;

والذي سيُحوَّل إلى cout << 10 + 2 لكن سيكون على المُصرِّف أن يقوم بالمزيد من العمل، كما قد تحدث مشكلة في حال لم تستخدم بشكل صحيح.

على سبيل المثال (مع ‎#define‎):

cout << N * 2;

سينتج:

cout << 10 + 2 * 2; // 14

سيعيد التقييم الأولي (pre-evaluated) القيمة ‎24‎ للتعبير الثابت ‎constexpr‎، كما هو مُتوقّع.

مقارنة مع const

تحتاج المتغيرات الثابتة (‎const‎) إلى ذاكرة لتخزينها، وذلك على خلاف التعبيرات الثابتة ‎constexpr‎، وتنتج التعبيرات الثابتة ‎constexpr‎ قيمًا ثابتة في وقت التصريف وغير قابلة للتغيير. قد يقال أيضًا أنّ القيمة الثابتة (‎const‎) هي أيضًا غير قابلة للتغيير، لكن انظر المثال التالي لتوضيح الفرق بينهما:

int main() {
    const int size1 = 10;
    const int size2 = abs(10);
    int arr_one[size1];
    int arr_two[size2];
}

ستفشل التعليمة الثانية في معظم المُصرِّفات -رغم أنها قد تعمل في GCC-، إذ يجب أن يكون حجم أيّ مصفوفة تعبيرًا ثابتًا (أي ينتُج عنه قيمة في وقت التصريف). وكما ترى في الشيفرة أعلاه، فقد أُسنِد إلى المتغيّر الثاني ‎size2‎ قيمة ستُحدَّد في وقت التشغيل (runtime) رغم أنّها تساوي ‎10‎، إلا أنّ المُصرِّف لا يعدُّها قيمة تصريفية (تصريفية، من وقت التصريف، compile-time).

هذا يعني أنّ ‎const‎ قد تكون أو لا تكون ثابتة تصريفية حقيقية، ولا تستطيع أن تضمن لقيمة ثابتة ‎const‎ معيّنة أن تكون تصريفيةً، ولك أن تستخدم ‎#define‎ رغم أنها لا تخلو من بعض المشاكل. وعليه، يمكنك استخدام الحلّ التالي:

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

int main() {
    constexpr int size = 10;
    int arr[size];
}

يجب تقييم التعابير الثابتة ‎constexpr‎ إلى قيم تصريفية، لذا لا يمكن استخدام ما يلي …

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

constexpr int size = abs(10);

… ما لم تكن الدالة (‎abs‎) نفسها تعيد تعبيرًا ثابتًا. يجوز تهيئة جميع الأنواع الأساسية باستخدام ‎constexpr‎.

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

constexpr bool FailFatal = true;
constexpr float PI = 3.14 f;
constexpr char* site = "StackOverflow";

يمكنك أيضًا استخدام ‎auto‎ كما في المثال التالي:

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

constexpr auto domain = ".COM"; // const char * const domain = ".COM"
constexpr auto PI = 3.14; // constexpr double

تعليمة if الساكنة

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

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

template<class T, class ... Rest>
void g(T &&p, Rest &&...rs)
{
// ... p معالجة
if constexpr (sizeof...(rs) > 0)
g(rs...); // لا تقم بالتهيئة باستخدام قائمة وسائط فارغة
}

لا يلزم تعريف المتغيّرات والدوال التي استُخدَمت قيمتها (odr-used) حصرًا داخل العبارات المُتجاهلَة (discarded statements)، كما لا تُستخدَم عبارات ‎return‎ المُتجاهلة في استنتاج النوع المعاد من الدالّة.

وتختلف العبارة ‎if‎ ‎constexpr‎ عن شيفرات التصريف الشرطية #ifdef. #ifdef، إذ تعتمد حصرًا على الشروط التي يمكن تقييمها في وقت المعالجة الأولية. فلا يمكن استخدام ‎#ifdef‎ للتحكم في تصريف الشيفرة بناءً على قيمة مُعامل القالب، لكن من ناحية أخرى، لا يمكن استخدام ‎if constexpr‎ لتجاهل الشيفرات ذات الصياغة غير الصحيحة، وذلك على خلاف ‎#ifdef‎.

if constexpr(false) {
    foobar; // error; foobar has not been declared
    std::vector < int > v("hello, world"); // error; no matching constructor
}

دوال التعبيرات الثابتة (constexpr functions)

ستكون الدوالّ المُصرَّح عنها بالكلمة المفتاحية ‎constexpr‎ مُضمّنة (inline) ضمنيًا، وسينتج عن استدعائها تعابير ثابتة، فسيعاد تعبير ثابت إذا كانت الوسائط المُمرّرة إلى الدالة التالية تعابير ثابتة أيضًا:

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

constexpr int Sum(int a, int b) {
    return a + b;
}

يمكن استخدام نتيجة استدعاء الدالّة كمصفوفة مربوطة (array bound) أو وسيط قالب، كما يمكن استخدامها لتهيئة متغيّر تعبير ثابت (constexpr variable):

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

int main() {
    constexpr int S = Sum(10, 20);

    int Array[S];
    int Array2[Sum(20, 30)]; // مصفوفة مؤلفة من 50 عنصرا في وقت التصريف
}

لاحظ أنّك إذا أزلت الكلمة المفتاحية ‎constexpr‎ من تعريف النوع المُعاد الخاص بالدالّة، فلن يعمل الإسناد إلى ‎S‎، لأنّ ‎S‎ متغيّر تعبير ثابت ويجب أن تُسند إليه قيمة تصريفية. وبالمثل، لن يكون حجم المصفوفة تعبيرًا ثابتًا إذا لم تكن ‎Sum‎ دالةَ تعبير ثابت. كذلك تستطيع استخدام دوال التعبيرات الثابتة (‎‎constexpr‎ functions) كما لو كانت دوال عادية:

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

int a = 20;
auto sum = Sum(a, abs(-20));

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

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

int a = 20;
constexpr auto sum = Sum(a, abs(-20));

وذلك لأنه لا ينبغي أن يُسنَد إلى تعبير ثابت إلّا ثابتة تصريفية (compile-time constant). بالمقابل فإن استدعاء الدالة أعلاه يجعل ‎Sum‎ تعبيرًا غير ثابت (القيمة اليمينية غير ثابتة، على خلاف القيمة اليسارية التي صُرَّح عنها كتعبير ثابت).

يجب أيضًا أن تُعيد دالة التعبير الثابت ثابتةً تصريفية (compile-time constant). في المثال التالي، لن تُصرَّف الشيفرة:

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

constexpr int Sum(int a, int b) {
    int a1 = a; // خطأ
    return a + b;
}

لأنّ ‎a1‎ متغيّر غير ثابت، ويمنع الدالّة من أن تكون دالة تعبير ثابت ‎constexpr‎ حقيقية، ولن تنجح محاولة جعلها تعبيرًا ثابتًا وإسناد قيمة a لها - نظرًا لأنّ قيمة a (المُعامل الوارد - incoming parameter) ما تزال غير معروفة بعد:

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

constexpr int Sum(int a, int b) {
        constexpr int a1 = a; // خطأ
        ..

وكذلك لن تُصرَّف الشيفرة التالية:

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

constexpr int Sum(int a, int b) {
    return abs(a) + b; // abs(a) + abs(b) أو
}

وبما أن ‎abs(a)‎ ليست تعبيرًا ثابتًا -ولن تعمل ‎abs(10)‎، إذ لن تعيد ‎abs‎ قيمة من النوع ‎constexpr int‎- فماذا عن الشيفرة التالية؟: الإصدار ≥ C++‎ 11

constexpr int Abs(int v) {
    return v >= 0 ? v : -v;
}
constexpr int Sum(int a, int b) {
    return Abs(a) + b;
}

لقد صمّمنا الدالّة ‎Abs‎ وجعلناها دالة تعبير ثابت، كما أنّ جسم ‎Abs‎ لن يخرق أيّ قاعدة. كذلك تعطي نتيجة تقييم التعبير هي تعبير ثابت ‎constexpr‎، وذلك في موضع الاستدعاء (داخل ‎Sum‎). ومن ثم يكون استدعاء Sum(-10, 20)‎‎ تعبيرًا ثابتًا في وقت التصريف (compile-time constexpr) ينتج عنه القيمة ‎30‎.

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

ترجمة -بتصرّف- للفصلين Chapter 118: static_assert و Chapter 119: constexpr من كتاب C++ Notes for Professionals


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...