سيتم تخصيص هذه السلسلة من المقالات لإمكانية إنشاء مصمم في C ++ ، وسيتم النظر في ميزات عملهم في Python ، وسيتم النظر في أحد خيارات تنفيذ هذه الوظيفة بلغتها المترجمة ، باستخدام النهج العام لإنشاء عمليات الإغلاق - تحويل الإغلاق وتحديث شجرة بناء الجملة.

تنصل, Python — . Python , (). - ( ), - ( , ..), Python «» .
الديكور في C ++
بدأ كل شيء بحقيقة أن صديقي VoidDruid قرر كتابة مترجم صغير كدبلوم ، السمة الرئيسية هي الديكور. حتى خلال فترة ما قبل الدفاع ، عندما أوجز جميع مزايا منهجه ، والتي تضمنت تغيير AST ، تساءلت: هل من المستحيل حقًا تنفيذ هذه الديكورات نفسها في C ++ العظيم والقوي والاستغناء عن أي مصطلحات ونهج معقدة؟ من خلال البحث في هذا الموضوع ، لم أجد أي طرق بسيطة وعامة لحل هذه المشكلة (بالمناسبة ، لقد صادفت فقط مقالات حول تنفيذ نمط التصميم) ثم جلست لكتابة الديكور الخاص بي.
ومع ذلك ، قبل الانتقال إلى وصف مباشر للتنفيذ ، أود أن أتحدث قليلاً عن كيفية ترتيب lambdas والإغلاق في C ++ وما هو الفرق بينهما. قم بالحجز على الفور أنه إذا لم يكن هناك ذكر لمعيار معين ، فإنني أعني بشكل افتراضي C ++ 20. باختصار ، lambdas هي وظائف مجهولة ، والإغلاق هي وظائف تستخدم كائنات من بيئتها. على سبيل المثال ، بدءًا من C ++ 11 ، يمكن الإعلان عن لامدا واستدعاءها على النحو التالي:
int main()
{
[] (int a)
{
std::cout << a << std::endl;
}(10);
}
أو قم بتعيين قيمته لمتغير وقم بتسميته لاحقًا.int main()
{
auto lambda = [] (int a)
{
std::cout << a << std::endl;
};
lambda(10);
}
ولكن ماذا يحدث أثناء التجميع وما هو لامدا؟ من أجل الانغماس في البنية الداخلية لامدا ، ما عليك سوى الانتقال إلى موقع cppinsights.io وتشغيل المثال الأول. بعد ذلك ، أرفقت نتيجة محتملة:class __lambda_60_19
{
public:
inline void operator()(int a) const
{
std::cout.operator<<(a).operator<<(std::endl);
}
using retType_60_19 = void (*)(int);
inline operator retType_60_19 () const noexcept
{
return __invoke;
};
private:
static inline void __invoke(int a)
{
std::cout.operator<<(a).operator<<(std::endl);
}
};
لذلك ، عند الترجمة ، تتحول لامدا إلى فئة ، أو بالأحرى ممر (كائن يتم تعريف عامل التشغيل () له ) باسم فريد يتم إنشاؤه تلقائيًا ، والذي يحتوي على عامل () ، والذي يقبل المعلمات التي مررناها إلى لامدا وجسمها يحتوي على الرمز الذي يجب أن تنفذه لامدا لدينا. مع هذا ، كل شيء واضح ، ولكن ماذا عن الطريقتين الأخريين ، لماذا هم؟ الأول هو عامل الصب إلى مؤشر دالة ، يتطابق النموذج الأولي الخاص به مع لامدا ، والثاني هو الرمز الذي يجب تنفيذه عندما يتم استدعاء لامدا لدينا عند تعيينه الأولي للمؤشر ، على سبيل المثال مثل:void (*p_lambda) (int) = lambda;
p_lambda(10);
حسنًا ، هناك لغز واحد أقل ، ولكن ماذا عن عمليات الإغلاق؟ دعونا نكتب أبسط مثال للإغلاق الذي يلتقط المتغير "a" بالإشارة ويزيده بمقدار واحد.int main()
{
int a = 10;
auto closure = [&a] () { a += 1; };
closure();
}
كما ترون ، فإن آلية إنشاء الإغلاق و lambdas في C ++ هي نفسها تقريبًا ، لذلك غالبًا ما يتم الخلط بين هذه المفاهيم وتسمى lambdas والإغلاق ببساطة lambdas.ولكن نعود إلى التمثيل الداخلي للإغلاق في C ++.class __lambda_61_20
{
public:
inline void operator()()
{
a += 1;
}
private:
int & a;
public:
__lambda_61_20(int & _a)
: a{_a}
{}
};
كما ترى ، قمنا بإضافة مُنشئ جديد غير افتراضي يأخذ معلمتنا حسب المرجع ويحفظها كعضو في الفصل. في الواقع ، هذا هو السبب في أنك بحاجة إلى توخي الحذر الشديد عند تعيين [&] أو [=] ، لأنه سيتم تخزين السياق بأكمله داخل الإغلاق ، ويمكن أن يكون هذا مثاليًا تمامًا من الذاكرة. بالإضافة إلى ذلك ، فقدنا عامل الإرسال إلى مؤشر دالة ، نظرًا لأن سياق المكالمة العادي مطلوب الآن. والآن لن يتم تجميع الرمز أعلاه:
int main()
{
int a = 10;
auto closure = [&a] () { a += 1; };
closure();
void (*ptr)(int) = closure;
}
ومع ذلك ، إذا كنت لا تزال بحاجة إلى تمرير الإغلاق في مكان ما ، فلن يلغي أحد استخدام الوظيفة std ::.std::function<void()> function = closure;
function();
الآن بعد أن توصلنا تقريبًا إلى معرفة ما هي الخروف والإغلاق في C ++ ، دعنا ننتقل إلى كتابة الديكور مباشرة. ولكن أولاً ، عليك أن تقرر متطلباتنا.لذلك ، يجب على الديكور أن يأخذ وظيفتنا أو أسلوبنا كمدخل ، ويضيف الوظيفة التي نحتاجها إليها (على سبيل المثال ، سيتم حذف هذا) ويعيد وظيفة جديدة عند استدعاؤها ، والتي تنفذ الكود الخاص بنا وكود الوظيفة / الطريقة. عند هذه النقطة ، سيقول أي بيثوني يحترم نفسه: "ولكن كيف ذلك! يجب على مصمم الديكور استبدال الكائن الأصلي وأي استدعاء له بالاسم يجب أن يستدعي وظيفة جديدة! " فقط هذا هو القيد الرئيسي لـ C ++ ، لا يمكننا منع المستخدم من استدعاء الوظيفة القديمة. بالطبع ، هناك خيار للحصول على عنوانه في الذاكرة وطحنه (في هذه الحالة ، سيؤدي الوصول إليه إلى إنهاء غير طبيعي للبرنامج) أو استبدال جسمه بتحذير أنه لا ينبغي استخدامه في وحدة التحكم ، ولكن هذا محفوف بعواقب وخيمة. إذا كان الخيار الأول يبدو صعبًا على الإطلاق ،ثم الثاني ، عند استخدام تحسينات المترجم المختلفة ، يمكن أن يؤدي أيضًا إلى حدوث عطل ، وبالتالي لن نستخدمها. أيضا ، استخدام أي سحر الماكرو هنا أعتبره زائدا.لذا ، دعنا ننتقل إلى كتابة الديكور لدينا. كان الخيار الأول الذي فكرت فيه هو:namespace Decorator
{
template<typename R, typename ...Args>
static auto make(const std::function<R(Args...)>& f)
{
std::cout << "Do something" << std::endl;
return [=](Args... args)
{
return f(std::forward<Args>(args)...);
};
}
};
دعها تكون بنية ذات طريقة ثابتة تأخذ وظيفة std :: وتعرض إغلاقًا يأخذ نفس المعلمات التي تستخدمها دالتنا وعندما يتم استدعاؤها ، سيتم استدعاء وظيفتنا فقط وإرجاع نتيجتها.دعونا ننشئ وظيفة بسيطة نريد تزيينها.void myFunc(int a)
{
std::cout << "here" << std::endl;
}
وسيبدو شكلنا الرئيسي كما يلي:int main()
{
std::function<void(int)> f = myFunc;
auto decorated = Decorator::make(f);
decorated(10);
}
كل شيء يعمل ، كل شيء على ما يرام وبشكل عام مرحى.في الواقع ، هذا الحل لديه العديد من المشاكل. لنبدأ بالترتيب:- لا يمكن ترجمة هذا الرمز إلا مع الإصدار C ++ 14 والإصدارات الأحدث ، لأنه لا يمكن معرفة النوع الذي تم إرجاعه مسبقًا. لسوء الحظ ، يجب أن أعيش مع هذا ولم أجد خيارات أخرى.
- make يتطلب تمرير الوظيفة std :: ، ويمرر دالة بالاسم إلى أخطاء في الترجمة. وهذا ليس مناسبًا على الإطلاق كما نود! لا يمكننا كتابة رمز مثل هذا:
Decorator::make([](){});
Decorator::make(myFunc);
void(*ptr)(int) = myFunc;
Decorator::make(ptr);
- أيضا ، لا يمكن تزيين طريقة الفصل.
لذلك ، بعد محادثة قصيرة مع الزملاء ، تم اختراع الخيار التالي لـ C ++ 17 وما فوق:namespace Decorator
{
template<typename Function>
static auto make(Function&& func)
{
return [func = std::forward<Function>(func)] (auto && ...args)
{
std::cout << "Do something" << std::endl;
return std::invoke(
func,
std::forward<decltype(args)>(args)...
);
};
}
};
مزايا هذا الخيار بالذات هي أنه يمكننا الآن تزيين أي كائن له عامل تشغيل () . لذا على سبيل المثال ، يمكننا تمرير اسم دالة مجانية ، ومؤشر ، ولامدا ، وأي ممول ، ووظيفة std :: ، وبالطبع طريقة الفصل. في حالة الأخيرة ، سيكون من الضروري أيضًا تمرير سياق إليها عند استدعاء وظيفة فك الشفرة.خيارات التطبيقint main()
{
auto decorated_1 = Decorator::make(myFunc);
decorated_1(1,2);
auto my_lambda = [] (int a, int b)
{
std::cout << a << " " << b <<std::endl;
};
auto decorated_2 = Decorator::make(my_lambda);
decorated_2(3,4);
int (*ptr)(int, int) = myFunc;
auto decorated_3 = Decorator::make(ptr);
decorated_3(5,6);
std::function<void(int, int)> fun = myFunc;
auto decorated_4 = Decorator::make(fun);
decorated_4(7,8);
auto decorated_5 = Decorator::make(decorated_4);
decorated_5(9, 10);
auto decorated_6 = Decorator::make(&MyClass::func);
decorated_6(MyClass(10));
}
بالإضافة إلى ذلك ، يمكن تجميع هذا الرمز مع C ++ 14 إذا كان هناك ملحق لاستخدام std :: invoke ، والذي يجب استبداله بـ std :: __ invoke. إذا لم يكن هناك امتداد ، فسيتعين عليك التخلي عن القدرة على تزيين طرق الفصل ، وستصبح هذه الوظيفة غير متاحة.لكي لا تكتب "std :: forward <dectype (args)> (args) المرهقة ..." ، يمكنك استخدام الوظائف المتاحة مع C ++ 20 وجعل مرجل لامدا الخاص بنا!namespace Decorator
{
template<typename Function>
static auto make(Function&& func)
{
return [func = std::forward<Function>(func)]
<typename ...Args> (Args && ...args)
{
return std::invoke(
func,
std::forward<Args>(args)...
);
};
}
};
كل شيء آمن تمامًا ويعمل حتى بالطريقة التي نريدها (أو على الأقل يتظاهر). تم تجميع هذا الرمز لكل من إصدارات gcc و clang 10-x ويمكنك العثور عليه هنا . سيكون هناك أيضا تطبيقات لمختلف المعايير.في المقالات التالية ، سننتقل إلى التنفيذ القانوني للديكور باستخدام مثال Python وهيكلها الداخلي.