في عملية الانتقال إلى اللقب الذي طال انتظاره لـ
Lead Senior C ++ Over-Engineer ، قررت العام الماضي إعادة كتابة اللعبة التي أقوم بتطويرها خلال ساعات العمل (Candy Crush Saga) ، باستخدام جوهر C ++ الحديث (C ++ 17). وهكذا ولدت
Meta Crush Saga :
لعبة تعمل في مرحلة التجميع . لقد ألهمتني لعبة Nibbler الخاصة بـ Matt Birner ، والتي استخدمت مخططًا بيئيًا نقيًا على قوالب لإعادة إنشاء Snake الشهير مع Nokia 3310.
"ما نوع
اللعبة التي يتم تشغيلها في مرحلة التجميع ؟" ، "كيف تبدو؟" ، "ما هي وظيفة
C ++ 17 التي استخدمتها في هذا المشروع؟" ، "ماذا تعلمت؟" - قد تتبادر إلى ذهنك أسئلة مشابهة. للإجابة عنها ، عليك إما قراءة المنشور بأكمله ، أو تحمل كسلك الداخلي ومشاهدة نسخة فيديو من المنشور - تقريري من
حدث Meetup في ستوكهولم:
ملحوظة: من أجل صحتك العقلية ولأنه
يخطئ الإنسان ،
ترد بعض الحقائق البديلة في هذه المقالة.
لعبة تعمل في وقت الترجمة؟
أعتقد أنه من أجل فهم ما أعنيه بـ "مفهوم"
لعبة يتم تنفيذها في مرحلة التجميع ، تحتاج إلى مقارنة دورة حياة هذه اللعبة بدورة حياة لعبة عادية.
دورة حياة اللعبة العادية:
بصفتك مطورًا منتظمًا للألعاب ذات الحياة العادية ، وتعمل في وظيفة عادية بمستوى عادي من الصحة العقلية ، تبدأ عادةً بكتابة
منطق اللعبة بلغتك المفضلة (في C ++ ، بالطبع!) ، ثم تشغيل
المترجم لتحويل هذا ، غالبًا مثل السباغيتي المنطق في
ملف قابل للتنفيذ . بعد النقر المزدوج على
الملف القابل للتنفيذ (أو البدء من وحدة التحكم) ، ينتج نظام التشغيل
عملية . ستقوم هذه
العملية بتنفيذ
منطق اللعبة ، الذي يتكون من
دورة لعبة في 99.42٪ من الوقت.
تقوم دورة اللعبة بتحديث حالة اللعبة وفقًا لقواعد معينة
وإدخال المستخدم ،
وتجعل الحالة المحسوبة الجديدة للعبة بالبكسل ، مرارًا وتكرارًا ومرة أخرى.
دورة حياة اللعبة التي تعمل أثناء عملية التجميع:
بصفتك مهندسًا فائقًا ينشئ لعبة تجميع جديدة رائعة ، ما زلت تستخدم لغتك المفضلة (لا تزال لغة C ++ بالطبع!) لكتابة
منطق اللعبة . ثم ، كما كان من قبل ، تستمر
مرحلة التجميع ، ولكن هناك تطور مؤامرة: تقوم
بتنفيذ منطق اللعبة في مرحلة التجميع. يمكنك تسميته "تنفيذ" (تجميع). وهنا يعتبر C ++ مفيدًا جدًا ؛ يحتوي على ميزات مثل
Template Meta Programming (TMP) و
constexpr التي تسمح لك بإجراء
العمليات الحسابية في
مرحلة التجميع . سنأخذ في الاعتبار لاحقًا الوظيفة التي يمكن استخدامها لهذا الغرض. نظرًا لأننا في هذه المرحلة ننفذ
منطق اللعبة ، فإننا نحتاج أيضًا في هذه اللحظة إلى إدخال إدخال
اللاعب . من الواضح أن المترجم سيواصل إنشاء
ملف تنفيذي عند الإخراج. ما يمكن استخدامه؟ لن يحتوي الملف القابل للتنفيذ على
حلقة اللعبة بعد الآن ، ولكن له مهمة بسيطة للغاية: عرض
حالة محسوبة جديدة. لنطلق على هذا
الملف القابل للتنفيذ جهاز العرض ، ويتم تقديم
البيانات التي يعرضها . في عرضنا
، لن يتم احتواء تأثيرات الجسيمات الجميلة ولا ظلال الغلق المحيط ، بل سيكون ASCII. يعد
عرض ASCII
للحالة المحسوبة الجديدة خاصية ملائمة يمكن إظهارها بسهولة للاعب ، ولكن بالإضافة إلى ذلك ، نقوم بنسخها إلى ملف نصي. لماذا ملف نصي؟ من الواضح ، لأنه يمكن دمجه بطريقة ما مع
الرمز وإعادة تنفيذ جميع الخطوات السابقة ، وبالتالي الحصول على
حلقة .
كما يمكنك أن تفهم بالفعل ، فإن اللعبة التي يتم
تنفيذها أثناء عملية التجميع تتكون من
دورة لعبة يكون فيها كل
إطار للعبة
مرحلة تجميع . تحسب كل
مرحلة من مراحل التجميع حالة جديدة للعبة ، والتي يمكن عرضها للاعب وإدراجها في
الإطار /
المرحلة التالية من التجميع .
يمكنك التفكير في هذا الرسم البياني الرائع بقدر ما تريد حتى تفهم ما كتبته للتو:
قبل أن نبدأ في تفاصيل تنفيذ هذه الدورة ، أنا متأكد من أنك تريد أن تسألني السؤال الوحيد ...
"لماذا تهتم بهذا؟"
هل تعتقد حقاً أن تدمير لغة برمجة C ++ الخاصة بي هو سؤال أساسي؟ نعم ، من أجل لا شيء في الحياة!
- الشيء الأول والأهم هو أن اللعبة التي يتم تنفيذها في مرحلة التجميع سيكون لها سرعة مذهلة في التنفيذ ، لأن الجزء الأكبر من الحسابات يتم إجراؤه في مرحلة التجميع . سرعة وقت التشغيل هي مفتاح نجاح لعبة AAA مع رسومات ASCII!
- أنت تقلل من احتمالية ظهور بعض القشريات في مستودعك وتطلب منك إعادة كتابة اللعبة في Rust . سوف ينهار خطابه المُعد جيدًا بمجرد أن تشرح له أن المؤشر غير الصالح لا يمكن أن يوجد في وقت الترجمة. يمكن لمبرمجي Haskell الواثقين من أنفسهم تأكيد سلامة النوع في التعليمات البرمجية الخاصة بك.
- ستكسب احترام مملكة Javascript hipster ، التي يمكن أن يحكم فيها أي إطار معاد تصميمه مع متلازمة NIH القوية ، شريطة أن يأتي باسم رائع.
- اعتاد صديق لي أن يقول أنه يمكن استخدام أي سطر من كود بيرل بحكم الواقع ككلمة مرور قوية جدًا. أنا متأكد من أنه لم يحاول أبدًا إنشاء كلمات مرور من وقت الترجمة C ++ .
كيف؟ هل أنت راضٍ عن إجاباتي؟ ثم ربما يجب أن يكون سؤالك: "كيف يمكنك حتى القيام بذلك؟"
في الواقع ، أردت حقًا تجربة الوظيفة المضافة في
C ++ 17 . يقصد به عدد غير قليل من الميزات لزيادة فعالية اللغة ، وكذلك ل metaprogramming (بشكل رئيسي constexpr). اعتقدت أنه بدلاً من كتابة أمثلة برموز صغيرة سيكون من المثير للاهتمام تحويل كل هذا إلى لعبة. تعد مشاريع الحيوانات الأليفة طريقة رائعة لتعلم المفاهيم التي لا يتعين عليك استخدامها كثيرًا. تثبت القدرة على تنفيذ منطق اللعبة الأساسي في وقت الترجمة مرة أخرى أن القوالب و constepxr هي مجموعات فرعية
كاملة من Turing للغة C ++.
ميتا كراش ساجا لعبة مراجعة
لعبة المباراة 3:
Meta Crush Saga هي
لعبة انضمام للبلاط مماثلة لـ
Bejeweled و
Candy Crush Saga . جوهر قواعد اللعبة هو ربط ثلاثة مربعات بنفس النمط للحصول على نقاط. فيما يلي نظرة سريعة على
حالة اللعبة التي "ألقيت بها" (الإغراق في ASCII سهل للغاية):
R "(
ملحمة ميتا سحق
------------------------
| |
| RBGBBYGR |
| |
| |
| YYGRBGBR |
| |
| |
| RBYRGRYG |
| |
| |
| RYBY (R) YGY |
| |
| |
| BGYRYGGR |
| |
| |
| RYBGYBBG |
| |
------------------------
> الدرجة: 9009
> التحركات: 27
) "
إن طريقة اللعب في لعبة Match-3 نفسها ليست مثيرة للاهتمام بشكل خاص ، ولكن ماذا عن الهندسة المعمارية التي تعمل عليها جميعًا؟ لكي تفهمها ، سأحاول شرح كل جزء من دورة حياة لعبة
الترجمة هذه
من حيث التعليمات البرمجية.
حقن لعبة اللعبة:
إذا كنت من محبي C ++ المتحمسين أو المتحمسين ، فقد تكون لاحظت أن تفريغ حالة اللعبة السابقة يبدأ بالنمط التالي:
R "( . في الواقع ، هذه
سلسلة حرفية من C ++ 11 الخام ، مما يعني أنني لست بحاجة إلى الهروب من أحرف خاصة ، على سبيل المثال ،
الترجمة سلاسل : يتم تخزين السلسلة الحرفية الأولية في ملف يسمى
current_state.txt .
كيف نحقن هذه الحالة الحالية للعبة في حالة التجميع؟ دعنا نضيفه فقط إلى حلقة المدخلات!
سواء كان ملف
.txt أو ملف
.h ،
فسيعمل التوجيه المتضمن من المعالج C بنفس الطريقة: فهو ينسخ محتويات الملف إلى موقعه. هنا أقوم بنسخ السلسلة الأولية لحالة اللعبة في ascii إلى متغير يسمى
game_state_string .
لاحظ أن
ملف الرأس
loop_inputs.hpp يقوم أيضًا بتوسيع إدخال لوحة المفاتيح إلى خطوة الإطار /
التحويل البرمجي الحالية. على عكس حالة اللعبة ، فإن حالة لوحة المفاتيح صغيرة جدًا ويمكن الحصول عليها بسهولة كتعريف للمعالج المسبق.
حساب حالة جديدة في وقت الترجمة:
الآن بعد أن جمعنا بيانات كافية ، يمكننا حساب الحالة الجديدة. أخيرًا ، وصلنا إلى النقطة التي نحتاج فيها إلى كتابة ملف
main.cpp :
غريب ، لكن رمز C ++ هذا لا يبدو مربكًا للغاية بالنظر إلى ما يفعله. يتم تنفيذ معظم التعليمات البرمجية في مرحلة التجميع ، ومع ذلك ، فإنه يتبع نماذج البرمجة الإجرائية OOP التقليدية. فقط السطر الأخير - تقديم - هو عقبة من أجل إجراء الحسابات بالكامل في وقت الترجمة. كما سنرى أدناه ، بإلقاء القليل من constexpr في الأماكن المناسبة ، يمكننا الحصول على برمجة تخطيطية أنيقة جدًا في C ++ 17. أجد السعادة الحرية التي يمنحها لنا C ++ عندما يتعلق الأمر بالتنفيذ المختلط في وقت التشغيل والتجميع.
ستلاحظ أيضًا أن هذا الرمز ينفذ إطارًا واحدًا فقط ، ولا توجد
حلقة لعبة . دعنا نحل هذه المشكلة!
نلصق كل شيء معًا:
إذا شعرت بالاشمئزاز من حيلتي باستخدام
C ++ ، آمل ألا تمانع في رؤية مهاراتي في
Bash . في الواقع ،
حلقة اللعبة الخاصة بي ليست أكثر من
نص باش يتم تجميعه باستمرار.
في الواقع ، كنت أواجه بعض المشاكل في الحصول على إدخال لوحة المفاتيح من وحدة التحكم. في البداية ، كنت أرغب في موازاة التجميع. بعد العديد من التجربة والخطأ ، تمكنت من الحصول على شيء أكثر أو أقل بالعمل مع أمر
read
من
Bash . لا أجرؤ أبدًا على محاربة الساحر
باش في مبارزة - هذه اللغة شريرة جدًا!
لذا ، يجب أن أعترف أنه من أجل إدارة دورة اللعبة كان علي أن ألجأ إلى لغة أخرى. على الرغم من عدم وجود شيء يمنعني من كتابة هذا الجزء من التعليمات البرمجية في C ++. بالإضافة إلى ذلك ، هذا لا ينفي حقيقة أن 90٪ من منطق لعبتي يتم تنفيذها داخل فريق تجميع
g ++ ، وهو أمر رائع جدًا!
لعبة صغيرة لإراحة عينيك:
الآن بعد أن واجهت عذاب شرح هندسة اللعبة ، حان الوقت للوحات لافتة للنظر:
هذه الصورة المنقطة هي عبارة عن سجل لكيفية لعب
Meta Crush Saga . كما ترى ، تعمل اللعبة بسلاسة كافية للعب في الوقت الحقيقي. من الواضح أنها ليست جذابة للغاية لدرجة أنني أستطيع بث Twitch لها وأصبح Pewdiepie الجديدة ، لكنها تعمل!
أحد الجوانب الممتعة لتخزين
حالة اللعبة في ملف
.txt هو القدرة على الغش أو اختبار الحالات القصوى بشكل مريح للغاية.
الآن بعد أن تعرفت على الهندسة المعمارية لفترة وجيزة ، سوف نتعمق في وظيفة C ++ 17 المستخدمة في هذا المشروع. لن أفكر في منطق اللعبة بالتفصيل ، لأنه يشير حصريًا إلى Match-3 ، ولكن بدلاً من ذلك سأتحدث عن جوانب C ++ التي يمكن تطبيقها في مشاريع أخرى.
برامجي التعليمية حول C ++ 17:
على عكس C ++ 14 ، الذي يحتوي بشكل أساسي على إصلاحات طفيفة ، يمكن لمعيار C ++ 17 الجديد أن يقدم لنا الكثير. كانت هناك آمال في أن تظهر أخيرا الميزات التي طال انتظارها (الوحدات والنماذج والمفاهيم ...) ، ولكن ... بشكل عام ... لم تظهر ؛ أزعج الكثير منا. ولكن بعد إزالة الحداد ، وجدنا العديد من الكنوز الصغيرة غير المتوقعة التي سقطت على الرغم من ذلك في المعيار.
أجرؤ على القول أن الأطفال الذين يحبون الرسم البياني مدللون للغاية هذا العام! تتيح لك التغييرات والإضافات الطفيفة المنفصلة على اللغة الآن كتابة التعليمات البرمجية التي تعمل كثيرًا في وقت الترجمة وبعدها ، في وقت التشغيل.
Constepxr في جميع المجالات:
كما تنبأ بن دين وجايسون تورنر في
تقريرهما عن C ++ 14 ، يسمح لك C ++ بتحسين تجميع القيم بسرعة في وقت الترجمة باستخدام الكلمة الأساسية
القادرة على كل شيء. من خلال تحديد موقع هذه الكلمة الرئيسية في الأماكن الصحيحة ، يمكنك إخبار المترجم أن التعبير ثابت
ويمكن تقييمه مباشرة في وقت الترجمة. في
C ++ 11 ، يمكننا بالفعل كتابة هذا الرمز:
constexpr int factorial(int n)
على الرغم من أن الكلمة الأساسية
constexpr قوية جدًا ، إلا أنها تحتوي على عدد قليل جدًا من قيود الاستخدام ، مما يجعل من الصعب كتابة رمز معبر بهذه الطريقة.
قلل
C ++ 14 بشكل كبير من متطلبات
constexpr وأصبح أكثر طبيعية للاستخدام. يمكن إعادة كتابة دالة عاملنا السابقة على النحو التالي:
constexpr int factorial(int n) { if (n <= 1) { return 1; } return n * factorial(n - 1); }
تخلص
C ++ 14 من القاعدة القائلة بأن
دالة constexpr يجب أن تتكون من بيان إرجاع واحد فقط ، مما أجبرنا على استخدام
عامل التشغيل الثلاثي كعنصر البناء الرئيسي. الآن يوفر
C ++ 17 المزيد من تطبيقات الكلمات الأساسية
constexpr التي يمكننا استكشافها!
التفرع في وقت الترجمة:
هل سبق لك في موقف تحتاج فيه إلى الحصول على سلوك مختلف اعتمادًا على معلمة القالب التي تتعامل معها؟ لنفترض أننا بحاجة إلى
serialize
دالة ذات معلمات ، والتي
.serialize()
إذا كان الكائن يوفرها ، وإلا فسوف تلجأ إلى استدعاء
to_string
أجلها. كما هو موضح بمزيد من التفصيل في هذا
المنشور حول SFINAE ، على الأرجح سيكون عليك كتابة مثل هذا الرمز الغريب:
template <class T> std::enable_if_t<has_serialize_v<T>, std::string> serialize(const T& obj) { return obj.serialize(); } template <class T> std::enable_if_t<!has_serialize_v<T>, std::string> serialize(const T& obj) { return std::to_string(obj); }
فقط في الحلم ، يمكنك إعادة كتابة هذه
الخدعة القبيحة
من خدعة SFINAE إلى
C ++ 14 في مثل هذا الرمز الرائع:
لسوء الحظ ، عندما استيقظت وبدأت في كتابة
كود C ++ 14 حقيقي ، أرسل مترجمك رسالة غير سارة حول استدعاء
serialize(42);
. وأوضح أن
obj
النوع
int
ليس له وظيفة عضو
serialize()
. بغض النظر عن الطريقة التي تثير غضبك ، فإن المترجم على حق! باستخدام هذا الرمز ، سيحاول دائمًا ترجمة كلا الفرعين -
return obj.serialize();
و
return std::to_string(obj);
. بالنسبة إلى الفرع
return obj.serialize();
قد يتبين أنه نوع من الكود الميت ، لأن
has_serialize(obj)
سيعرض دائمًا
false
، ولكن لا يزال يتعين على المترجم
has_serialize(obj)
.
كما خمنت على الأرجح ، فإن
C ++ 17 ينقذنا من مثل هذا الموقف غير السار ، لأنه جعل من الممكن إضافة
constexpr بعد عبارة if إلى "القوة" المتفرعة في وقت الترجمة وتجاهل الإنشاءات غير المستخدمة:
من الواضح أن هذا تحسن كبير
عن خدعة SFINAE التي كان علينا تطبيقها من قبل. بعد ذلك ، بدأنا في الحصول على نفس الإدمان مثل Ben و Jason - بدأنا في استخدام
constexpr في كل مكان ودائمًا. للأسف ، هناك مكان آخر
تتناسب فيه الكلمة الأساسية
constexpr ، ولكن لم يتم استخدامها بعد:
معلمات constexpr .
معلمات Constexpr:
إذا كنت حذرًا ، فقد تلاحظ وجود نمط غريب في مثال الرمز السابق. أنا أتحدث عن مدخلات حلقة:
لماذا يتم تغليف متغير
game_state_string في كونكستسبر لامدا؟ لماذا لا تجعلها
متغير عالمي متغير ؟
كنت أرغب في تمرير هذا المتغير ومحتوياته في بعض الوظائف. على سبيل المثال ، تحتاج إلى تمريرها إلى
parse_board الخاص بي واستخدامها في بعض التعبيرات الثابتة:
constexpr int parse_board_size(const char* game_state_string); constexpr auto parse_board(const char* game_state_string) { std::array<GemType, parse_board_size(game_state_string)> board{};
إذا ذهبنا بهذه الطريقة ، فإن المترجم
المتذمر سيشتكي من أن معلمة
game_state_string ليست تعبيرًا ثابتًا. عندما أقوم بإنشاء مصفوفة بلاطات ، أحتاج إلى حساب سعتها الثابتة مباشرة (لا يمكننا استخدام المتجهات في وقت الترجمة لأنها تتطلب تخصيص ذاكرة) وتمريرها كوسيطة إلى قالب القيمة في
std :: array . لذلك ، يجب أن يكون تعبير
parse_board_size (game_state_string) تعبيرًا ثابتًا. على الرغم من أنه
تم وضع علامة صريحة على
parse_board_size على أنها
constexpr ، فإن
game_state_string لا يمكن ولا يمكن! في هذه الحالة ، هناك قاعدتان تتداخلان معنا:
- وسيطات دالة constexpr ليست constexpr!
- ولا يمكننا إضافة constexpr أمامهم!
كل هذا يتلخص في حقيقة أن
وظائف constexpr يجب أن تكون قابلة للتطبيق في كل من وقت التشغيل ووقت التجميع. بافتراض وجود
معلمات constexpr ، لن يسمح ذلك باستخدامها في وقت التشغيل.
لحسن الحظ ، هناك طريقة لتسوية هذه المشكلة. بدلاً من قبول القيمة كمعلمة عادية للدالة ، يمكننا تغليف هذه القيمة في نوع وتمرير هذا النوع كمعلمة قالب:
template <class GameStringType> constexpr auto parse_board(GameStringType&&) { std::array<CellType, parse_board_size(GameStringType::value())> board{};
في مثال التعليمات البرمجية هذا ، أقوم بإنشاء نوع هيكلي
GameString له
قيمة كونستكسبر الدالة العضو الثابتة
() التي ترجع السلسلة الحرفية التي أريد تمريرها إلى
parse_board . في
parse_board ، أحصل على هذا النوع من خلال
معلمة قالب
GameStringType ، باستخدام قواعد استخراج وسائط القالب. باستخدام
GameStringType ، نظرًا لأن
القيمة () هي constexpr ، يمكنني ببساطة استدعاء
قيمة دالة العضو الثابتة
() في الوقت المناسب للحصول على سلسلة حرفية حتى في الأماكن التي تتطلب تعابير ثابتة.
تمكنا من تغليف
الحرف من أجل تمريره بطريقة أو بأخرى إلى
parse_board باستخدام constexpr. ومع ذلك ، من المزعج للغاية أن تحتاج إلى تحديد نوع جديد في كل مرة تحتاج فيها إلى إرسال
حرف تحليلي جديد: "... شيء 1 ..." ، "... شيء 2 ...". لحل هذه المشكلة في
C ++ 11 ، يمكنك تطبيق بعض العنونة القبيحة وغير المباشرة باستخدام اتحاد مجهول ولامبدا. شرح مايكل بارك هذا الموضوع جيدًا في
إحدى مشاركاته .
في
C ++ 17 ، الوضع أفضل. إذا قمنا بإدراج متطلبات تمرير السلسلة الحرفية لدينا ، نحصل على ما يلي:
- تم إنشاء الوظيفة
- هذا هو constexpr
- باسم فريد أو مجهول
يجب أن تعطيك هذه المتطلبات تلميحًا. ما نحتاجه هو
كونستيكسبر لامدا ! وفي
C ++ 17 ، أضافوا بشكل طبيعي تمامًا القدرة على استخدام
الكلمة الأساسية constexpr لوظائف لامدا. يمكننا إعادة كتابة نموذج التعليمات البرمجية على النحو التالي:
template <class LambdaType> constexpr auto parse_board(LambdaType&& get_game_state_string) { std::array<CellType, parse_board_size(get_game_state_string())> board{};
صدقوني ، يبدو هذا بالفعل أكثر ملاءمة من القرصنة السابقة في
C ++ 11 باستخدام وحدات الماكرو. لقد اكتشفت هذه الحيلة الرائعة بفضل
Bjorn Fahler ، عضو مجموعة C ++ mitap التي أشارك فيها. اقرأ المزيد عن هذه الحيلة في
مدونته . تجدر الإشارة أيضًا إلى أن الكلمة الأساسية
constexpr في الواقع اختيارية في هذه الحالة: جميع
lambdas التي لديها القدرة على أن تصبح
constexpr هي بشكل افتراضي. إن إضافة
constexpr بشكل صريح هو توقيع يبسط عملية استكشاف الأخطاء وإصلاحها لدينا.
الآن يجب أن تفهم لماذا أجبرت على استخدام
كونكستسبر لامدا لتمرير سلسلة تمثل حالة اللعبة. انظر إلى وظيفة لامدا هذه ، وسيكون لديك سؤال آخر مرة أخرى. ما هو نوع
constexpr_string الذي أستخدمه أيضًا للف المخزون الحرفي؟
constexpr_string و constexpr_string_view:
عند العمل مع السلاسل ، يجب ألا تقوم بمعالجتها بأسلوب C. تحتاج إلى نسيان كل هذه الخوارزميات المزعجة التي تؤدي التكرارات الأولية وتحقق من عدم اكتمالها! البديل الذي تقدمه
C ++ هو
std :: string والقوية وخوارزميات STL . لسوء الحظ ، قد يتطلب الأمر
std :: string تخصيص ذاكرة على الكومة (حتى مع Small String Optimization) لتخزين محتوياته. مرة أخرى بمعيار أو معيارين ، يمكننا استخدام
constexpr new / delete أو يمكننا تمرير
مخصصات constexpr إلى
std :: string ، لكننا الآن بحاجة إلى إيجاد حل آخر.
كان طريقي هو كتابة فئة
constexpr_string بسعة ثابتة. يتم تمرير هذه السعة كمعلمة لقالب القيمة. هنا لمحة موجزة عن صفي:
template <std::size_t N>
تسعى فئة
constexpr_string إلى تقليد واجهة
std :: string في أقرب وقت ممكن (للعمليات التي
أحتاجها ): يمكننا طلب
التكرارات من البداية والنهاية ، والحصول على
الحجم (الحجم) ، والوصول إلى
البيانات (البيانات) ،
وحذف (محو) جزء منها ، والحصول على سلسلة فرعية باستخدام
substr وهلم جرا. هذا
يجعل من السهل جدًا تحويل جزء من التعليمات البرمجية من
std :: string إلى
constexpr_string . قد تتساءل عما يحدث عندما نحتاج إلى استخدام العمليات التي تتطلب عادةً إبراز في
std :: string . في مثل هذه الحالات ، اضطررت إلى تحويلها إلى
عمليات غير قابلة للتغيير تخلق
مثيلًا جديدًا من
constexpr_string .
دعونا نلقي نظرة على عملية
الإلحاق :
template <std::size_t N>
لا تحتاج إلى الحصول على جائزة Fields لتفترض أنه إذا كان لدينا سلسلة من الحجم
N وسلسلة من الحجم
M ، فستكون سلسلة الحجم
N + M كافية لتخزين تسلسلها. قد نهدر جزءًا من "مستودع وقت الترجمة" ، نظرًا لأن كلا الخطين قد لا يستخدمان السعة الكاملة ، ولكن هذا سعر صغير جدًا للراحة. من الواضح أنني كتبت أيضًا نسخة مكررة من
std :: string_view ، والتي تسمى
constexpr_string_view .
مع هاتين الفئتين ، كنت على استعداد لكتابة كود أنيق لتحليل
حالة لعبتي. فكر في شيء مثل هذا:
constexpr auto game_state = constexpr_string(“...something...”);
كان من السهل جدًا التكرار حول الجواهر في الملعب - بالمناسبة ، هل لاحظت ميزة أخرى ثمينة لـ
C ++ 17 في مثال الرمز هذا؟
نعم فعلا! لم أكن مضطرًا إلى تحديد سعة
constexpr_string بشكل صريح عند إنشائه. في السابق ، عند استخدام
قالب فئة ، كان علينا الإشارة بوضوح إلى حججه. لتجنب هذه
الآلام ، نقوم بإنشاء
دوال make_xxx لأنه يمكن تتبع معلمات
قوالب الدوال . شاهد كيف يغير
وسيطات قالب الفصل الدراسي حياتنا للأفضل:
template <int N> struct constexpr_string { constexpr_string(const char(&a)[N]) {}
في بعض المواقف الصعبة ، ستحتاج إلى مساعدة المترجم على حساب الحجج بشكل صحيح. إذا واجهت مثل هذه المشكلة ، فقم بدراسة
أدلة حسابات الوسيط المعرفة من قبل المستخدم .
طعام مجاني من المحكمة الخاصة بلبنان:
حسنًا ، يمكننا دائمًا إعادة كتابة كل شيء بمفردنا. ولكن ربما قام أعضاء اللجنة بإعداد شيء لنا بسخاء في المكتبة القياسية؟
أنواع المساعد الجديدة:
في
C ++ 17 ، تتم إضافة
std :: variant و
std :: اختياري إلى أنواع القاموس القياسية ، بناءً على
constexpr . الأول مثير للاهتمام للغاية لأنه يسمح لنا بالتعبير عن ارتباطات آمنة للنوع ، ولكن التنفيذ في
مكتبة libstdc ++ مع
GCC 7.2 يواجه مشاكل عند استخدام التعبيرات المستمرة. لذلك ، تخليت عن فكرة إضافة
std :: variant إلى التعليمات البرمجية الخاصة بي واستخدام
std :: اختياري فقط.
مع النوع T ، يسمح لنا النوع std :: اختياري بإنشاء نوع جديد std :: اختياري <T> ، والذي يمكن أن يحتوي على قيمة من النوع T أو لا شيء. هذا يشبه إلى حد كبير الأنواع ذات المغزى التي تسمح بقيمة غير محددة في C # . دعنا نلقي نظرة على الدالة find_in_board ، التي تُرجع موضع العنصر الأول في حقل يؤكد صحة المسند. قد لا يكون هناك مثل هذا العنصر في الميدان. للتعامل مع هذا الموقف ، يجب أن يكون نوع المركز اختياريًا: template <class Predicate> constexpr std::optional<std::pair<int, int>> find_in_board(GameBoard&& g, Predicate&& p) { for (auto item : g.items()) { if (p(item)) { return {item.x, item.y}; }
في السابق ، كان علينا اللجوء إلى دلالات المؤشرات ، أو إضافة "حالة فارغة" مباشرة إلى نوع الموضع ، أو إرجاع قيمة منطقية وأخذ معلمة الإخراج . من المسلم به أن هذا كان محرجا جدا!كما تلقت بعض الأنواع الموجودة مسبقًا دعم كونستركسبر : tuple و pair . لن أشرح بالتفصيل استخدامها ، لأن الكثير قد كتب عنها بالفعل ، لكنني سأشارك إحدى خيباتي. أضافت اللجنة السكر النحوي إلى المعيار لاستخراج القيم الواردة في tuple أو الزوج . هذا النوع الجديد من التصريحات يسمى الربط المنظم، يستخدم الأقواس لتحديد المتغيرات لتخزين tuple tuple أو الزوج : std::pair<int, int> foo() { return {42, 1337}; } auto [x, y] = foo();
ذكي جدا! ولكن من المؤسف أن أعضاء اللجنة [لم يستطيعوا ، لم يرغبوا ، لم يجدوا الوقت ، نسيوا ] لجعلهم ودودين مع constexpr . أتوقع شيئا مثل هذا: constexpr auto [x, y] = foo();
لدينا الآن حاويات معقدة وأنواع مساعدة ، ولكن كيف نتعامل معها بشكل ملائم؟الخوارزميات:
إن ترقية حاوية لمعالجة constexpr مهمة رتيبة للغاية. بالمقارنة مع ذلك ، فإن نقل كونستكسبر إلى خوارزميات غير معدلة يبدو بسيطا بما فيه الكفاية. ولكن من الغريب إلى حد ما أنه في C ++ 17 لم نشهد تقدمًا في هذا المجال ، فسيظهر فقط في C ++ 20 . على سبيل المثال ، لم تحصل الخوارزميات std :: find الرائعة على توقيعات constexpr .لكن لا تخف! كما أوضح بين وجيسون ، يمكنك بسهولة تحويل الخوارزمية إلى constexpr ببساطة عن طريق نسخ التنفيذ الحالي (ولكن لا تنس حقوق النشر) ؛ cppreference أمر جيد. سيداتي وسادتي ، أوجه انتباهكمكونستيكسبر ستد :: تجد : template<class InputIt, class T> constexpr InputIt find(InputIt first, InputIt last, const T& value) // ^ !!! constexpr. { for (; first != last; ++first) { if (*first == value) { return first; } } return last; }
أستطيع بالفعل أن أسمع من المدرجات صرخات محبي التحسين! نعم ، مجرد إضافة كونستكسبر أمام نموذج الكود الذي تم توفيره بواسطة cppreference قد لا يعطينا سرعة مثالية في وقت التشغيل . ولكن إذا كان علينا تحسين هذه الخوارزمية ، فستكون هناك حاجة للسرعة في وقت الترجمة . بقدر ما أعرف ، عندما يتعلق الأمر بسرعة التجميع ، فإن الحلول البسيطة هي الأفضل.السرعة والأخطاء:
يجب على مطوري أي لعبة AAA الاستثمار في حل هذه المشاكل ، أليس كذلك؟السرعة:
عندما تمكنت من إنشاء نسخة نصف عمل من Meta Crush Saga ، أصبح العمل أكثر سلاسة. في الواقع ، تمكنت من تحقيق ما يزيد قليلاً عن 3 إطارات في الثانية (إطارات في الثانية) على جهاز الكمبيوتر المحمول القديم الخاص بي مع زيادة سرعة i5 إلى 1.80 جيجاهرتز (التردد مهم في هذه الحالة). كما هو الحال في أي مشروع ، أدركت بسرعة أن الشفرة المكتوبة سابقًا كانت مثيرة للاشمئزاز ، وبدأت في إعادة كتابة تحليل حالة اللعبة باستخدام constexpr_string والخوارزميات القياسية. على الرغم من أن هذا جعل الرمز أكثر ملاءمة للاحتفاظ به ، إلا أن التغييرات أثرت بشكل خطير على السرعة ؛ السقف الجديد 0.5 FPS .على الرغم من القول القديم حول C ++ ، فإن "الملخصات ذات الرأس الصفري" لا تنطبق على حسابات وقت الترجمة. هذا أمر منطقي تمامًا إذا اعتبرنا المترجم مترجمًا لبعض "كود وقت الترجمة". لا تزال التحسينات لمختلف المترجمين ممكنة ، ولكن هناك أيضًا فرص نمو لنا ، مؤلفو مثل هذا الرمز. فيما يلي قائمة غير مكتملة من الملاحظات والنصائح التي وجدتها ، ربما خاصة بدول مجلس التعاون الخليجي:- تعمل صفائف C أفضل بكثير من std :: array . std :: array هو القليل من مستحضرات التجميل الحديثة C ++ فوق مجموعة C-style ويجب عليك دفع ثمن استخدامها في مثل هذه الظروف.
- , ( ) . , , , . : , , , , ( ) , .
- , . , .
- . GCC. , «».
:
في كثير من الأحيان أفرز المترجم الخاص بي أخطاء تجميع رهيبة ، وعانى منطق الكود الخاص بي. ولكن كيف تجد المكان الذي يختبئ فيه الخطأ؟ دون المصحح و printf كل شيء يصبح أكثر صعوبة. إذا كانت "لحية المبرمج" المجازية الخاصة بك لم تنمو حتى الآن على ركبتيها (كل من اللحية المجازية والحقيقية لا تزال بعيدة عن هذه التوقعات) ، فربما لا يكون لديك دافع لاستخدام templight أو تصحيح المترجم.سيكون صديقنا الأول static_assert ، مما يمنحنا الفرصة للتحقق من القيمة المنطقية لوقت الترجمة. سيكون صديقنا الثاني عبارة عن ماكرو يتيح لـ constexpr ويعطله قدر الإمكان: #define CONSTEXPR constexpr
باستخدام هذا الماكرو ، يمكننا جعل المنطق يعمل في وقت التشغيل ، مما يعني أنه يمكننا إرفاق مصحح أخطاء به.Meta Crush Saga II - جاهد من أجل اللعب بالكامل في وقت التشغيل:
من الواضح أن Meta Crush Saga لن تفوز بجوائز The Game Awards هذا العام . لديها إمكانات كبيرة ، ولكن لم يتم تنفيذ طريقة اللعب بالكامل في وقت الترجمة . هذا يمكن أن يزعج اللاعبين المتشددين ... لا يمكنني التخلص من نص bash ما لم يضيف أحدهم إدخال لوحة المفاتيح والمنطق غير النظيف في مرحلة التجميع (وهذا جنون صريح!). لكني أعتقد أنه في يوم ما سأكون قادرًا على التخلي تمامًا عن الملف القابل للتنفيذ الخاص بالعارض وتقديم حالة اللعبة في وقت الترجمة :قام الرجل المجنون الذي يحمل الاسم المستعار ساراز بتوسيع دول مجلس التعاون الخليجي لإضافة بنية static_print إلى اللغة . يجب أن يأخذ هذا البناء العديد من التعبيرات الثابتة أو حرفية السلسلة وإخراجها في مرحلة التجميع. سأكون سعيدًا إذا تمت إضافة هذه الأداة إلى المعيار ، أو على الأقل الموسع static_assert بحيث تقبل التعبيرات المستمرة.ومع ذلك ، في C ++ 17 ، قد تكون هناك طريقة لتحقيق هذه النتيجة. المجمعين نستنتج بالفعل أمرين: - الأخطاء و التحذيرات ! إذا تمكنا بطريقة أو بأخرى من إدارة التحذيرات أو تغييرها وفقًا لاحتياجاتنا ، فسوف نحصل بالفعل على نتيجة جديرة بالاهتمام. لقد جربت عدة حلول ، على وجه الخصوصسمة موقوفة : template <char... words> struct useless { [[deprecated]] void call() {}
على الرغم من أن الناتج موجود بشكل واضح ويمكن تحليله ، للأسف ، فإن الكود غير قابل للتشغيل! إذا كنت ، بمحض الصدفة ، عضوًا في جمعية سرية لمبرمجي C ++ الذين يمكنهم أداء الإخراج أثناء التجميع ، فسأكون سعيدًا لتوظيفك في فريقي لإنشاء Meta Crush Saga II المثالي !الاستنتاجات:
انتهى بي الأمر ببيع لعبة احتيالي الخاصة بك . آمل أن تجد هذا المنشور فضوليًا وأن تتعلم شيئًا جديدًا في عملية قراءته. إذا وجدت أخطاء أو طرقًا لتحسين المقالة ، فاتصل بي.أود أن أشكر فريق SwedenCpp على السماح لي بإجراء تقرير مشروعي في أحد مناسباتهم. بالإضافة إلى ذلك ، أود أن أعبر عن امتناني العميق لألكسندر جورديف ، الذي ساعدني على تحسين الجوانب المهمة في Meta Crush Saga .