Cara mengkompilasi dekorator - C ++, Python dan implementasinya sendiri. Bagian 1

Rangkaian artikel ini akan dikhususkan untuk kemungkinan membuat dekorator dalam C ++, fitur-fitur pekerjaan mereka dalam Python, dan salah satu opsi untuk mengimplementasikan fungsi ini dalam bahasa yang dikompilasi sendiri akan dipertimbangkan, menggunakan pendekatan umum untuk membuat penutupan - konversi penutupan dan modernisasi pohon sintaksis.



Penolakan
, Python โ€” . Python , (). - ( ), - ( , ..), Python ยซยป .

Dekorator dalam C ++


Semuanya berawal dari kenyataan bahwa teman saya VoidDruid memutuskan untuk menulis kompiler kecil sebagai diploma, fitur utama di antaranya adalah dekorator. Bahkan selama pra-pertahanan, ketika dia menguraikan semua keuntungan dari pendekatannya, yang termasuk mengubah AST, saya bertanya-tanya: apakah benar-benar mustahil untuk mengimplementasikan dekorator yang sama ini dalam C ++ yang hebat dan kuat dan melakukannya tanpa syarat dan pendekatan yang rumit? Googling topik ini, saya tidak menemukan pendekatan sederhana dan umum untuk menyelesaikan masalah ini (omong-omong, saya hanya menemukan artikel tentang penerapan pola desain) dan kemudian duduk untuk menulis dekorator saya sendiri.


Namun, sebelum melanjutkan ke deskripsi langsung implementasi saya, saya ingin berbicara sedikit tentang bagaimana lambda dan penutupan di C ++ diatur dan apa perbedaan di antara mereka. Segera buat reservasi bahwa jika tidak disebutkan standar tertentu, maka secara default yang saya maksud adalah C ++ 20. Singkatnya, lambdas adalah fungsi anonim, dan penutupan adalah fungsi yang menggunakan objek dari lingkungannya. Jadi misalnya, dimulai dengan C ++ 11, lambda dapat dideklarasikan dan dipanggil seperti ini:

int main() 
{
    [] (int a) 
    {
        std::cout << a << std::endl;
    }(10);
}

Atau tetapkan nilainya ke variabel dan panggil nanti.

int main() 
{
    auto lambda = [] (int a) 
    {
        std::cout << a << std::endl;
    };
    lambda(10);
}

Tetapi apa yang terjadi selama kompilasi dan apa itu lambda? Untuk membenamkan diri dalam struktur internal lambda, buka saja situs web cppinsights.io dan jalankan contoh pertama kami. Selanjutnya, saya lampirkan kesimpulan yang mungkin:

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);
    }    
};


Jadi, ketika mengkompilasi, lambda berubah menjadi kelas, atau lebih tepatnya functor (objek yang didefinisikan operator ) dengan nama unik yang dihasilkan secara otomatis, yang memiliki operator () , yang menerima parameter yang kami lewati ke lambda dan tubuhnya berisi kode yang harus dieksekusi lambda kita. Dengan ini, semuanya jelas, tetapi bagaimana dengan dua metode lainnya, mengapa mereka? Yang pertama adalah operator casting ke pointer fungsi, prototipe yang bertepatan dengan lambda kita, dan yang kedua adalah kode yang harus dieksekusi ketika lambda kita dipanggil pada tugas awal untuk pointer, misalnya,

void (*p_lambda) (int) = lambda;
p_lambda(10);

Yah, ada sedikit teka-teki, tapi bagaimana dengan penutupan? Mari kita menulis contoh paling sederhana dari penutupan yang menangkap variabel "a" dengan referensi dan meningkatkannya dengan satu.

int main()
{
    int a = 10;
    auto closure = [&a] () { a += 1; };
    closure();
}

Seperti yang Anda lihat, mekanisme untuk membuat closures dan lambdas di C ++ hampir sama, sehingga konsep-konsep ini sering membingungkan dan lambdas dan closure hanya disebut lambdas.

Tetapi kembali ke representasi internal penutupan di C ++.

class __lambda_61_20
{
public:
    inline void operator()()
    {
        a += 1;
    }
private:
    int & a;
public:
    __lambda_61_20(int & _a)
    : a{_a}
    {}
};

Seperti yang Anda lihat, kami telah menambahkan konstruktor non-standar baru yang mengambil parameter kami dengan referensi dan menyimpannya sebagai anggota kelas. Sebenarnya, inilah mengapa Anda harus sangat berhati-hati ketika mengatur [&] atau [=], karena seluruh konteks akan disimpan dalam penutupan, dan ini bisa sangat suboptimal dari memori. Selain itu, kami kehilangan operator casting ke pointer fungsi, karena sekarang untuk konteks panggilan normal diperlukan. Dan sekarang kode di atas tidak akan dikompilasi:

int main()
{
    int a = 10;
    auto closure = [&a] () { a += 1; };
    closure();
    void (*ptr)(int) = closure;
}

