Implementasi malas melintasi pohon anak-anak dari kelas QObject

Pendahuluan


Artikel ini menjelaskan implementasi malas dari traversal pohon di C ++ menggunakan coroutine dan rentang menggunakan contoh meningkatkan antarmuka untuk bekerja dengan anak-anak dari kelas QObject dari kerangka Qt. Penciptaan pandangan khusus untuk bekerja dengan elemen anak dipertimbangkan secara rinci, dan implementasi malas dan klasik diberikan. Di akhir artikel ada tautan ke repositori dengan kode sumber lengkap.


Tentang penulis


Saya bekerja sebagai pengembang senior di kantor The Qt Company di Norwegia. Saya telah mengembangkan widget dan elemen QtQuick, baru-baru ini Qt Core. Saya menggunakan C ++ dan sedikit tertarik pada pemrograman fungsional. Terkadang saya membuat laporan dan menulis artikel.


Apa itu Qt


Qt adalah kerangka kerja lintas platform untuk membuat antarmuka pengguna grafis (GUI). Selain modul untuk membuat GUI, Qt berisi banyak modul untuk mengembangkan perangkat lunak aplikasi. Kerangka kerja ini dirancang terutama dalam bahasa pemrograman C ++, beberapa komponen menggunakan QML dan JavaScript .


Kelas QObject


QObject adalah kelas di mana model objek Qt dibangun. Kelas yang diwarisi dari QObject dapat digunakan dalam model sinyal slot dan loop peristiwa. Selain itu, QObject memungkinkan Anda untuk mengakses informasi kelas meta-objek dan mengatur objek ke dalam struktur pohon.


QObject tree structure


Menggunakan struktur pohon berarti bahwa setiap objek QObject dapat memiliki satu orangtua dan nol atau lebih anak. Objek induk mengontrol masa hidup objek anak. Dalam contoh berikut, dua anak akan dihapus secara otomatis:


 auto parent = std::make_unique<QObject>(); auto onDestroyed = [](auto obj){ qDebug("Object %p destroyed.", obj); }; QObject::connect(new QObject(parent.get()), &QObject::destroyed, onDestroyed); QObject::connect(new QObject(parent.get()), &QObject::destroyed, onDestroyed); //       

Sayangnya, sejauh ini sebagian besar API Qt hanya berfungsi dengan pointer mentah. Kami sedang mengerjakan ini, dan mungkin segera situasi akan berubah menjadi lebih baik setidaknya sebagian.


QObject kelas QObject memungkinkan Anda untuk mendapatkan daftar semua objek anak dan mencari berdasarkan beberapa kriteria. Pertimbangkan contoh mendapatkan daftar semua objek anak:


 auto parent = std::make_unique<QObject>(); //  10   for (std::size_t i = 0; i < 10; ++i) { auto obj = new QObject(parent.get()); obj->setObjectName(QStringLiteral("Object %1").arg(i)); } const auto& children = parent->children(); qDebug() << children; // => (QObject(0x1f7ffa0, name = "Object 0"), ...) qDebug() << children.count(); // => 10 

Metode QObject::children mengembalikan daftar semua anak dari objek yang diberikan. Namun, pencarian sering diperlukan di antara seluruh subtree objek dengan beberapa kriteria:


 auto children = parent->findChildren<QObject>(QRegularExpression("0$")); qDebug() << children.count(); 

Contoh di atas menunjukkan cara mendapatkan daftar semua anak dari jenis QObject yang namanya berakhir dengan 0. Tidak seperti metode children - children , metode findChildren melintasi pohon secara rekursif, yaitu, pencarian melalui seluruh hierarki objek. Perilaku ini dapat diubah dengan melewati Qt::FindDirectChildrenOnly .


Kekurangan antarmuka untuk bekerja dengan elemen anak


