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>();
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); };
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:
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>> {
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 {
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); }); } }
Sekarang Anda dapat menulis kode ini:
for (auto &&c : root | qt::children) {
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(); }); } }
Sebagai contoh penggunaan, pertimbangkan kode berikut:
for (auto &&c : root | qt::children | qt::with_type<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); } } }
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:
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.