لقد جلبت Modern C ++ مجموعة من الميزات التي كانت تفتقر إلى اللغة في السابق. من أجل الحصول على تأثير مماثل بطريقة ما ، تم اختراع العكازات المذهلة لفترة طويلة ، وتتألف بشكل رئيسي من موطئ قدم كبيرة جداً من الأنماط ووحدات الماكرو (غالبًا ما تكون ذاتيًا أيضًا). ولكن الآن ، من وقت لآخر ، تنشأ حاجة إلى فرص لا تزال غير موجودة في اللغة. وبدأنا في إعادة اختراع التصميمات المعقدة من القوالب ووحدات الماكرو مرة أخرى ، وإنشاءها وتحقيق السلوك الذي نحتاجه. هذه مجرد قصة.
خلال النصف الأول من العام الماضي ، كنت بحاجة مرتين إلى القيم التي يمكن استخدامها في معلمات القالب. في الوقت نفسه ، أردت أن يكون لدي أسماء يمكن قراءتها من قبل الإنسان لهذه القيم واستبعاد الحاجة إلى إعلان هذه الأسماء مقدمًا. كانت المهام المحددة التي قمت بحلها مشكلة منفصلة ، ربما سأكتب لاحقًا منشورات منفصلة عنها ، في مكان ما في مركز "البرمجة غير الطبيعية". الآن سأتحدث عن الطريقة التي حل بها هذه المشكلة.
لذلك ، عندما يتعلق الأمر بمعلمات القوالب ، يمكننا استخدام إما قيمة ثابتة أو ثابتة. بالنسبة لمعظم المهام ، هذا أكثر من كافٍ. نريد استخدام معرفات يمكن قراءتها من قبل الإنسان في المعلمات - نعلن عن التركيب أو التعداد أو الثابت واستخدامها. تبدأ المشاكل عندما يتعذر علينا تحديد هذا المعرّف مقدمًا ونريد القيام بذلك في مكانه.
سيكون من الممكن إعلان بنية أو فئة مباشرة في معلمة القالب. سيعمل هذا حتى إذا كان القالب لا يفعل أي شيء مع هذه المعلمة التي تتطلب وصفاً كاملاً للهيكل. بالإضافة إلى ذلك ، لا يمكننا التحكم في مساحة الاسم التي يتم فيها الإعلان عن هذه البنية. وسوف تتحول بدائل القوالب المتطابقة تمامًا إلى رمز مختلف تمامًا إذا كانت هذه الأسطر في فئات أو مساحات أسماء مجاورة.
تحتاج إلى استخدام القيم الحرفية ، ومن بين جميع القيم الحرفية في C ++ ، يمكن تسمية حرفي حرفي وحرف سلسلة فقط للقراءة. لكن حرفي حرف يقتصر على أربعة أحرف (عند استخدام char32_t) ، والحرف سلسلة هو مجموعة من الأحرف ولا يمكن تمرير قيمتها إلى معلمات القالب.
اتضح نوعا من الحلقة المفرغة. يجب عليك إما الإعلان عن شيء مقدمًا ، أو استخدام معرفات غير مريحة. دعونا نحاول الحصول على اللغة التي لم يتم تكييفها. ماذا لو كان لتطبيق ماكرو من شأنه أن يجعل شيئا مناسبا للاستخدام في وسيطات القالب من سلسلة حرفية؟
دعونا نجعل هيكل السلسلة
أولا ، دعونا نجعل الأساس للسلسلة. في C ++ 11 ، ظهرت وسيطات قالب varadic.
نعلن بنية تحتوي على أحرف سلسلة في الوسائط:
template <char... Chars> struct String{};
جيثبإنه يعمل. يمكننا حتى استخدام هذه الخطوط على الفور مثل هذا:
template <class T> struct Foo {}; Foo<String<'B', 'a', 'r'>> foo;
الآن اسحب هذا الخط إلى وقت التشغيل
عظيم لن يكون سيئًا أن تكون قادرًا على الحصول على قيمة هذه السلسلة في وقت التشغيل. فليكن هناك بنية قالب إضافية تستخرج الوسائط من مثل هذه السلسلة وتجعل منها ثابتًا:
template <class T> struct Get; template <char... Chars> struct Get<String<Chars...>> { static constexpr char value[] = { Chars... }; };
هذا يعمل ايضا. نظرًا لأن خطوطنا لا تحتوي على '\ 0' في النهاية ، فأنت بحاجة إلى معالجة هذا الثابت بعناية (من الأفضل ، في رأيي ، إنشاء عرض_السلاسل على الفور باستخدام الثابت والحجم منه في وسيطات المُنشئ). يمكن للمرء فقط إضافة "\ 0" في نهاية المصفوفة ، ولكن هذا ليس ضروريًا لمهامي.
تأكد من أننا قادرون على التعامل مع هذه السلاسل
حسنا ، ماذا يمكنك أن تفعل مع هذه السلاسل؟ على سبيل المثال سلسل:
template <class A, class B> struct Concatenate; template <char... Chars, char... ExtraChars...> struct Concatenate<String<Chars...>, String<ExtraChars...>> { using type = String<Chars..., ExtraChars...>; };
جيثبمن حيث المبدأ ، يمكنك إجراء أكثر أو أقل من أي عملية (لم أجربها ، لأنني لست بحاجة إليها ، لكن يمكنني أن أتخيل فقط كيف يمكنك البحث عن سلسلة فرعية أو حتى استبدال سلسلة فرعية).
الآن لدينا السؤال الرئيسي ، كيفية استخراج الأحرف من سلسلة حرفية في وقت الترجمة ووضعها في وسيطات القوالب.
ارسم البومة ، اكتب ماكرو.
لنبدأ بطريقة لوضع الأحرف في وسيطات القالب واحدًا تلو الآخر:
template <class T, char c> struct PushBackCharacter; template <char... Chars, char c> struct PushBackCharacter<String<Chars...>, c> { using type = String<Chars..., c>; }; template <char... Chars> struct PushBackCharacter<String<Chars...>, '\0'> { using type = String<Chars...>; };
جيثبأستخدم تخصصًا منفصلاً للحرف '\ 0' ، حتى لا أضيفه إلى السلسلة المستخدمة. بالإضافة إلى ذلك ، هذا يبسط إلى حد ما الأجزاء الأخرى من الماكرو.
والخبر السار هو أن سلسلة حرفية يمكن أن تكون معلمة لوظيفة constexpr. سنقوم بكتابة دالة تُرجع حرفًا بفهرس في سلسلة أو '\ 0' إذا كانت السلسلة أقصر من الفهرس (هنا يكون تخصص PushBackCharacter للحرف '\ 0' مفيدًا).
template <size_t N> constexpr char CharAt(const char (&s)[N], size_t i) { return i < N ? s[i] : '\0'; }
في الأساس ، يمكننا بالفعل كتابة شيء مثل هذا:
PushBackCharacter< PushBackCharacter< PushBackCharacter< PushBackCharacter< String<>, CharAt("foo", 0) >::type, CharAt("foo", 1) >::type, CharAt("foo", 2) >::type, CharAt("foo", 3) >::type
نحن نضع مثل هذه القاعدة ، ولكن أكثر أصالة (يمكننا كتابة البرامج النصية لإنشاء رمز) داخل الماكرو لدينا ، وهذا كل شيء!
هناك فارق بسيط. إذا كان عدد الأحرف في السطر أكبر من مستويات التعشيش في الماكرو ، فسيتم ببساطة قطع السطر ولن نلاحظه. الفوضى.
دعنا نجعل بنية أخرى ، والتي لا تحول السلسلة التي تصل إليها بأي شكل من الأشكال ، لكنها تفعل static_assert بحيث لا يتجاوز طولها ثابتًا:
#define _NUMBER_TO_STR(n) #n #define NUMBER_TO_STR(n) _NUMBER_TO_STR(n) template <class String, size_t size> struct LiteralSizeLimiter { using type = String; static_assert(size <= MAX_META_STRING_LITERAL_SIZE, "at most " NUMBER_TO_STR(MAX_META_STRING_LITERAL_SIZE) " characters allowed for constexpr string literal"); }; #undef NUMBER_TO_STR #undef _NUMBER_TO_STR
حسنًا ، سيبدو الماكرو بشيء من هذا القبيل:
#define MAX_META_STRING_LITERAL_SIZE 256 #define STR(literal) \ ::LiteralSizeLimiter< \ ::PushBackCharacter< \ ... \ ::PushBackCharacter< \ ::String<> \ , ::CharAt(literal, 0)>::type \ ... \ , ::CharAt(literal, 255)>::type \ , sizeof(literal) - 1>::type
جيثباتضح
template <class S> std::string_view GetContent() { return std::string_view(Get<S>::value, sizeof(Get<S>::value)); } std::cout << GetContent<STR("Hello Habr!")>() << std::endl;
يمكن
العثور على التطبيق الذي حصلت
عليه على جيثب .
سيكون من المثير للاهتمام بالنسبة لي أن أسمع عن التطبيقات الممكنة لهذه الآلية ، مختلفة عن تلك التي توصلت إليها.