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