C ++ ، بسبب الكتابة الصارمة ، يمكن أن يساعد المبرمج في مرحلة الترجمة. يوجد بالفعل الكثير من المقالات على المحور تصف كيف ، باستخدام الأنواع ، لتحقيق ذلك ، وهذا جيد. لكن في كل ما قرأته ، هناك عيب واحد. قارن مع منهج ++ ونهج C باستخدام CMSIS ، وهو مألوف في عالم البرمجة متحكم:
some_stream.set (Direction::to_periph) SOME_STREAM->CR |= DMA_SxCR_DIR_0 .inc_memory() | DMA_SxCR_MINC_Msk .size_memory (DataSize::word16) | DMA_SxCR_MSIZE_0 .size_periph (DataSize::word16) | DMA_SxCR_PSIZE_0 .enable_transfer_complete_interrupt(); | DMA_SxCR_TCIE_Msk;
من الواضح على الفور أن أسلوب C ++ أكثر قابلية للقراءة ، وبما أن كل وظيفة تأخذ نوعًا معينًا ، فلا يمكن فهمها. النهج C لا يتحقق من صحة البيانات ، فإنه يقع على عاتق مبرمج. كقاعدة عامة ، يتم التعرف على خطأ فقط أثناء تصحيح الأخطاء. ولكن النهج ج ++ ليست حرة. في الواقع ، تتمتع كل وظيفة بوصولها الخاص إلى السجل ، بينما يتم جمع القناع في C لأول مرة من جميع المعلمات في مرحلة التجميع ، لأن هذه كلها ثوابت ، ويتم كتابتها إلى السجل مرة واحدة. بعد ذلك ، سوف أصف كيف حاولت دمج أمان النوع مع ++ مع تقليل الوصول إلى الحالات. سترى أنه أبسط بكثير مما يبدو.
أولاً ، سأقدم مثالًا على الشكل الذي أرغب فيه. من المستحسن أن هذا لا يختلف كثيرا عن نهج C ++ مألوف بالفعل.
some_stream.set( dma_stream::direction::to_periph , dma_stream::inc_memory , dma_stream::memory_size::byte16 , dma_stream::periph_size::byte16 , dma_stream::transfer_complete_interrupt::enable );
كل معلمة في طريقة المجموعة هي نوع منفصل يمكنك من خلاله فهم أي سجل تريد أن تكتب القيمة ، مما يعني أنه أثناء التحويل البرمجي يمكنك تحسين الوصول إلى السجلات. الطريقة متباينة ، لذلك يمكن أن يكون هناك أي عدد من الوسائط ، ولكن يجب أن يكون هناك تحقق من أن جميع الوسائط تنتمي إلى هذا المحيط.
كانت هذه المهمة في وقت مبكر معقدة إلى حد ما بالنسبة لي ، حتى صادفت مقطع الفيديو هذا حول مخطط البرمجة القائمة على القيمة . يسمح لك هذا النهج في metaprogramming بكتابة خوارزميات معممة كما لو كانت كود زائد عادي. في هذه المقالة ، سأقدم فقط الفيديو الأكثر ضرورة لحل المشكلة ، فهناك خوارزميات أكثر عمومية.
سوف أحل المشكلة في ملخص ، وليس لمحيط محدد. لذلك ، هناك العديد من حقول التسجيل ، سأكتبها بشكل مشروط كتعدادات.
enum struct Enum1 { _0, _1, _2, _3 }; enum struct Enum2 { _0, _1, _2, _3 }; enum struct Enum3 { _0, _1, _2, _3, _4 }; enum struct Enum4 { _0, _1, _2, _3 };
الثلاثة الأولى ستتعلق بأحد الأطراف ، والأربعة إلى الأخرى. وبالتالي ، إذا أدخلت قيمة التعداد الرابع في طريقة المحيط الأول ، فيجب أن يكون هناك خطأ في الترجمة ، ويفضل أن يكون مفهومًا. أيضًا ، ستتعلق أول 2 قائمة بسجل واحد ، والثالث بسجل آخر.
نظرًا لأن قيم التعداد لا تخزن في حد ذاتها أي شيء آخر غير القيم الفعلية ، فهناك حاجة إلى نوع إضافي يخزن ، على سبيل المثال ، قناعًا لتحديد أي جزء من السجل سيتم كتابة هذا التعداد.
struct Enum1_traits { static constexpr std::size_t mask = 0b00111; }; struct Enum2_traits { static constexpr std::size_t mask = 0b11000; }; struct Enum3_traits { static constexpr std::size_t mask = 0b00111; }; struct Enum4_traits { static constexpr std::size_t mask = 0b00111; };
يبقى لربط هذه الأنواع 2. الرقاقة هنا مفيدة بالفعل لـ 20 معيارًا ، لكنها تافهة للغاية ويمكنك تنفيذها بنفسك.
template <class T> struct type_identity { using type = T; };
خلاصة القول هي أنه يمكنك جعل قيمة من أي نوع وتمريرها إلى الدالة كوسيطة. هذا هو لبنة أساسية في نهج metaprogramming القائم على القيمة ، حيث يجب عليك محاولة تمرير معلومات الكتابة من خلال القيم ، وليس كمعلمة قالب. أنا هنا حددت ماكرو ، لكنني خصم منهم في c ++. لكنه يسمح بمزيد من الكتابة أقل. بعد ذلك ، سأقدم تعدادًا للربط وخصائصه إلى دالة وماكرو آخر يسمح بتقليل عدد لصق النسخ.
constexpr auto traits(type_identity<Enum1>) { return type_identity<Enum1_traits>{}; } #define MAKE_TRAITS_WITH_MASK(enum, mask_) struct enum##_traits { \ static constexpr std::size_t mask = mask_; \ }; \ constexpr auto traits(type_identity<enum>) { \ return type_identity<enum##_traits>{}; \ }
يجب أن ترتبط الحقول بالسجلات المقابلة. لقد اخترت العلاقة من خلال الميراث ، لأن المعيار يحتوي بالفعل على ملف تعريف std::is_base_of
، والذي سيتيح لك تحديد العلاقة بين الحقول والسجلات بالفعل في نموذج معمم. لا يمكنك أن ترث من التعدادات ، لذلك نحن نرث من خصائصها.
struct Register1 : Enum1_traits, Enum2_traits { static constexpr std::size_t offset = 0x0; };
يتم تخزين العنوان الذي يوجد به السجل كإزاحة من بداية المحيط.
قبل وصف المحيط ، من الضروري التحدث عن قائمة الأنواع في metaprogramming القائمة على القيمة. هذا هيكل بسيط إلى حد ما يسمح لك بحفظ عدة أنواع وتمريرها حسب القيمة. يشبه type_identity
، لكن بالنسبة لبعض الأنواع.
template <class...Ts> struct type_pack{}; using empty_pack = type_pack<>;
يمكنك تنفيذ العديد من وظائف constexpr لهذه القائمة. تنفيذها أسهل بكثير لفهم من قوائم نوع Alexandrescu الشهيرة (مكتبة Loki). فيما يلي أمثلة.
يجب أن تكون الخاصية المهمة الثانية للمحيط هي القدرة على تحديد موقعها على عنوان محدد (في متحكم) وتمرير العنوان ديناميكيًا للاختبارات. لذلك ، فإن بنية المحيط ستكون عبارة عن لوحة نحاسية ، وكمعلمة تأخذ نوعًا يخزن عنوانًا محددًا للمحيط في حقل القيمة. سيتم تحديد المعلمة قالب من المنشئ. حسنا ، طريقة مجموعة ، والتي ذكرت سابقا.
template<class Address> struct Periph1 { Periph1(Address) {} static constexpr auto registers = type_pack<Register1, Register2>{}; template<class...Ts> static constexpr void set(Ts...args) { ::set(registers, Address::value, args...); } };
كل ما تفعله المجموعة المحددة هو استدعاء دالة مجانية ، مع تمرير كل المعلومات اللازمة للخوارزمية المعممة.
سأقدم أمثلة على الأنواع التي توفر عنوانًا للمحيط.
يتم إعداد جميع المعلومات الخاصة بالخوارزمية المعممة ، ويبقى تنفيذها. سأقدم نص هذه الوظيفة.
template<class...Registers, class...Args> constexpr void set(type_pack<Registers...> registers, std::size_t address, Args...args) {
يعد تنفيذ دالة تقوم بتحويل الوسائط (حقول سجل محددة) إلى type_pack
بسيطًا للغاية. واسمحوا لي أن أذكرك بأن علامة القطع لقائمة أنواع القوالب تكشف عن قائمة بأنواع مفصولة بفواصل.
template <class...Ts> constexpr auto make_type_pack(type_identity<Ts>...) { return type_pack<Ts...>{}; }
للتحقق من أن جميع الوسائط مرتبطة بالسجلات المنقولة ، وبالتالي بأجهزة طرفية محددة ، من الضروري تنفيذ خوارزمية all_of. عن طريق القياس مع المكتبة القياسية ، تتلقى الخوارزمية قائمة الأنواع ووظيفة التقييم كمدخل. نحن نستخدم امدا كدالة.
template <class F, class...Ts> constexpr auto all_of(type_pack<Ts...>, F f) { return (f(type_identity<Ts>{}) and ...); }
هنا ، لأول مرة ، يتم تطبيق تعبير المسح 17 معيار . إن هذا الابتكار هو الذي سهِّل إلى حد كبير حياة أولئك الذين يعشقون البرمجة النصية. في هذا المثال ، يتم تطبيق الدالة f على كل نوع من الأنواع في القائمة Ts ، وتحويله إلى type_identity
، ويتم جمع نتيجة كل مكالمة بواسطة I.
داخل static_assert
، يتم تطبيق هذه الخوارزمية. يتم تمرير args_traits
ملفوفة في type_identity
إلى lambda بدوره. داخل lambda ، يتم استخدام ملف تعريف قياسي std :: is_base_of ، ولكن نظرًا لأنه يمكن أن يكون هناك أكثر من سجل واحد ، يتم استخدام تعبير المسح الضوئي لتنفيذه لكل من السجلات وفقًا لمنطق OR. نتيجة لذلك ، إذا كان هناك وسيطة واحدة على الأقل لا تعد خصائصها أساسية لسجل واحد على الأقل ، static assert
ويعرض رسالة خطأ واضحة. من السهل أن نفهم من أي مكان كان الخطأ (تجاوز الوسيطة الخاطئة للطريقة set
) وإصلاحه.
any_of
خوارزمية any_of
، والتي ستكون مطلوبة لاحقًا:
template <class F, class...Ts> constexpr auto any_of(type_pack<Ts...>, F f) { return (f(type_identity<Ts>{}) or ...); }
تتمثل المهمة التالية للخوارزمية المعممة في تحديد السجلات التي ستحتاج إلى كتابتها. للقيام بذلك ، قم بتصفية القائمة الأولية للسجلات واترك فقط تلك التي توجد بها وسائط في وظيفتنا. نحتاج إلى خوارزمية filter
تأخذ type_pack
الأصلي ، وتطبق الدالة المسند لكل نوع من القائمة ، type_pack
إلى القائمة الجديدة إذا عادت المسند صحيحًا.
template <class F, class...Ts> constexpr auto filter(type_pack<Ts...>, F f) { auto filter_one = [](auto v, auto f) { using T = typename decltype(v)::type; if constexpr (f(v)) return type_pack<T>{}; else return empty_pack{}; }; return (empty_pack{} + ... + filter_one(type_identity<Ts>{}, f)); }
أولاً ، يتم وصف lambda التي تؤدي وظيفة المسند على نوع واحد وتُرجع type_pack
معها إذا عادت المسند true أو فارغة type_pack
إذا تم إرجاع المسند false
. ميزة أخرى جديدة من إيجابيات الماضي يساعد هنا - constexpr إذا. جوهرها هو أنه في الكود الناتج هناك واحد فقط إذا الفرع ، والثاني ألقيت. ونظرًا لأن الأنواع المختلفة ترجع إلى فروع مختلفة ، فبدون constexpr سيكون هناك خطأ في الترجمة. يتم type_pack
نتيجة تنفيذ هذا lambda لكل نوع من القائمة في type_pack
ناتج واحد ، مرة أخرى بفضل تعبير type_pack
. لا يوجد تحميل زائد لمشغل الإضافة type_pack
. تنفيذه هو أيضا بسيط جدا:
template <class...Ts, class...Us> constexpr auto operator+ (type_pack<Ts...>, type_pack<Us...>) { return type_pack<Ts..., Us...>{}; }
تطبيق الخوارزمية الجديدة على قائمة السجلات ، فقط تلك التي يجب أن تكتب الوسيطات المنقولة إليها في القائمة الجديدة.
الخوارزمية التالية التي ستكون مطلوبة هي foreach
. إنه ببساطة يطبق وظيفة على كل نوع في القائمة ، type_identity
في type_identity
. هنا ، يتم استخدام عامل الفاصلة في تعبير الفحص ، الذي يقوم بتنفيذ جميع الإجراءات الموصوفة بواسطة فاصلة ويعيد نتيجة الإجراء الأخير.
template <class F, class...Ts> constexpr void foreach(type_pack<Ts...>, F f) { (f(type_identity<Ts>{}), ...); }
تتيح لك الوظيفة الوصول إلى كل من السجلات التي تريد الكتابة فيها. تحسب lambda قيمة الكتابة إلى السجل ، وتحدد العنوان الذي تريد الكتابة إليه ، وتكتب مباشرةً إلى السجل.
من أجل حساب قيمة سجل واحد ، يتم حساب قيمة كل وسيطة ينتمي إليها هذا السجل ، والجمع بين النتيجة OR.
template<class Register, class...Args> constexpr std::size_t register_value(type_identity<Register> reg, Args...args) { return (arg_value(reg, args) | ...); }
يجب إجراء حساب قيمة لحقل معين فقط للوسيطات الموروثة من هذا السجل. بالنسبة للوسيطة ، نقوم باستخراج قناع من الخاصية الخاصة به ، وتحديد إزاحة القيمة داخل السجل من القناع.
template<class Register, class Arg> constexpr std::size_t arg_value(type_identity<Register>, Arg arg) { constexpr auto arg_traits = traits(type_identity<Arg>{});
يمكنك كتابة الخوارزمية لتحديد قناع الإزاحة بنفسك ، لكني استخدمت الدالة المضمنة الحالية.
constexpr auto shift(std::size_t mask) { return __builtin_ffs(mask) - 1; }
تبقى الوظيفة الأخيرة التي تكتب القيمة إلى عنوان محدد.
inline void write(std::size_t address, std::size_t v) { *reinterpret_cast<std::size_t*>(address) |= v; }
لاختبار المهمة ، يتم كتابة اختبار صغير:
تم الجمع بين كل شيء مكتوب هنا وتجميعها في godbolt . يمكن لأي شخص هناك تجربة مع هذا النهج. يمكن ملاحظة أن الهدف قد تحقق: لا توجد مكالمات إضافية إلى الذاكرة. يتم احتساب القيمة التي يجب كتابتها إلى السجلات في مرحلة الترجمة:
main: mov QWORD PTR Address::value[rip], OFFSET FLAT:arr or QWORD PTR arr[rip], 25 or QWORD PTR arr[rip+8], 4 mov eax, 0 ret
PS:
شكرا للجميع على التعليقات ، شكرا لهم ، لقد عدلت قليلا من النهج. تستطيع أن ترى النسخة الجديدة هنا
- أنواع إزالة المساعدين * _traits ، يمكن حفظ القناع مباشرة في القائمة.
enum struct Enum1 { _0, _1, _2, _3, mask = 0b00111 };
- يتم الآن تسجيل اتصال مع الحجج ليس من خلال الميراث ، والآن هو حقل سجل ثابت
static constexpr auto params = type_pack<Enum1, Enum2>{};
- لأن الاتصال لم يعد من خلال الوراثة ، واضطررت إلى كتابة وظيفة يحتوي على:
template <class T, class...Ts> constexpr auto contains(type_pack<Ts...>, type_identity<T> v) { return ((type_identity<Ts>{} == v) or ...); }
- دون أنواع زائدة اختفت جميع وحدات الماكرو
- أقوم بتمرير الوسائط إلى الطريقة من خلال معلمات القالب لاستخدامها في سياق constexpr
- الآن في الأسلوب المحدد ، يتم فصل منطق constexpr بوضوح عن منطق السجل نفسه
template<auto...args> static void set() { constexpr auto values_for_write = extract(registers, args...); for (auto [value, offset] : values_for_write) { write(Address::value + offset, value); } }
- تخصص وظيفة الاستخراج في constexpr مجموعة من القيم للكتابة على السجلات. يشبه تنفيذه وظيفة المجموعة السابقة ، إلا أنه لا يكتب مباشرة إلى السجل.
- اضطررت إلى إضافة ملف تعريف آخر يحول type_pack إلى صفيف وفقًا لوظيفة lambda.
template <class F, class...Ts> constexpr auto to_array(type_pack<Ts...> pack, F f) { return std::array{f(type_identity<Ts>{})...}; }