Hai, Habr. Jika Anda tahu jawaban pertanyaan dalam judul, selamat, Anda tidak perlu artikel ini. Ini ditujukan kepada pemula dalam pemrograman, seperti saya, yang tidak selalu dapat memahami semua seluk beluk C ++ dan bahasa lainnya yang diketik secara mandiri, dan jika mereka bisa, lebih baik untuk belajar dari kesalahan orang lain.
Pada artikel ini, saya tidak hanya akan menjawab pertanyaan "
Mengapa kita membutuhkan fungsi virtual di C ++ ", tetapi saya akan memberikan contoh dari praktik saya. Untuk jawaban singkat, Anda dapat beralih ke mesin pencari yang menghasilkan sesuatu seperti berikut: "
Fungsi virtual diperlukan untuk memberikan polimorfisme - salah satu dari tiga paus OOP. Berkat mereka, mesin itu sendiri dapat menentukan jenis objek dengan pointer tanpa memuat tugas kepada programmer. " Oke, tetapi pertanyaan "mengapa" tetap ada, meskipun sekarang artinya sedikit berbeda: "
Mengapa mengandalkan mesin, menghabiskan waktu dan memori ekstra, jika Anda dapat podcast pointer sendiri, karena jenis objek yang dirujuk hampir selalu dikenal? " Memang, casting sekilas meninggalkan fungsi virtual tanpa kerja, dan inilah yang menyebabkan kesalahpahaman dan kode buruk. Dalam proyek-proyek kecil, kerugiannya tidak terlihat, tetapi, seperti yang akan segera Anda lihat, dengan pertumbuhan program, kasta meningkatkan daftar dalam perkembangan yang hampir geometris.
Pertama, mari kita ingat di mana kasta dan fungsi virtual mungkin diperlukan sama sekali. Tipe hilang ketika suatu objek yang dideklarasikan dengan tipe A dialokasikan operasi baru untuk mengalokasikan memori untuk objek tipe B yang kompatibel dengan tipe A, biasanya diwarisi dari A. Paling sering objek bukan satu, tetapi seluruh array. Array pointer dari tipe yang sama, masing-masing menunggu penugasan area memori dengan objek dari tipe yang sama sekali berbeda. Berikut adalah contoh yang akan kami pertimbangkan.
Saya tidak akan berlama-lama, tugasnya adalah ini: berdasarkan pada dokumen yang ditandai dengan bahasa markup Markedit hypertext (Anda dapat membacanya di
sini ), membangun parsing tree dan membuat file yang berisi dokumen yang sama dalam markup HTML. Solusi saya terdiri dari tiga rutinitas berurutan: mem-parsing teks sumber menjadi token, membangun pohon sintaks dari token dan membangun dokumen HTML berdasarkan itu. Kami tertarik pada bagian kedua.
Faktanya adalah bahwa node dari pohon tujuan memiliki jenis yang berbeda (bagian, paragraf, simpul teks, tautan, catatan kaki, dll.), Tetapi untuk simpul induk, pointer ke simpul anak disimpan dalam larik, dan karenanya memiliki satu jenis - Node.
Parser itu sendiri, dalam bentuk yang disederhanakan, berfungsi seperti ini: ia menciptakan "root" dari pohon sintaksis
pohon dengan tipe
Root , menyatakan pointer
open_node dari tipe umum
Node , yang segera menetapkan alamat
tree , dan variabel
tipe dari tipe yang
disebutkan ,
Node_type , dan kemudian loop dimulai,
mengulangi token dari awal. sampai yang terakhir. Pada setiap iterasi, jenis
open_node open node dimasukkan ke dalam variabel tipe
pertama (tipe dalam bentuk enumerasi disimpan dalam struktur node), diikuti oleh
pernyataan switch , yang memeriksa jenis token berikutnya (jenis token sudah dengan hati-hati disediakan oleh lexer). Di setiap cabang switch, cabang lain disajikan yang memeriksa variabel
tipe , di mana, seperti yang kita ingat, jenis node terbuka terkandung. Bergantung pada nilainya, tindakan yang berbeda dilakukan, misalnya: menambahkan simpul-daftar dari jenis tertentu ke simpul terbuka, membuka simpul lain dari jenis tertentu dalam simpul terbuka dan meneruskan alamatnya ke
open_node , menutup simpul terbuka, melempar pengecualian. Berlaku untuk topik artikel, kami tertarik pada contoh kedua. Setiap node terbuka (dan umumnya setiap node yang dapat dibuka) sudah berisi array pointer ke node tipe
Node . Oleh karena itu, ketika kita membuka simpul baru di simpul terbuka (kita menetapkan area memori untuk objek tipe lain ke pointer array berikutnya), untuk penganalisis semantik C ++, ia tetap merupakan instance dari tipe
Node tanpa memperoleh bidang dan metode baru. Pointer ke sana sekarang ditugaskan ke variabel
open_node , tanpa kehilangan jenis
Node . Tetapi bagaimana cara bekerja dengan pointer dari tipe
Node umum ketika Anda perlu memanggil metode, misalnya, paragraf? Misalnya,
open_bold () , yang membuka simpul font tebal di dalamnya? Bagaimanapun,
open_bold () dideklarasikan dan didefinisikan sebagai metode kelas
Paragraph , dan
Node sama sekali tidak menyadarinya. Selain itu,
open_node juga dideklarasikan sebagai pointer ke
Node , dan harus menerima metode dari semua jenis node pembuka.
Ada dua solusi di sini: yang jelas dan yang tepat. Jelas untuk pemula adalah
static_cast , dan fungsi virtual benar. Pertama mari kita lihat satu cabang parser switch yang ditulis menggunakan metode pertama:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::SECTION) { open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->open_bold(); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->open_bold(); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->open_bold(); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::LINK) open_node = static_cast<Link*>(open_node)->open_bold(); else
Tidak buruk. Dan sekarang, saya tidak akan menyeretnya untuk waktu yang lama, saya akan menunjukkan bagian kode yang sama yang ditulis menggunakan fungsi virtual:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::SECTION) { open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::UNORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else
Keuntungannya jelas, tetapi apakah kita benar-benar membutuhkannya? Setelah semua, maka Anda harus mendeklarasikan dalam kelas
Node semua metode dari semua kelas turunan sebagai virtual dan entah bagaimana mengimplementasikannya di setiap kelas turunan. Jawabannya adalah ya. Tidak ada banyak metode khusus dalam program ini (29), dan implementasinya dalam kelas turunan yang tidak terkait dengannya hanya terdiri dari satu baris:
throw string ("error!"); . Anda dapat mengaktifkan mode kreatif dan menghasilkan garis unik untuk setiap lemparan pengecualian. Tetapi yang paling penting - karena pengurangan kode, jumlah kesalahan di dalamnya telah menurun. Casting adalah salah satu penyebab kesalahan kode yang paling penting. Karena setelah menerapkan
static_cast, kompiler berhenti bersumpah jika kelas yang dipanggil terkandung dalam kelas yang diberikan. Sementara itu, kelas yang berbeda mungkin berisi metode yang berbeda dengan nama yang sama. Dalam kasus saya, 6 disembunyikan dalam kode !!! kesalahan, sementara salah satu dari mereka diduplikasi di beberapa cabang switch. Ini dia:
else if (type == Node:: open_node = static_cast<Title*>(open_node)->open_italic();
Selanjutnya, di bawah spoiler, saya membawa daftar lengkap parser versi pertama dan kedua.
Parser dengan casting Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->add_text("\n"); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->add_text("\n"); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->add_text("\n"); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::LINK) { open_node = static_cast<Link*>(open_node)->add_text(lexer[i].lexeme); } else
Parser dengan akses ke metode virtual Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH || type == Node::TITLE || type == Node::QUOTE || type == Node::TITLE || type == Node::QUOTE) open_node = open_node->add_text("\n"); else if (type == Node::UNORDERED_LIST || type == Node::ORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); } else
Dari 1357 baris, kode dikurangi menjadi 487 - hampir tiga kali, tidak termasuk panjang garis!Masih ada satu pertanyaan: bagaimana dengan lead time? Berapa milidetik yang harus kita bayarkan untuk komputer itu sendiri untuk menentukan jenis simpul terbuka? Saya melakukan percobaan - saya memperbaiki waktu kerja parser dalam milidetik dalam kasus pertama dan kedua untuk dokumen yang sama di komputer rumah saya. Inilah hasilnya:Casting - 538 ms.Fungsi virtual - 1174 ms.Total, 636 ms - biaya untuk kekompakan kode dan tidak adanya kesalahan. Apakah ini banyak? Mungkin Tetapi jika kita membutuhkan program yang bekerja secepat mungkin dan membutuhkan memori sesedikit mungkin, kita tidak akan pergi ke OOP dan menulisnya dalam bahasa assembly bersama, menghabiskan waktu seminggu dan mengambil risiko membuat sejumlah besar kesalahan. Jadi pilihan saya adalah di mana static_cast dan dynamic_cast bertemu di program , ganti dengan fungsi virtual. Apa pendapat anda