Sekilas, mungkin terlihat bahwa antarmuka untuk bekerja dengan anak-anak dipikirkan dengan baik dan fleksibel. Namun, dia bukannya tanpa cacat. Mari kita pertimbangkan beberapa di antaranya:


  • Antarmuka redundan
    Ada dua metode findChildren berbeda (ada tiga belum lama ini): metode findChild untuk menemukan satu item dan metode anak-anak. Semuanya sebagian tumpang tindih.
  • Antarmuka sulit diubah
    Qt menjamin kompatibilitas dan kompatibilitas biner pada tingkat kode sumber dalam satu rilis utama tunggal. Oleh karena itu, Anda tidak bisa hanya mengubah tanda tangan suatu metode atau menambahkan metode baru.
  • Antarmuka sulit diperluas
    Selain pelanggaran kompatibilitas, tidak mungkin, misalnya, untuk mendapatkan daftar elemen anak sesuai dengan kriteria yang ditentukan. Untuk menambahkan fungsionalitas ini, Anda harus menunggu rilis berikutnya atau membuat metode lain.
  • Lebih dari menyalin semua item
    Seringkali, Anda hanya perlu melihat daftar semua elemen anak yang difilter dengan kriteria tertentu. Untuk melakukan ini, tidak perlu mengembalikan wadah petunjuk ke semua elemen ini.
  • Kemungkinan pelanggaran SRP
    Ini adalah masalah yang agak kontroversial, namun, kebutuhan untuk mengubah antarmuka kelas untuk berubah, katakanlah, metode untuk melintasi anak-anak terlihat aneh.

Menggunakan range-v3 untuk memperbaiki beberapa kekurangan


range-v3 adalah perpustakaan yang menyediakan komponen untuk bekerja dengan rentang elemen. Bahkan, ini adalah lapisan tambahan abstraksi atas iterator klasik, yang memungkinkan Anda untuk menyusun operasi dan memanfaatkan perhitungan malas.


Pustaka pihak ketiga digunakan karena pada saat penulisan, tidak ada kompiler yang diketahui oleh penulis dengan dukungan bawaan untuk fungsi ini. Mungkin situasinya akan segera berubah.


Untuk QObject menggunakan pendekatan ini akan memungkinkan kami untuk memisahkan operasi traversal dari pohon anak-anak dari kelas dan membuat antarmuka yang fleksibel untuk mencari objek sesuai dengan kriteria yang diberikan, yang dapat dengan mudah dimodifikasi.


Contoh Ranges-v3


Untuk memulai, pertimbangkan contoh sederhana menggunakan perpustakaan. Sebelum melanjutkan ke contoh, kami memperkenalkan notasi singkat untuk ruang nama:


 namespace r = ranges; namespace v = r::views; namespace a = r::actions; 

Sekarang perhatikan contoh program yang mencetak kubus dari semua angka ganjil dalam interval [1, 10) dalam urutan terbalik:


 auto is_odd = [](int n) { return n % 2 != 0; }; auto pow3 = [](int n) { return std::pow(n, 3); }; //  [729,343,125,27,1] std::cout << (v::ints(1, 10) | v::filter(is_odd) | v::transform(pow3) | v::reverse); 

Perlu dicatat bahwa semua perhitungan terjadi dengan malas, mis. set data sementara tidak dibuat atau disalin. Program di atas setara dengan ini, dengan pengecualian memformat output:


 //  729 343 125 27 1 for (int i = 9; i > 0; --i) { if (i % 2 != 0) { std::cout << std::pow(i, 3) << " "; } } 

Seperti yang dapat Anda lihat dari contoh di atas, perpustakaan memungkinkan Anda menyusun berbagai operasi dengan anggun. Lebih banyak contoh penggunaan dapat ditemukan dalam direktori tests dan examples repositori range-v3 .


Kelas untuk mewakili urutan anak-anak


Perpustakaan range-v3 menyediakan kelas pembantu untuk membuat berbagai kelas bungkus kustom; di antara mereka adalah kelas dari kategori view . Kelas-kelas ini dirancang untuk mewakili urutan elemen dengan cara tertentu tanpa mengubah dan menyalin urutan itu sendiri. Dalam contoh sebelumnya, kelas filter digunakan untuk mempertimbangkan hanya elemen-elemen dari urutan yang cocok dengan kriteria yang ditentukan.


