كيفية تجميع الديكور - C ++ و Python وتنفيذها الخاص. الجزء الأول

سيتم تخصيص هذه السلسلة من المقالات لإمكانية إنشاء مصمم في 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);
}


كل شيء يعمل ، كل شيء على ما يرام وبشكل عام مرحى.

في الواقع ، هذا الحل لديه العديد من المشاكل. لنبدأ بالترتيب:

  1. لا يمكن ترجمة هذا الرمز إلا مع الإصدار C ++ 14 والإصدارات الأحدث ، لأنه لا يمكن معرفة النوع الذي تم إرجاعه مسبقًا. لسوء الحظ ، يجب أن أعيش مع هذا ولم أجد خيارات أخرى.
  2. make يتطلب تمرير الوظيفة std :: ، ويمرر دالة بالاسم إلى أخطاء في الترجمة. وهذا ليس مناسبًا على الإطلاق كما نود! لا يمكننا كتابة رمز مثل هذا:

    Decorator::make([](){});
    Decorator::make(myFunc);
    void(*ptr)(int) = myFunc;
    Decorator::make(ptr);

  3. أيضا ، لا يمكن تزيين طريقة الفصل.

لذلك ، بعد محادثة قصيرة مع الزملاء ، تم اختراع الخيار التالي لـ 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 وهيكلها الداخلي.

All Articles