Bagaimana cara mengatur kelas yang mewakili antarmuka? Apakah ini hanya kelas dasar abstrak?
Bagaimana cara mengatur kelas yang mewakili antarmuka? Apakah ini hanya kelas dasar abstrak?
Jawaban:
Untuk memperluas jawaban oleh bradtgmurray , Anda mungkin ingin membuat satu pengecualian untuk daftar metode virtual murni antarmuka Anda dengan menambahkan destruktor virtual. Ini memungkinkan Anda untuk meneruskan kepemilikan pointer ke pihak lain tanpa memaparkan kelas turunannya. Destructor tidak perlu melakukan apa-apa, karena antarmuka tidak memiliki anggota konkret. Mungkin tampak kontradiktif untuk mendefinisikan fungsi sebagai virtual dan inline, tetapi percayalah - tidak.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Parent
{
public:
virtual ~Parent();
};
class Child : public Parent, public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
Anda tidak harus menyertakan body untuk destruktor virtual - ternyata beberapa kompiler mengalami kesulitan dalam mengoptimalkan destruktor kosong dan Anda lebih baik menggunakan default.
=0
destructor virtual ( ) murni dengan sebuah body. Keuntungannya di sini adalah bahwa kompiler dapat, secara teoritis, melihat bahwa vtable tidak memiliki anggota yang valid sekarang, dan membuangnya sama sekali. Dengan destruktor virtual dengan tubuh, kata destruktor dapat disebut (hampir) misalnya di tengah konstruksi melalui this
pointer (ketika objek yang dibangun masih Parent
bertipe), dan oleh karena itu kompiler harus menyediakan vtable yang valid. Jadi, jika Anda tidak secara eksplisit memanggil destruktor virtual melalui this
selama konstruksi :) Anda dapat menghemat ukuran kode.
override
kata kunci untuk memungkinkan argumen waktu kompilasi dan memeriksa jenis nilai kembali. Misalnya, dalam deklarasi Childvirtual void OverrideMe() override;
Buat kelas dengan metode virtual murni. Gunakan antarmuka dengan membuat kelas lain yang menimpa metode virtual tersebut.
Metode virtual murni adalah metode kelas yang didefinisikan sebagai virtual dan ditugaskan ke 0.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
override
di C ++ 11
Seluruh alasan Anda memiliki kategori-jenis Antarmuka khusus selain kelas dasar abstrak di C # / Java adalah karena C # / Java tidak mendukung pewarisan berganda.
C ++ mendukung multiple inheritance, jadi tipe khusus tidak diperlukan. Kelas dasar abstrak tanpa metode non-abstrak (virtual murni) secara fungsional setara dengan antarmuka C # / Java.
Thread
contoh. Warisan berganda dapat berupa desain dan komposisi yang buruk. Itu semua tergantung kasus.
Tidak ada konsep "antarmuka" per se dalam C ++. AFAIK, antarmuka pertama kali diperkenalkan di Jawa untuk mengatasi kurangnya pewarisan berganda. Konsep ini ternyata sangat berguna, dan efek yang sama dapat dicapai dalam C ++ dengan menggunakan kelas dasar abstrak.
Kelas dasar abstrak adalah kelas di mana setidaknya satu fungsi anggota (metode dalam istilah Java) adalah fungsi virtual murni yang dideklarasikan menggunakan sintaksis berikut:
class A
{
virtual void foo() = 0;
};
Kelas dasar abstrak tidak dapat dipakai, yaitu Anda tidak bisa mendeklarasikan objek kelas A. Anda hanya bisa mendapatkan kelas dari A, tetapi setiap kelas turunan yang tidak menyediakan implementasi foo()
juga akan abstrak. Untuk berhenti menjadi abstrak, kelas turunan harus menyediakan implementasi untuk semua fungsi virtual murni yang diwarisi.
Perhatikan bahwa kelas dasar abstrak bisa lebih dari satu antarmuka, karena dapat berisi data anggota dan fungsi anggota yang bukan virtual murni. Setara dengan antarmuka akan menjadi kelas dasar abstrak tanpa data dengan hanya fungsi virtual murni.
Dan, seperti yang ditunjukkan oleh Mark Ransom, kelas dasar abstrak harus menyediakan destruktor virtual, sama seperti kelas dasar mana pun.
Sejauh yang saya bisa uji, sangat penting untuk menambahkan destructor virtual. Saya menggunakan objek yang dibuat dengan new
dan dihancurkan delete
.
Jika Anda tidak menambahkan destruktor virtual di antarmuka, maka destruktor dari kelas yang diturunkan tidak disebut.
class IBase {
public:
virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
virtual void Describe() = 0; // pure virtual method
};
class Tester : public IBase {
public:
Tester(std::string name);
virtual ~Tester();
virtual void Describe();
private:
std::string privatename;
};
Tester::Tester(std::string name) {
std::cout << "Tester constructor" << std::endl;
this->privatename = name;
}
Tester::~Tester() {
std::cout << "Tester destructor" << std::endl;
}
void Tester::Describe() {
std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}
void descriptor(IBase * obj) {
obj->Describe();
}
int main(int argc, char** argv) {
std::cout << std::endl << "Tester Testing..." << std::endl;
Tester * obj1 = new Tester("Declared with Tester");
descriptor(obj1);
delete obj1;
std::cout << std::endl << "IBase Testing..." << std::endl;
IBase * obj2 = new Tester("Declared with IBase");
descriptor(obj2);
delete obj2;
// this is a bad usage of the object since it is created with "new" but there are no "delete"
std::cout << std::endl << "Tester not defined..." << std::endl;
descriptor(new Tester("Not defined"));
return 0;
}
Jika Anda menjalankan kode sebelumnya tanpa virtual ~IBase() {};
, Anda akan melihat bahwa destructor Tester::~Tester()
tidak pernah dipanggil.
Jawaban saya pada dasarnya sama dengan yang lain tapi saya pikir ada dua hal penting yang harus dilakukan:
Deklarasikan destruktor virtual di antarmuka Anda atau buat yang non-virtual yang dilindungi untuk menghindari perilaku yang tidak terdefinisi jika seseorang mencoba menghapus objek tipe IDemo
.
Gunakan pewarisan virtual untuk menghindari masalah dengan pewarisan berganda. (Ada lebih sering pewarisan berganda ketika kita menggunakan antarmuka.)
Dan seperti jawaban lain:
Gunakan antarmuka dengan membuat kelas lain yang menimpa metode virtual tersebut.
class IDemo
{
public:
virtual void OverrideMe() = 0;
virtual ~IDemo() {}
}
Atau
class IDemo
{
public:
virtual void OverrideMe() = 0;
protected:
~IDemo() {}
}
Dan
class Child : virtual public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
}
Di C ++ 11 Anda dapat dengan mudah menghindari warisan sama sekali:
struct Interface {
explicit Interface(SomeType& other)
: foo([=](){ return other.my_foo(); }),
bar([=](){ return other.my_bar(); }), /*...*/ {}
explicit Interface(SomeOtherType& other)
: foo([=](){ return other.some_foo(); }),
bar([=](){ return other.some_bar(); }), /*...*/ {}
// you can add more types here...
// or use a generic constructor:
template<class T>
explicit Interface(T& other)
: foo([=](){ return other.foo(); }),
bar([=](){ return other.bar(); }), /*...*/ {}
const std::function<void(std::string)> foo;
const std::function<void(std::string)> bar;
// ...
};
Dalam hal ini, suatu Antarmuka memiliki semantik referensi, yaitu Anda harus memastikan bahwa objek hidup lebih lama dari antarmuka (juga dimungkinkan untuk membuat antarmuka dengan semantik nilai).
Jenis antarmuka ini memiliki pro dan kontra:
Akhirnya, warisan adalah akar dari semua kejahatan dalam desain perangkat lunak yang kompleks. Dalam Semantics Value Seant Parent dan Polymorphism berbasis Konsep (sangat disarankan, versi yang lebih baik dari teknik ini dijelaskan di sana) kasus berikut dipelajari:
Katakanlah saya memiliki aplikasi tempat saya menangani bentuk saya secara polimorfik menggunakan MyShape
antarmuka:
struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle
Dalam aplikasi Anda, Anda melakukan hal yang sama dengan berbagai bentuk menggunakan YourShape
antarmuka:
struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...
Sekarang katakan Anda ingin menggunakan beberapa bentuk yang saya kembangkan di aplikasi Anda. Secara konseptual, bentuk kami memiliki antarmuka yang sama, tetapi untuk membuat bentuk saya berfungsi di aplikasi Anda, Anda perlu memperluas bentuk saya sebagai berikut:
struct Circle : MyShape, YourShape {
void my_draw() { /*stays the same*/ };
void your_draw() { my_draw(); }
};
Pertama, memodifikasi bentuk saya mungkin tidak mungkin sama sekali. Lebih lanjut, multiple inheritance menuntun jalan ke kode spaghetti (bayangkan proyek ketiga datang menggunakan TheirShape
antarmuka ... apa yang terjadi jika mereka juga memanggil fungsi draw mereka my_draw
?).
Pembaruan: Ada beberapa referensi baru tentang polimorfisme berbasis non-warisan:
Circle
kelas adalah desain yang buruk. Anda harus menggunakan Adapter
pola dalam kasus seperti itu. Maaf jika ini akan terdengar agak keras, tetapi cobalah untuk menggunakan perpustakaan kehidupan nyata seperti Qt
sebelum membuat penilaian tentang warisan. Warisan membuat hidup lebih mudah.
Adapter
pola? Saya tertarik melihat kelebihannya.
Square
belum ada di sana? Ramalan? Karena itulah ia terlepas dari kenyataan. Dan pada kenyataannya jika Anda memilih untuk mengandalkan pustaka "MyShape" Anda dapat mengadopsi antarmuka-nya dari awal. Dalam contoh bentuk ada banyak nonsense (salah satunya adalah Anda memiliki dua Circle
struct), tetapi adaptor akan terlihat seperti itu -> ideone.com/UogjWk
Semua jawaban bagus di atas. Satu hal tambahan yang harus Anda ingat - Anda juga dapat memiliki destruktor virtual murni. Satu-satunya perbedaan adalah Anda masih perlu mengimplementasikannya.
Bingung?
--- header file ----
class foo {
public:
foo() {;}
virtual ~foo() = 0;
virtual bool overrideMe() {return false;}
};
---- source ----
foo::~foo()
{
}
Alasan utama Anda ingin melakukan ini adalah jika Anda ingin memberikan metode antarmuka, seperti yang saya miliki, tetapi menjadikannya overriding sebagai opsional.
Untuk membuat kelas sebagai kelas antarmuka membutuhkan metode virtual murni, tetapi semua metode virtual Anda memiliki implementasi standar, jadi satu-satunya metode yang tersisa untuk membuat virtual murni adalah destruktor.
Mengimplementasikan kembali destruktor di kelas turunan bukanlah masalah besar sama sekali - saya selalu menerapkan kembali destruktor, virtual atau tidak, di kelas turunan saya.
Jika Anda menggunakan kompiler C ++ Microsoft, maka Anda dapat melakukan hal berikut:
struct __declspec(novtable) IFoo
{
virtual void Bar() = 0;
};
class Child : public IFoo
{
public:
virtual void Bar() override { /* Do Something */ }
}
Saya suka pendekatan ini karena menghasilkan banyak kode antarmuka yang lebih kecil dan ukuran kode yang dihasilkan bisa jauh lebih kecil. Penggunaan novtable menghapus semua referensi ke pointer vtable di kelas itu, sehingga Anda tidak akan pernah bisa instantiate secara langsung. Lihat dokumentasi di sini - novtable .
novtable
standarvirtual void Bar() = 0;
= 0;
yang saya tambahkan). Baca dokumentasi jika Anda tidak memahaminya.
= 0;
dan menganggap itu hanya cara non-standar untuk melakukan hal yang persis sama.
Sedikit tambahan untuk apa yang ditulis di sana:
Pertama, pastikan destructor Anda juga virtual murni
Kedua, Anda mungkin ingin mewarisi secara virtual (bukan normal) ketika Anda menerapkan, hanya untuk langkah-langkah yang baik.
Anda juga dapat mempertimbangkan kelas kontrak yang diimplementasikan dengan NVI (Non Virtual Interface Pattern). Contohnya:
struct Contract1 : boost::noncopyable
{
virtual ~Contract1();
void f(Parameters p) {
assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
// + class invariants.
do_f(p);
// Check post-conditions + class invariants.
}
private:
virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
virtual void do_f(Parameters p); // From contract 1.
virtual void do_g(Parameters p); // From contract 2.
};
Saya masih baru dalam pengembangan C ++. Saya mulai dengan Visual Studio (VS).
Namun, tampaknya tidak ada yang disebutkan __interface
dalam VS (.NET) . Saya tidak begitu yakin apakah ini cara yang baik untuk mendeklarasikan antarmuka. Tetapi tampaknya memberikan penegakan tambahan (disebutkan dalam dokumen ). Sedemikian rupa sehingga Anda tidak perlu menentukan secara eksplisit virtual TYPE Method() = 0;
, karena itu akan secara otomatis dikonversi.
__interface IMyInterface {
HRESULT CommitX();
HRESULT get_X(BSTR* pbstrName);
};
Namun, saya tidak menggunakannya karena saya khawatir tentang kompatibilitas kompilasi lintas platform, karena hanya tersedia di .NET.
Jika ada yang punya sesuatu yang menarik tentang itu, silakan bagikan. :-)
Terima kasih.
Memang benar itu virtual
adalah standar de-facto untuk mendefinisikan antarmuka, jangan lupa tentang pola klasik C-like, yang dilengkapi dengan konstruktor di C ++:
struct IButton
{
void (*click)(); // might be std::function(void()) if you prefer
IButton( void (*click_)() )
: click(click_)
{
}
};
// call as:
// (button.*click)();
Ini memiliki keuntungan bahwa Anda dapat mengikat kembali runtime acara tanpa harus membangun kelas Anda lagi (karena C ++ tidak memiliki sintaks untuk mengubah tipe polimorfik, ini merupakan solusi untuk kelas bunglon).
Kiat:
click
konstruktor keturunan Anda.protected
anggota dan memiliki public
referensi dan / atau pengambil.if
perubahan s vs status dalam kode Anda, ini mungkin lebih cepat daripada switch()
es atau if
s (turnaround diharapkan sekitar 3-4 if
s, tetapi selalu ukur terlebih dahulu.std::function<>
lebih dari pointer fungsi, Anda mungkin dapat mengelola semua data objek Anda di dalamnya IBase
. Dari titik ini, Anda dapat memiliki skema nilai untuk IBase
(misalnya, std::vector<IBase>
akan berfungsi). Perhatikan bahwa ini mungkin lebih lambat tergantung pada kompiler dan kode STL Anda; juga bahwa implementasi saat ini std::function<>
cenderung memiliki overhead jika dibandingkan dengan pointer fungsi atau bahkan fungsi virtual (ini mungkin berubah di masa depan).Berikut adalah definisi abstract class
dalam standar c ++
n4687
13.4.2
Kelas abstrak adalah kelas yang hanya dapat digunakan sebagai kelas dasar dari beberapa kelas lain; tidak ada objek dari kelas abstrak yang dapat dibuat kecuali sebagai sub-objek dari kelas yang diturunkan darinya. Kelas adalah abstrak jika memiliki setidaknya satu fungsi virtual murni.
class Shape
{
public:
// pure virtual function providing interface framework.
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
cout << "Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
cout << "Triangle area: " << Tri.getArea() << endl;
return 0;
}
Hasil: Area segi empat: 35 Area segitiga: 17
Kami telah melihat bagaimana kelas abstrak mendefinisikan antarmuka dalam hal getArea () dan dua kelas lainnya menerapkan fungsi yang sama tetapi dengan algoritma yang berbeda untuk menghitung area khusus untuk bentuk.