Untuk membuat kelas seperti itu untuk bekerja dengan elemen anak QObject, itu harus diwarisi dari ranges::view_facade kelas tambahan ranges::view_facade :


 namespace qt::detail { template <class T = QObject> class children_view : public r::view_facade<children_view<T>> { //   friend r::range_access; //   ,       T *obj; //    (  ) Qt::FindChildOptions opts; //  --    cursor begin_cursor() { return cursor(obj, opts); } public: //  }; } // namespace qt::detail 

Perlu dicatat bahwa kelas secara otomatis mendefinisikan metode end_cursor , yang mengembalikan tanda akhir urutan. Jika perlu, metode ini dapat diganti.


Selanjutnya, kita mendefinisikan kelas kursor itu sendiri. Ini bisa dilakukan di dalam kelas children_view dan di luar:


 struct cursor { // ,      std::shared_ptr<ObjectVector> children; //    std::size_t current_index = 0; //       decltype(auto) read() const { return (*children)[current_index]; } //     void next() { ++current_index; } //     auto equal(ranges::default_sentinel_t) const { return current_index == children->size(); } //  }; 

Kursor yang ditentukan di atas adalah sekali-lewat. Ini berarti bahwa urutan dibiarkan bergerak hanya dalam satu arah dan hanya sekali. Untuk implementasi ini, ini tidak perlu, karena kami menyimpan urutan semua objek anak dan dapat melewatinya ke arah mana pun sebanyak yang Anda suka. Untuk menunjukkan bahwa Anda dapat melalui urutan beberapa kali, Anda harus menerapkan metode berikut di kelas kursor:


 auto equal(const cursor &that) const { return current_index == that.current_index; } 

Sekarang Anda perlu menambahkan untuk memastikan bahwa tampilan yang dibuat dapat dimasukkan dalam komposisi. Untuk melakukan ini, gunakan ranges::make_pipeable fungsi bantu ranges::make_pipeable :


 namespace qt { constexpr auto children = r::make_pipeable([](auto &&o) { return detail::children_view(o); }); constexpr auto find_children(Qt::FindChildOptions opts = Qt::FindChildrenRecursively) { return r::make_pipeable([opts](auto &&o) { return detail::children_view(o, opts); }); } } // namespace qt 

Sekarang Anda dapat menulis kode ini:


 for (auto &&c : root | qt::children) { //     () } for (auto &&c : root | qt::find_children(Qt::FindDirectChildrenOnly)) { //     } 

Menerapkan Fungsi Kelas QObject yang Ada


Setelah menerapkan kelas presentasi, Anda dapat dengan mudah mengimplementasikan semua fungsi untuk bekerja dengan anak-anak. Untuk melakukan ini, Anda perlu mengimplementasikan tiga fungsi:


 namespace qt { template <class T> const auto with_type = v::filter([](auto &&o) { using ObjType = std::remove_cv_t<std::remove_pointer_t<T>>; return ObjType::staticMetaObject.cast(o); }) | v::transform([](auto &&o){ return static_cast<T>(o); }); auto by_name(const QString &name) { return v::filter([name](auto &&obj) { return obj->objectName() == name; }); } auto by_re(const QRegularExpression &re) { return v::filter([re](auto &&obj) { return re.match(obj->objectName()).hasMatch(); }); } } // namespace qt 

Sebagai contoh penggunaan, pertimbangkan kode berikut:


 for (auto &&c : root | qt::children | qt::with_type<Foo*>) { //       Foo } 

Kesimpulan menengah


Seperti dapat dinilai oleh kode, sekarang cukup mudah untuk memperluas fungsionalitas tanpa mengubah antarmuka kelas. Selain itu, semua operasi diwakili oleh fungsi yang terpisah dan dapat diatur dalam urutan yang diinginkan. Ini, antara lain, meningkatkan pembacaan kode dan menghindari penggunaan fungsi dengan beberapa parameter di antarmuka kelas. Perlu diperhatikan juga pembongkaran antarmuka kelas dan pengurangan jumlah alasan untuk mengubahnya.


Faktanya, implementasi ini telah menghilangkan hampir semua kelemahan antarmuka yang terdaftar, kecuali bahwa kita masih harus menyalin semua anak ke dalam wadah. Salah satu cara untuk mengatasi masalah ini adalah dengan menggunakan coroutine.


Malas implementasi traversal objek pohon menggunakan coroutine


Coroutine (coroutine) memungkinkan Anda untuk menjeda fungsi dan melanjutkannya nanti. Anda dapat mempertimbangkan teknologi ini sebagai semacam mesin negara terbatas.


Pada saat penulisan, perpustakaan standar tidak memiliki banyak elemen penting yang diperlukan untuk penggunaan coroutine yang nyaman. Oleh karena itu, diusulkan untuk menggunakan perpustakaan cppcoro pihak ketiga , yang kemungkinan akan memasukkan standar dalam satu atau lain bentuk.


Untuk memulainya, kami akan menulis fungsi yang akan mengembalikan anak berikutnya sesuai permintaan:


 namespace qt::detail { cppcoro::recursive_generator<QObject*> takeChildRecursivelyImpl( const QObjectList &children, Qt::FindChildOptions opts) { for (QObject *c : children) { if (opts == Qt::FindChildrenRecursively) { co_yield takeChildRecursivelyImpl(c->children(), opts); } co_yield c; } } cppcoro::recursive_generator<QObject*> takeChildRecursively( QObject *root, Qt::FindChildOptions opts = Qt::FindChildrenRecursively) { if (root) { co_yield takeChildRecursivelyImpl(root->children(), opts); } } } // namespace qt::detail 

Instruksi co_yield mengembalikan nilai ke kode panggilan dan menjeda coroutine.


Sekarang mengintegrasikan kode ini ke dalam kelas children_view . Kode berikut hanya menunjukkan elemen yang telah berubah:


 //   children_view //   Data{obj, takeChildRecursively(obj, opts)} struct Data { T *obj; cppcoro::recursive_generator<QObject*> gen; }; std::shared_ptr<Data> m_data; // ... cursor begin_cursor() { return cursor(m_data->gen.begin()); } 

Kursor juga harus diubah:


 template <class T> struct children_view<T>::cursor { cppcoro::recursive_generator<QObject*>::iterator it; decltype(auto) read() const { return *it; } void next() { ++it; } auto equal(ranges::default_sentinel_t) const { return it == cppcoro::recursive_generator<QObject*>::iterator(nullptr); } explicit cursor(cppcoro::recursive_generator<QObject*>::iterator it): it(it) {} cursor() = default; }; 

Kursor di sini hanya bertindak sebagai pembungkus di sekitar iterator biasa. Sisa kode dapat digunakan apa adanya, tanpa perubahan tambahan.


Bahaya malas berjalan di pohon


Perlu dicatat bahwa traversal malas pohon anak-anak tidak selalu aman. Ini terutama berkaitan dengan memintas hierarki elemen grafis yang kompleks, misalnya widget. Faktanya adalah bahwa dalam proses traversal hierarki dapat dibangun kembali, dan beberapa elemen dihapus sepenuhnya. Jika Anda menggunakan solusi malas dalam hal ini, Anda bisa mendapatkan hasil program yang sangat menarik dan tidak dapat diprediksi.


Ini berarti bahwa dalam beberapa kasus, berguna untuk menyalin semua elemen ke dalam wadah. Untuk melakukan ini, Anda dapat menggunakan fungsi pembantu berikut:


 auto children = ranges::to<std::vector>(root | qt::children); 

Sebenarnya, dalam hal ini tidak perlu menggunakan coroutine dan Anda dapat menggunakan tampilan dari iterasi pertama.


Apakah akan di Qt


Mungkin, tetapi tidak di rilis berikutnya. Ada beberapa alasan untuk ini:


  • Rilis besar berikutnya, Qt 6, secara resmi akan membutuhkan dan mendukung C ++ 17, tetapi tidak lebih tinggi.
  • Tidak ada cara untuk mengimplementasikannya tanpa perpustakaan pihak ketiga.
  • Akan relatif sulit untuk mengadaptasi basis kode yang ada.
    Kemungkinan besar, mereka akan kembali ke masalah ini sebagai bagian dari rilis Qt 7.

Kesimpulan


Usulan implementasi melintasi elemen pohon anak membuatnya mudah untuk menambahkan fungsionalitas baru. Karena pemisahan operasi, penulisan kode pembersih dan penghapusan elemen yang tidak perlu dari antarmuka kelas tercapai.


Perlu dicatat bahwa kedua perpustakaan yang digunakan (range-v3 dan cpp-coro) disediakan sebagai file header, yang menyederhanakan proses pembuatan. Di masa depan, itu akan mungkin dilakukan tanpa perpustakaan pihak ketiga sama sekali.


Namun, pendekatan yang dijelaskan memiliki beberapa kelemahan. Di antara mereka, satu dapat mencatat sintaks yang tidak biasa bagi banyak pengembang, kompleksitas relatif implementasi dan kemalasan, yang dapat berbahaya dalam beberapa kasus.


Opsional


Kode sumber


Terima kasih khusus kepada Misha Svetkin ( Trilla ) untuk kontribusinya dalam implementasi dan diskusi proyek.

Source: https://habr.com/ru/post/id481402/


All Articles