Catatan: berikut ini adalah kode C ++ 03, tetapi kami mengharapkan perpindahan ke C ++ 11 dalam dua tahun ke depan, jadi kami harus mengingatnya.
Saya menulis panduan (untuk pemula, antara lain) tentang cara menulis antarmuka abstrak di C ++. Saya telah membaca kedua artikel Sutter tentang masalah ini, mencari di internet untuk contoh dan jawaban, dan membuat beberapa tes.
Kode ini TIDAK boleh dikompilasi!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Semua perilaku di atas menemukan sumber masalah mereka dalam mengiris : Antarmuka abstrak (atau kelas non-daun dalam hierarki) tidak boleh dibangun atau dapat diopi / ditugaskan, BAHKAN jika kelas turunannya bisa.
Solusi 0: antarmuka dasar
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Solusi ini sederhana, dan agak naif: Ini gagal semua kendala kami: Ini dapat dibangun secara default, salin-dibangun dan salin-ditugaskan (saya bahkan tidak yakin tentang memindahkan konstruktor dan tugas, tetapi saya masih memiliki 2 tahun untuk mencari keluar).
- Kami tidak dapat mendeklarasikan destructor pure virtual karena kami harus tetap inline, dan beberapa kompiler kami tidak akan mencerna metode virtual murni dengan inline empty body.
- Ya, satu-satunya poin dari kelas ini adalah membuat implementator benar-benar dapat dirusak, yang merupakan kasus yang jarang terjadi.
- Bahkan jika kita memiliki metode virtual murni tambahan (yang merupakan mayoritas kasus), kelas ini akan tetap dapat ditugaskan copy.
Jadi tidak ...
Solusi 1: boost :: noncopyable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Solusi ini adalah yang terbaik, karena jelas, jelas, dan C ++ (tidak ada makro)
Masalahnya adalah bahwa itu masih tidak berfungsi untuk antarmuka spesifik itu karena VirtuallyConstructible masih dapat dibangun secara default .
- Kami tidak dapat mendeklarasikan virtual destructor murni karena kami harus menjaganya tetap inline, dan beberapa kompiler kami tidak akan mencernanya.
- Ya, satu-satunya poin dari kelas ini adalah membuat implementator benar-benar dapat dirusak, yang merupakan kasus yang jarang terjadi.
Masalah lain adalah bahwa kelas yang mengimplementasikan antarmuka yang tidak dapat disalin kemudian harus secara eksplisit mendeklarasikan / mendefinisikan copy constructor dan operator penugasan jika mereka perlu memiliki metode tersebut (dan dalam kode kami, kami memiliki kelas nilai yang masih dapat diakses oleh klien kami melalui antarmuka).
Ini bertentangan dengan Aturan Nol, yang merupakan tujuan yang ingin kita tuju: Jika implementasi default ok, maka kita harus dapat menggunakannya.
Solusi 2: buat mereka terlindungi!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Pola ini mengikuti kendala teknis yang kami miliki (setidaknya dalam kode pengguna): MyInterface tidak dapat dibangun secara default, tidak dapat dibuat-salin, dan tidak dapat ditugaskan-salin.
Selain itu, tidak ada batasan buatan untuk mengimplementasikan kelas , yang kemudian bebas untuk mengikuti Aturan Nol, atau bahkan mendeklarasikan beberapa konstruktor / operator sebagai "= default" di C ++ 11/14 tanpa masalah.
Sekarang, ini sangat bertele-tele, dan sebuah alternatif akan menggunakan makro, sesuatu seperti:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
Yang dilindungi harus tetap berada di luar makro (karena tidak memiliki cakupan).
Benar "namespaced" (yaitu, diawali dengan nama perusahaan atau produk Anda), makro seharusnya tidak berbahaya.
Dan keuntungannya adalah bahwa kode tersebut diperhitungkan dalam satu sumber, alih-alih disalin dari semua antarmuka. Jika move-constructor dan move-assignment secara eksplisit dinonaktifkan dengan cara yang sama di masa depan, ini akan menjadi perubahan yang sangat ringan dalam kode.
Kesimpulan
- Apakah saya paranoid ingin kode dilindungi dari pemotongan di antarmuka? (Aku yakin bukan, tapi tidak ada yang tahu ...)
- Apa solusi terbaik di antara yang di atas?
- Apakah ada solusi lain yang lebih baik?
Harap diingat bahwa ini adalah pola yang akan berfungsi sebagai pedoman bagi pemula (antara lain), jadi solusi seperti: "Setiap kasus harus memiliki implementasinya" bukan solusi yang layak.
Hadiah dan hasil
Saya memberikan hadiah untuk coredump karena waktu yang dihabiskan untuk menjawab pertanyaan, dan relevansi dari jawaban.
Solusi saya untuk masalah mungkin akan pergi ke sesuatu seperti itu:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... dengan makro berikut:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
Ini adalah solusi yang layak untuk masalah saya karena alasan berikut:
- Kelas ini tidak dapat dipakai (konstruktor dilindungi)
- Kelas ini dapat dihancurkan secara virtual
- Kelas ini dapat diwarisi tanpa memaksakan batasan yang tidak semestinya pada kelas-kelas yang diwarisi (mis. Kelas yang diwariskan secara default dapat disalin)
- Penggunaan makro berarti antarmuka "deklarasi" mudah dikenali (dan dapat dicari), dan kodenya diperhitungkan di satu tempat sehingga lebih mudah untuk dimodifikasi (nama yang diawali dengan tepat akan menghapus bentrokan nama yang tidak diinginkan)
Perhatikan bahwa jawaban lain memberi wawasan berharga. Terima kasih untuk semua yang telah mencobanya.
Perhatikan bahwa saya kira saya masih bisa memberi hadiah lain untuk pertanyaan ini, dan saya menghargai jawaban yang cukup mencerahkan yang harus saya lihat, saya akan membuka hadiah hanya untuk memberikannya pada jawaban itu.
virtual ~VirtuallyDestructible() = 0
warisan virtual dari kelas antarmuka (hanya dengan anggota abstrak). Anda mungkin menghilangkan VirtuallyDestructible, mungkin.
virtual void bar() = 0;
sebagai contoh? Itu akan mencegah antarmuka Anda dari instanciated.