Namun, jika Anda masih harus melewati penutupan di suatu tempat, tidak ada yang membatalkan penggunaan fungsi std ::.

std::function<void()> function = closure;
function();

Sekarang setelah kita mengetahui secara kasar apa itu lambdas dan penutupan di C ++, mari beralih ke menulis dekorator secara langsung. Tetapi pertama-tama, Anda perlu memutuskan persyaratan kami.

Jadi, dekorator harus mengambil fungsi atau metode kita sebagai input, menambahkan fungsionalitas yang kita perlukan (misalnya, ini akan dihilangkan) dan mengembalikan fungsi baru ketika dipanggil, yang mengeksekusi kode kita dan kode fungsi / metode. Pada titik ini, setiap pythonis yang menghargai diri sendiri akan mengatakan: "Tapi bagaimana bisa begitu! Penghias harus mengganti objek asli dan panggilan apa pun dengan namanya harus memanggil fungsi baru! " Hanya ini adalah batasan utama C ++, kami tidak dapat menghentikan pengguna dari memanggil fungsi yang lama. Tentu saja, ada opsi untuk mendapatkan alamatnya di memori dan menggilingnya (dalam hal ini, mengaksesnya akan menyebabkan penghentian program yang tidak normal) atau mengganti badannya dengan peringatan bahwa itu tidak boleh digunakan di konsol, tetapi ini penuh dengan konsekuensi serius. Jika opsi pertama tampaknya cukup sulit,maka yang kedua, ketika menggunakan berbagai optimisasi kompiler, juga dapat menyebabkan crash, dan karena itu kami tidak akan menggunakannya. Juga, penggunaan sihir makro apa pun di sini saya anggap berlebihan.

Jadi, mari kita beralih ke menulis dekorator kami. Pilihan pertama yang muncul di benak saya adalah ini:

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)...);
        };
    }
};

Biarkan itu menjadi struktur dengan metode statis yang mengambil fungsi std :: dan mengembalikan penutupan yang akan mengambil parameter yang sama dengan fungsi kita dan ketika dipanggil, ia hanya akan memanggil fungsi kita dan mengembalikan hasilnya.

Mari kita buat fungsi sederhana yang ingin kita hias.

void myFunc(int a)
{
    std::cout << "here" << std::endl;
}

Dan utama kita akan terlihat seperti ini:

int main()
{
    std::function<void(int)> f = myFunc;
    auto decorated = Decorator::make(f);
    decorated(10);
}


Semuanya berfungsi, semuanya baik-baik saja dan secara umum Hore.

Sebenarnya, solusi ini memiliki beberapa masalah. Mari kita mulai dengan urutan:

  1. Kode ini hanya dapat dikompilasi dengan versi C ++ 14 dan lebih tinggi, karena tidak mungkin untuk mengetahui jenis yang dikembalikan sebelumnya. Sayangnya, saya harus hidup dengan ini dan saya tidak menemukan opsi lain.
  2. make membutuhkan std :: function untuk diteruskan ke sana, dan meneruskan fungsi dengan nama mengarah ke kesalahan kompilasi. Dan ini sama sekali tidak senyaman yang kita inginkan! Kami tidak dapat menulis kode seperti ini:

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

  3. Juga, tidak mungkin untuk menghias metode kelas.

Oleh karena itu, setelah percakapan singkat dengan kolega, opsi berikut diciptakan untuk C ++ 17 ke atas:

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)...
            );
        };
    }
};

Keuntungan dari opsi khusus ini adalah bahwa sekarang kita dapat menghiasi objek apa pun yang memiliki operator () . Jadi misalnya, kita dapat melewatkan nama fungsi bebas, pointer, lambda, functor, std :: function, dan tentu saja metode kelas. Dalam kasus yang terakhir, juga akan diperlukan untuk menyampaikan konteks ketika memanggil fungsi yang diterjemahkan.

Opsi aplikasi
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));
}


Selain itu, kode ini dapat dikompilasi dengan C ++ 14 jika ada ekstensi untuk menggunakan std :: invoke, yang perlu diganti dengan std :: __ invoke. Jika tidak ada ekstensi, maka Anda harus menyerahkan kemampuan untuk menghias metode kelas, dan fungsi ini tidak akan tersedia.

Agar tidak menulis "std :: forward <type (args)> yang rumit ..." Anda dapat menggunakan fungsionalitas yang tersedia dengan C ++ 20 dan membuat lambda boilerda kami!

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)...
            );
        };
    }
};

Semuanya sangat aman dan bahkan berfungsi seperti yang kita inginkan (atau setidaknya berpura-pura). Kode ini dikompilasi untuk versi gcc dan clang 10-x dan Anda dapat menemukannya di sini . Juga akan ada implementasi untuk berbagai standar.

Pada artikel selanjutnya, kita akan beralih ke implementasi dekorator kanonik menggunakan contoh Python dan struktur internal mereka.

All Articles