Memahami / persyaratan untuk polimorfisme
Untuk memahami polimorfisme - seperti istilah yang digunakan dalam Ilmu Komputer - itu membantu untuk memulai dari tes sederhana untuk dan definisi itu. Mempertimbangkan:
Type1 x;
Type2 y;
f(x);
f(y);
Di sini, f()
adalah untuk melakukan beberapa operasi dan diberi nilai x
dan y
sebagai input.
Untuk menunjukkan polimorfisme, f()
harus dapat beroperasi dengan nilai-nilai dari setidaknya dua jenis yang berbeda (misalnya int
dan double
), menemukan dan mengeksekusi kode yang sesuai jenis yang berbeda.
Mekanisme C ++ untuk polimorfisme
Polimorfisme khusus yang ditentukan oleh programmer
Anda dapat menulis f()
sedemikian rupa sehingga dapat beroperasi pada banyak jenis dengan salah satu cara berikut:
Preprocessing:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
Overloading:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
Templat:
template <typename T>
void f(T& x) { x += 2; }
Pengiriman virtual:
struct Base { virtual Base& operator+=(int) = 0; };
struct X : Base
{
X(int n) : n_(n) { }
X& operator+=(int n) { n_ += n; return *this; }
int n_;
};
struct Y : Base
{
Y(double n) : n_(n) { }
Y& operator+=(int n) { n_ += n; return *this; }
double n_;
};
void f(Base& x) { x += 2; } // run-time polymorphic dispatch
Mekanisme terkait lainnya
Polimorfisme yang disediakan oleh kompiler untuk tipe bawaan, Konversi standar, dan casting / paksaan dibahas nanti untuk kelengkapan sebagai:
- mereka umumnya dimengerti secara intuisi (menjamin " oh, itu reaksi "),
- mereka memengaruhi ambang batas dalam membutuhkan, dan kelancaran dalam menggunakan, mekanisme di atas, dan
- Penjelasan adalah pengalih perhatian dari konsep yang lebih penting.
Terminologi
Kategorisasi lebih lanjut
Dengan adanya mekanisme polimorfik di atas, kita dapat mengategorikannya dengan berbagai cara:
1 - Template sangat fleksibel. SFINAE (lihat juga std::enable_if
) secara efektif memungkinkan beberapa rangkaian harapan untuk polimorfisme parametrik. Misalnya, Anda dapat menyandikan bahwa ketika tipe data yang Anda proses memiliki .size()
anggota Anda akan menggunakan satu fungsi, jika tidak, fungsi lain yang tidak perlu .size()
(tetapi mungkin menderita dalam beberapa cara - misalnya menggunakan yang lebih lambat strlen()
atau tidak mencetak sebagai berguna pesan dalam log). Anda juga dapat menentukan perilaku ad-hoc ketika template dibuat dengan parameter tertentu, baik meninggalkan beberapa parameter parametrik ( spesialisasi template parsial ) atau tidak ( spesialisasi penuh ).
"Polimorfik"
Alf Steinbach berkomentar bahwa dalam C ++ Standard polymorphic hanya mengacu pada run-time polymorphism menggunakan virtual dispatch. General Comp. Sci. artinya lebih inklusif, sesuai dengan C ++ glosarium pencipta Bjarne Stroustrup ( http://www.stroustrup.com/glossary.html ):
polymorphism - menyediakan antarmuka tunggal untuk entitas dari berbagai jenis. Fungsi virtual menyediakan polimorfisme dinamis (run-time) melalui antarmuka yang disediakan oleh kelas dasar. Fungsi dan template yang kelebihan beban menyediakan polimorfisme statis (waktu kompilasi). TC ++ PL 12.2.6, 13.6.1, A&P 2.9.
Jawaban ini - seperti pertanyaannya - menghubungkan fitur-fitur C ++ ke Comp. Sci. terminologi.
Diskusi
Dengan C ++ Standard menggunakan definisi sempit "polimorfisme" daripada Comp. Sci. komunitas, untuk memastikan saling pengertian bagi audiens Anda pertimbangkan ...
- menggunakan terminologi yang tidak ambigu ("bisakah kita membuat kode ini dapat digunakan kembali untuk jenis lain?" atau "bisakah kita menggunakan pengiriman virtual?" daripada "bisakah kita membuat kode ini polimorfik?"), dan / atau
- mendefinisikan terminologi Anda dengan jelas.
Namun, apa yang penting untuk menjadi programmer C ++ yang hebat adalah memahami apa yang sebenarnya dilakukan polimorfisme untuk Anda ...
memungkinkan Anda menulis kode "algoritmik" satu kali dan kemudian menerapkannya ke banyak jenis data
... dan kemudian sangat sadar bagaimana mekanisme polimorfik yang berbeda sesuai dengan kebutuhan Anda yang sebenarnya.
Setelan polimorfisme run-time:
- input diproses dengan metode pabrik dan dimuntahkan sebagai koleksi objek heterogen yang ditangani melalui
Base*
s,
- implementasi yang dipilih pada saat runtime berdasarkan pada file konfigurasi, sakelar baris perintah, pengaturan UI dll.,
- implementasi bervariasi pada saat runtime, seperti untuk pola mesin negara.
Ketika tidak ada driver yang jelas untuk polimorfisme run-time, opsi waktu kompilasi lebih disukai. Mempertimbangkan:
- kompilasi-apa yang disebut aspek kelas templated lebih baik daripada antarmuka lemak gagal saat runtime
- SFINAE
- CRTP
- optimisasi (banyak termasuk penghapusan inlining dan kode mati, loop membuka gulungan, array berbasis stack statis vs heap)
__FILE__
,, __LINE__
string concatenation literal dan kemampuan unik makro lainnya (yang tetap jahat ;-))
- templates dan macro test penggunaan semantik didukung, tetapi jangan secara artifisial membatasi bagaimana dukungan itu disediakan (seperti pengiriman virtual cenderung dengan membutuhkan pencocokan fungsi anggota yang persis sama)
Mekanisme lain yang mendukung polimorfisme
Seperti yang dijanjikan, untuk kelengkapan beberapa topik tambahan dibahas:
- overload yang disediakan kompiler
- konversi
- gips / paksaan
Jawaban ini diakhiri dengan diskusi tentang bagaimana kombinasi di atas untuk memberdayakan dan menyederhanakan kode polimorfik - terutama polimorfisme parametrik (template dan makro).
Mekanisme untuk pemetaan ke operasi tipe-spesifik
> Overload yang disediakan compiler implisit
Secara konsep, kompiler membebani banyak operator untuk tipe builtin. Secara konseptual tidak berbeda dari overload yang ditentukan pengguna, tetapi terdaftar karena mudah diabaikan. Misalnya, Anda dapat menambahkan ke int
dan double
menggunakan notasi yang sama x += 2
dan kompiler menghasilkan:
- instruksi CPU tipe-spesifik
- hasil dari jenis yang sama.
Overloading kemudian meluas ke tipe yang ditentukan pengguna:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Overload yang disediakan oleh kompiler untuk tipe dasar adalah umum dalam bahasa komputer level tinggi (3GL +), dan diskusi eksplisit tentang polimorfisme umumnya menyiratkan sesuatu yang lebih. (2GL - bahasa assembly - sering mengharuskan programmer untuk secara eksplisit menggunakan mnemonik yang berbeda untuk jenis yang berbeda.)
> Konversi standar
Bagian keempat C ++ Standard menjelaskan konversi Standar.
Poin pertama merangkum dengan baik (dari konsep lama - semoga masih secara substansial benar):
-1- Konversi standar adalah konversi implisit yang ditentukan untuk tipe bawaan. Klausa conv merinci set lengkap konversi tersebut. Urutan konversi standar adalah urutan konversi standar dalam urutan berikut:
Nol atau satu konversi dari rangkaian berikut: konversi lvalue-ke-rvalue, konversi array-ke-pointer, dan konversi fungsi-ke-pointer.
Nol atau satu konversi dari rangkaian berikut: promosi integral, promosi floating point, konversi integral, konversi floating point, konversi floating-integral, konversi pointer, konversi pointer ke anggota, dan konversi boolean.
Nol atau satu konversi kualifikasi.
[Catatan: urutan konversi standar bisa kosong, yaitu tidak boleh ada konversi. ] Urutan konversi standar akan diterapkan ke ekspresi jika perlu untuk mengubahnya ke jenis tujuan yang diperlukan.
Konversi ini memungkinkan kode seperti:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Menerapkan tes sebelumnya:
Untuk menjadi polimorfik, [ a()
] harus dapat beroperasi dengan nilai setidaknya dua jenis yang berbeda (misalnya int
dan double
), menemukan dan mengeksekusi kode yang sesuai jenis .
a()
sendiri menjalankan kode khusus untuk double
dan karena itu bukan polimorfik.
Tapi, dalam panggilan kedua ke a()
kompiler tahu untuk menghasilkan kode yang sesuai jenis untuk "promosi floating point" (Standar §4) untuk dikonversi 42
menjadi 42.0
. Kode tambahan itu ada dalam fungsi panggilan . Kami akan membahas pentingnya hal ini dalam kesimpulan.
> Paksaan, gips, konstruktor implisit
Mekanisme ini memungkinkan kelas yang ditentukan pengguna untuk menentukan perilaku yang mirip dengan konversi standar tipe yang dibangun. Mari kita lihat:
int a, b;
if (std::cin >> a >> b)
f(a, b);
Di sini, objek std::cin
dievaluasi dalam konteks boolean, dengan bantuan operator konversi. Ini dapat dikelompokkan secara konseptual dengan "promosi integral" dkk dari Konversi standar dalam topik di atas.
Konstruktor implisit secara efektif melakukan hal yang sama, tetapi dikendalikan oleh tipe cast-to:
f(const std::string& x);
f("hello"); // invokes `std::string::string(const char*)`
Implikasi dari overload, konversi, dan paksaan yang disediakan oleh kompiler
Mempertimbangkan:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Jika kita ingin jumlah x
diperlakukan sebagai bilangan real selama pembagian (yaitu menjadi 6,5 daripada dibulatkan ke 6), kita hanya perlu mengubah ke typedef double Amount
.
Itu bagus, tetapi tidak akan terlalu banyak pekerjaan untuk membuat kode secara eksplisit "ketik benar":
void f() void f()
{ {
typedef int Amount; typedef double Amount;
Amount x = 13; Amount x = 13.0;
x /= 2; x /= 2.0;
std::cout << double(x) * 1.1; std::cout << x * 1.1;
} }
Tetapi, pertimbangkan bahwa kami dapat mengubah versi pertama menjadi template
:
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Ini karena "fitur kenyamanan" kecil yang dapat dengan mudah dipakai untuk int
atau double
bekerja sebagaimana dimaksud. Tanpa fitur-fitur ini, kita akan membutuhkan gips yang eksplisit, ketik ciri-ciri dan / atau kelas kebijakan, beberapa kekacauan yang cenderung kesalahan seperti:
template <typename Amount, typename Policy>
void f()
{
Amount x = Policy::thirteen;
x /= static_cast<Amount>(2);
std::cout << traits<Amount>::to_double(x) * 1.1;
}
Jadi, overloading yang disediakan operator compiler untuk tipe builtin, Konversi standar, casting / koersi / konstruktor implisit - mereka semua memberikan dukungan halus untuk polimorfisme. Dari definisi di bagian atas jawaban ini, mereka membahas "menemukan dan mengeksekusi kode yang sesuai jenis" dengan memetakan:
Mereka tidak membangun konteks polimorfik sendiri, tetapi membantu memberdayakan / menyederhanakan kode di dalam konteks tersebut.
Anda mungkin merasa ditipu ... sepertinya tidak banyak. Signifikansi adalah bahwa dalam konteks polimorfik parametrik (yaitu di dalam template atau makro), kami mencoba untuk mendukung berbagai jenis sewenang-wenang tetapi sering ingin mengekspresikan operasi pada mereka dalam hal fungsi lain, literal dan operasi yang dirancang untuk suatu set kecil jenis. Ini mengurangi kebutuhan untuk membuat fungsi atau data yang hampir identik pada basis per jenis ketika operasi / nilai secara logis sama. Fitur-fitur ini bekerja sama untuk menambah sikap "upaya terbaik", melakukan apa yang secara intuitif diharapkan dengan menggunakan fungsi dan data yang tersedia terbatas dan hanya berhenti dengan kesalahan ketika ada ambiguitas nyata.
Ini membantu membatasi kebutuhan kode polimorfik yang mendukung kode polimorfik, menarik jaring yang lebih ketat di sekitar penggunaan polimorfisme sehingga penggunaan yang terlokalisasi tidak memaksa penggunaan yang luas, dan membuat manfaat polimorfisme tersedia sesuai kebutuhan tanpa membebankan biaya karena harus mengekspos implementasi di mengkompilasi waktu, memiliki banyak salinan dari fungsi logis yang sama dalam kode objek untuk mendukung jenis yang digunakan, dan dalam melakukan pengiriman virtual sebagai lawan inlining atau setidaknya kompilasi waktu penyelesaian panggilan. Seperti biasa dalam C ++, programmer diberi banyak kebebasan untuk mengontrol batas-batas di mana polimorfisme digunakan.