تشير البرمجة الوصفية (Metaprogramming) في C++ إلى استخدام وحدات الماكرو أو القوالب لتوليد شيفرة أثناء وقت التصريف (compile)، ويُفضّل استخدام القوالب بدلًا من وحدات الماكرو رغم أنّها أقلّ شمولية منها.
وغالبًا ما تستفيد البرمجة الوصفية للقوالب من الحسابات أثناء وقت التصريف (compile-time)، سواء عبر القوالب أو عبر دوال constexpr
لتوليد الشيفرة البرمجية. تجدر الإشارة إلى أنّ حسابات وقت التصريف لا تُصنّف على أنّها من البرمجة الوصفية.
حساب المضروب (Calculating Factorial)
يمكن حساب المضروب أثناء التصريف باستخدام تقنيّات البرمجة الوصفية للقوالب (template metaprogramming)، انظر:
#include <iostream> template < unsigned int n > struct factorial { enum { value = n * factorial < n - 1 > ::value }; }; template < > struct factorial < 0 > { enum { value = 1 }; }; int main() { std::cout << factorial < 7 > ::value << std::endl; // "5040" طباعة }
المضروب factorial
هو بُنية (struct)، بيْد أنّ البرمجة الوصفية للقوالب تُعامله على أنّه دالة قالب وصفية (template metafunction). اصطلاحًا، تُقيَّم دوال القالب الوصفية عن طريق التحقق من عضو معيّن، إمّا ::type
بالنسبة للدوال الوصفية التي تعيد أنواعًا، أو ::value
بالنسبة للدوال الوصفية التي تعيد قيمًا. وفي الشيفرة أعلاه، نقيِّم الدالة الوصفية factorial
عن طريق استنساخ (instantiating) القالب باستخدام المعامِلات التي نريد تمريرها، ونستخدم ::value
للحصول على نتيجة التقييم.
تعتمد الدالة الوصفية على استنساخ نفس الدالة الوصفية ذاتيًا لكن مع قيم أصغر، فيما تمثّل الحالة الخاصة factorial <0>
شرطَ الإنهاء. وتنطبق على البرمجة الوصفية للقوالب معظم قيود لغات البرمجة الدالّية (functional programming language)، لذا فإنّ الذاتية (recursion) هي البنية الأساسية للتكرار "looping". ولمّا كانت دوال القالب الوصفية تُنفَّذ في وقت التصريف، فيمكن استخدام نتائجها في السياقات التي تتطلب قِيَمَ وقت التصريف (compiletime values). انظر المثال التالي:
int my_array[factorial<5>::value];
يجب أن يُحدَّ حجم المصفوفات أثناء التصريف، وتكونَ نتيجة الدالة الوصفية قيمة ثابتة وقت التصريف كي يمكن استخدامها هنا.
ملاحظة: لن تسمح معظم المُصرّفات بأن تتعمّق العوديّة أبعد من حدّ معيّن. على سبيل المثال، المُصرّف g++
افتراضيًّا يحدُّ العوديّة في 256 مستوى. في حالة المصرّف g++
، يمكن للمُبرمج ضبط عمق الذاتية باستخدام الخيار -ftemplate-depth-X
.
الإصدار ≥ C++ 11
منذ الإصدار C++ 11، يمكن استخدام قالب std::integral_constant
في هذا النوع من حسابات القوالب:
#include <iostream> #include <type_traits> template < long long n > struct factorial: std::integral_constant < long long, n * factorial < n - 1 > ::value > {}; template < > struct factorial < 0 >: std::integral_constant < long long, 1 > {}; int main() { std::cout << factorial < 7 > ::value << std::endl; // "5040" }
كذلك فإن دوال constexpr
بديل أفضل. انظر:
#include <iostream> constexpr long long factorial(long long n) { return (n == 0) ? 1 : n * factorial(n - 1); } int main() { char test[factorial(3)]; std::cout << factorial(7) << '\n'; }
يُكتب متن الدالة factorial()
كتعليمة واحدة لأن دوال التعابير الثابتة constexpr
في C++ 11 تستخدم قسمًا محدودًا للغاية من اللغة.
الإصدار ≥ C++ 14
صار من السهل كتابة دوال التعابير الثابتة constexpr
منذ إصدار C++ 14 بعد إسقاط العديد من القيود على كتابتها، انظر:
constexpr long long factorial(long long n) { if (n == 0) return 1; else return n * factorial(n - 1); }
أو حتى بالصورة التالية:
constexpr long long factorial(int n) { long long result = 1; for (int i = 1; i <= n; ++i) { result *= i; } return result; }
الإصدار ≥ C++ 17
بدءًا من الإصدار C++ 17، صار من الممكن استخدام تعبير مطوي (fold expression) لحساب المضروب، انظر:
#include <iostream> #include <utility> template < class T, T N, class I = std::make_integer_sequence < T, N >> struct factorial; template < class T, T N, T...Is > struct factorial < T, N, std::index_sequence < T, Is... >> { static constexpr T value = (static_cast < T > (1) * ... * (Is + 1)); }; int main() { std::cout << factorial < int, 5 > ::value << std::endl; }
التكرار على حزمة معامِلات
قد نحتاج أحيانًا إلى إجراء عملية على كل عنصر من عناصر حزمة معاملات قالب متغير (variadic template parameter pack)، وهناك عدة طرق لفعل ذلك كما أنها صارت أسهل في C++ 17.
إذا افترضنا أننا نريد طباعة كل عنصر في الحزمة يكون أبسط حل لدينا هو الذاتية (Recursion)، انظر المثال التالي:
الإصدار C++ 11
void print_all(std::ostream & os) { // الحالة الأساسية } template < class T, class...Ts > void print_all(std::ostream & os, T const & first, Ts const & ...rest) { os << first; print_all(os, rest...); }
نستطيع استخدام أسلوب الموسِّع expander
بدلًا مما سبق من أجل تنفيذ كل عمليات البث (streaming) في دالة واحدة، ولم نكن لنحتاج إلى تحميلٍ زائدٍ آخر، لكن يعيب هذا الأسلوب أنه يجعل من الصعب قراءة الشيفرة. انظر:
الإصدار C++ 11
template < class...Ts > void print_all(std::ostream & os, Ts const & ...args) { using expander = int[]; (void) expander { 0, (void(os << args), 0)... }; }
راجع إجابة TC في موقع stackoverflow لتفهم كيفية عمل ما سبق بشكل أوضح.
الإصدار ≥ C++ 17
لدينا بدءًا من الإصدار C++ 17 أَداتين جديدتين لحلّ هذه المشكلة، الأولى هي التعبير المطوي (fold-expression):
template < class...Ts > void print_all(std::ostream & os, Ts const & ...args) { ((os << args), ...); }
والثانية هي تعليمة if constexpr
، والتي تسمح لنا بكتابة حلّنا الذاتي الأول في دالة واحدة، انظر المثال التالي حيث يُستنسَخ سطر if constexpr
في حالة وجود وسائط أخرى فقط، أما إن كان rest
فارغًا فلن يكون أي استدعاءٍ لـ (print_all(os
.
template < class T, class...Ts > void print_all(std::ostream & os, T const & first, Ts const & ...rest) { os << first; if constexpr(sizeof...(rest) > 0) { print_all(os, rest...); } }
التكرار عبر std::integer_sequence
صار قالب الصنف (class template) متاحًا منذ إصدار C++ 14، انظر:
template < class T, T...Ints > class integer_sequence; template < std::size_t...Ints > using index_sequence = std::integer_sequence < std::size_t, Ints... > ;
وكذلك دالةُ عُليا مولِّدة (generating metafunction) لأجله:
template < class T, T N > using make_integer_sequence = std::integer_sequence < T, /* a sequence 0, 1, 2, ..., N-1 */ > ; template < std::size_t N > using make_index_sequence = make_integer_sequence < std::size_t, N > ;
ورغم أنّ هذا لم يصبح معتمدًا إلا منذ الإصدار C++ 14، إلا أنه يمكن تنفيذه باستخدام أدوات C++ 11، ونستطيع استخدام هذه الأداة لاستدعاء دالّة مع تمرير صفّ std::tuple
من الوسائط (اعتُمِدَت في C++ 17 كـ std::apply
) انظر:
namespace detail { template <class F, class Tuple, std::size_t... Is> decltype(auto) apply_impl(F&& f, Tuple&& tpl, std::index_sequence<Is...> ) { return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(tpl))...); } } template <class F, class Tuple> decltype(auto) apply(F&& f, Tuple&& tpl) { return detail::apply_impl(std::forward<F>(f), std::forward<Tuple>(tpl), std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{}); } // 3 هذا سيطبع int f(int, char, double); auto some_args = std::make_tuple(42, 'x', 3.14); int r = apply(f, some_args); // تستدعي f(42, 'x', 3.14)
إرسال الوسوم (Tag Dispatching)
إحدى أبسط الطرق للاختيار بين الدوال عند وقت التصريف هي إرسال دالة إلى زوج من الدوال المُحمّلة بحمل زائد والتي تأخذ وسمًا كأحد وسائطها (attributes) -يكون الأخير عادة-، فمثلًا لتطبيق std::advance()
، نستطيع تنفيذ الإرسال على فئة المُكرّر، انظر:
namespace details { template < class RAIter, class Distance > void advance(RAIter & it, Distance n, std::random_access_iterator_tag) { it += n; } template < class BidirIter, class Distance > void advance(BidirIter & it, Distance n, std::bidirectional_iterator_tag) { if (n > 0) { while (n--) ++it; } else { while (n++) --it; } } template < class InputIter, class Distance > void advance(InputIter & it, Distance n, std::input_iterator_tag) { while (n--) { ++it; } } } template < class Iter, class Distance > void advance(Iter & it, Distance n) { details::advance(it, n, typename std::iterator_traits < Iter > ::iterator_category {}); }
وسائط std::XY_iterator_tag
الخاصّة بدوال details::advance
ذات الأحمال الزائدة هي مُعامِلات غير مستخدمة، ذلك أن التطبيق الفعلي لا يهم لأنه فارغ تمامًا، والغرض الوحيد منها هو السماح للمصرّف باختيار التحميل الزائد بناءً على وسم الصنف details::advance
الذي استُدعي معه.
تستخدم advance
في هذا المثال دالةَ iterator_traits<T>::iterator_category
الوصفية، والتي تعيد أحد أصناف iterator_tag
بناءً على نوع Iter
الفعلي، ثم بعد ذلك يتيح كائن منشأ افتراضيًا من نوع iterator_category<Iter>::type
للمصرِّف اختيار أحد تحميلات details::advance
الزائدة. سيتجاوز المصرِّفُ على الأرجح معامِل الدالة ذاك بما أنه كائن منشأ افتراضيًا لِبُنيَة فارغة لم تستخدم.
إرسال الوسوم يجعل الشيفرة أسهل في القراءة مقارنة بغيرها، وذلك باستخدام قاعدة "خطأ التعويض ليس خطأً" أو SFINAE اختصارًا بالإنجليزية، وكذلك استخدام enable_if
.
ملاحظة: رغم أنّ if constexpr
قد تبسِّط تنفيذ advance
بشكل خاص، فإنها -خلاف إرسال الوسوم- غير مناسبة للتطبيقات المفتوحة (open implementations).
التحقق من صحة تعبير ما
نستطيع التحقق إن كان يجوز استدعاء عامل (operator) أو دالة على نوع ما، كذلك يمكن التحقق إن كان صنف ما لديه تحميل زائد على std::hash
، وذلك كما يلي:
#include <functional> // for std::hash #include <type_traits> // for std::false_type and std::true_type #include <utility> // for std::declval template<class, class = void> struct has_hash : std::false_type {}; template<class T> struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())> : std::true_type {};
الإصدار ≥ C++ 17
يمكن استخدام std::void_t
لتبسيط هذا النوع من الإنشاءات منذ الإصدار C++ 17.
#include <functional> // for std::hash #include <type_traits> // for std::false_type, std::true_type, std::void_t #include <utility> // for std::declval template<class, class = std::void_t<> > struct has_hash : std::false_type {}; template<class T> struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > > : std::true_type {};
حيث تٌعرَّف std::void_t
على النحو التالي:
template< class... > using void_t = void;
للتحقق مما إذا كان أحد العوامل مثل operator<
مُعرّفًا، فإن بنية الشيفرة تكون نفسها تقريبًا، انظر:
template<class, class = void> struct has_less_than : std::false_type {}; template<class T> struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())> : std::true_type {};
يمكن الاستفادة مما سبق واستخدامه عند استخدام std::unordered_map<T>
إن كان لـ T
تحميل زائد على std::hash
، لكن فيما سوى ذلك فاستخدام std::map<T>
:
template <class K, class V> using hash_invariant_map = std::conditional_t< has_hash<K>::value, std::unordered_map<K, V>, std::map<K,V>>;
If-then-else
الإصدار ≥ C++ 11
يستطيع نوعُ std::conditional
الموجود في ترويسة المكتبة القياسية
template < typename T > struct ValueOrPointer { typename std::conditional < (sizeof(T) > sizeof(void * )), T * , T > ::type vop; };
تحتوي هذه البنية مؤشّرًا إلى T
إن كان T
أكبر من حجم المؤشّر، أو إلى T
نفسه إذا كان أصغر أو يساوي حجم المؤشر، وعليه سيكون sizeof(ValueOrPointer)
أصغر دائمًا من sizeof(void*)
.
التمييز اليدوي للأنواع عند إعطائها أيَّ نوع T
عند تطبيق قاعدة SFINAE باستخدام std::enable_if
يكون مفيدًا أن نصل إلى قوالب مساعِدةٍ (helper templates) تحدّدُ إذا كان نوع T
المُعطى يطابق معايير معيّنة. وتوفّر المكتبة القياسيّة نوعين مشابهين لـ true
و false
، وهما std::true_type
و std::false_type
، واللذان يمكن استخدامُهما لتحقيق الغرض أعلاه.
يوضّح المثال التالي كيفية التحقّق ممّا إذا كان نوع T
مُؤشِّرًا (pointer) أم لا، ويحاكي قالبُ is_pointer
سلوكَ دالة std::is_pointer
المساعِدة القياسية:
template < typename T > struct is_pointer_: std::false_type {}; template < typename T > struct is_pointer_ < T * >: std::true_type {}; template < typename T > struct is_pointer: is_pointer_ < typename std::remove_cv < T > ::type > {}
تتألّف الشيفرة أعلاه من ثلاث خطوات (قد لا نحتاج أكثر من خطوتين أحيانًا):
-
تصريح
is_pointer_
الأوّل هو الحالة الافتراضية، ويرث منstd::false_type
. يجب أن ترث الحالة الافتراضيّة دائمًا منstd::false_type
لأنّها تشبه "الشرط الخاطئfalse
". -
التصريح الثاني يخصّص القالب
is_pointer_
لأجل المؤشّرT*
بغضِّ النظر عن ماهيةT
، وترث هذه النسخة منstd::true_type
. -
التصريح الثالث (وهو التصريح الحقيقي) يزيل أيّ معلومات غير ضروريّة من
T
(في هذه الحالة يزيل المؤهّليْن (qualifiers) const
وvolatile
) ثم يعود إلى أحد التصريحين السابقين.
وبما أنّ is_pointer<T>
هو صنفٌ فإننا نحتاج إلى التالي من أجل الوصول إلى قيمته:
-
استخدم
::value
، مثلًا:is_pointer<int>::value
: القيمةvalue
هو عضو صنفٍ ثابت (static class member) من النوعbool
موروثٌ منstd::true_type
أوstd::false_type
. -
أنشئ كائنًا من هذا النوع، على سبيل المثال
is_pointer<int>{}
: ذلك مناسب لأنّstd::is_pointer
ترث منشئها الافتراضي منstd::true_type
أوstd::false_type
(التي لها مُنشئات من نوعconstexpr
)، ولكلٍّ منstd::true_type
وstd::false_type
معاملاتُ تحويل منconstexpr
إلىbool
.
من الجيد توفير مساعِدات للقوالب المساعدة، تتيح الوصول المباشر إلى القيمة، انظر:
template <typename T> constexpr bool is_pointer_v = is_pointer<T>::value;
الإصدار ≥ C++ 17 توفر معظم قوالب المساعدة في إصدار C++ 17 وما بعده نسخةَ _v
، انظر المثال التالي:
template< class T > constexpr bool is_pointer_v = is_pointer<T>::value; template< class T > constexpr bool is_reference_v = is_reference<T>::value;
حساب القوة في C++ 11 وما بعدها
صارت العمليات الحسابية في وقت التصريف أيسر بكثير منذ إصدار C++ 11، فيمكن حساب قوة عدد معين في وقت التصريف مثلًا على النحو التالي:
template < typename T > constexpr T calculatePower(T value, unsigned power) { return power == 0 ? 1 : value * calculatePower(value, power - 1); }
وتكون الكلمة المفتاحية constexpr
مسؤولة عن حساب الدالة في وقت التصريف عند استيفاء جميع المتطلّبات اللازمة، كأن تكون جميع الوسائط معروفة في وقت التصريف.
ملاحظة: يجب أن تكون دوال التعبير الثابت (constexpr
) في C++ 11 مكونة من تعليمة return
واحدة فقط.
عند مقارنة هذه الطريقة بالطريقة القياسية لحسابات وقت التصريف، فإن هذه الطريقة مفيدة أيضًا في حسابات وقت التشغيل، فإن كانت وسائط دالة ما مجهولةً وقتَ التصريف (مثل القيمة والقوة المعطاتيْن كمدخلات من قِبل المستخدم) فإن الدالة تُنفَّذ في وقت التصريف، فلا حاجة إذًا لتكرار الشيفرة كما كان في المعايير الأقدم من C++. انظر المثال التالي:
void useExample() { constexpr int compileTimeCalculated = calculatePower(3, 3); // الحساب وقت التصريف // لأنّ كلا المُعاملين معروفان وقت التصريف ويُستخدم في تعبير ثابت int value; std::cin >> value; int runtimeCalculated = calculatePower(value, 3); // runtime calculated, // لأنّ القيمة لن تكون معروفة حتى وقت التشغيل }
الإصدار ≥ C++ 17
وهناك طريقة أخرى لحساب القوة في وقت التصريف تستخدم التعابير المطوية، انظر:
#include <iostream> #include <utility> template <class T, T V, T N, class I = std::make_integer_sequence<T, N>> struct power; template <class T, T V, T N, T... Is> struct power<T, V, N, std::integer_sequence<T, Is...>> { static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1))); }; int main() { std::cout << power < int, 4, 2 > ::value << std::endl; }
دالة عامّة لتحديد القيم الصغرى والعظمى مع عدد وسائط متغير
الإصدار> C++ 11
من الممكن كتابة دالة عامة (على سبيل المثال دالة تحسب الحد الأدنى min
) تقبل أنواعًا عدديّة مختلفة، وعدد وسائط عشوائي بواسطة قالَب برمجة وصفية. انظر المثال التالي حيث تصرّح الدالة عن دالة min
تقبل وسيطين أو أكثر.
template <typename T1, typename T2> auto min(const T1 &a, const T2 &b) -> typename std::common_type<const T1&, const T2&>::type { return a < b ? a : b; } template <typename T1, typename T2, typename ... Args> auto min(const T1 &a, const T2 &b, const Args& ... args) -> typename std::common_type<const T1&, const T2&, const Args& ...>::type { return min(min(a, b), args...); } auto minimum = min(4, 5.8f, 3, 1.8, 3, 1.1, 9);
حسابيات البرمجة الوصفية
فيما يلي أمثلة على استخدام برمجة القوالب الوصفية في C++ لإنجاز العمليات الحسابية في وقت التصريف.
حساب الأس في (O(log n
يوضّح هذا المثال طريقة فعالة لحساب الأسّ باستخدام برمجة القوالب الوصفية.
template <int base, unsigned int exponent> struct power { static const int halfvalue = power<base, exponent / 2>::value; static const int value = halfvalue * halfvalue * power<base, exponent % 2>::value; }; template <int base> struct power<base, 0> { static const int value = 1; static_assert(base != 0, "power<0, 0> is not allowed"); }; template <int base> struct power<base, 1> { static const int value = base; };
مثال تطبيقي:
std::cout << power < 2, 9 > ::value;
الإصدار ≥ C++ 14
هذا المثال يعالج الأسّ السلبيّ أيضًا:
template <int base, int exponent> struct powerDouble { static const int exponentAbs = exponent < 0 ? (-exponent) : exponent; static const int halfvalue = powerDouble<base, exponentAbs / 2>::intermediateValue; static const int intermediateValue = halfvalue * halfvalue * powerDouble<base, exponentAbs %2>::intermediateValue; constexpr static double value = exponent < 0 ? (1.0 / intermediateValue) : intermediateValue; }; template <int base> struct powerDouble<base, 0> { static const int intermediateValue = 1; constexpr static double value = 1; static_assert(base != 0, "powerDouble<0, 0> is not allowed"); }; template <int base> struct powerDouble<base, 1> { static const int intermediateValue = base; constexpr static double value = base; }; int main() { std::cout << powerDouble<2,-3>::value; }
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -بتصرّف- للفصل Chapter 16: Metaprogramming والفصل Chapter 125: Arithmitic Metaprogramming من كتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.