Tidak seperti warisan yang dilindungi, warisan pribadi C ++ masuk ke dalam pengembangan C ++ arus utama. Namun, saya masih belum menemukan manfaat yang baik untuk itu.
Kapan kalian menggunakannya?
Tidak seperti warisan yang dilindungi, warisan pribadi C ++ masuk ke dalam pengembangan C ++ arus utama. Namun, saya masih belum menemukan manfaat yang baik untuk itu.
Kapan kalian menggunakannya?
Jawaban:
Catatan setelah penerimaan jawaban: Ini BUKAN jawaban lengkap. Bacalah jawaban lain seperti di sini (secara konseptual) dan di sini (baik teoretis maupun praktis) jika Anda tertarik dengan pertanyaannya. Ini hanyalah trik mewah yang dapat dicapai dengan warisan pribadi. Meskipun mewah , ini bukanlah jawaban atas pertanyaan tersebut.
Selain penggunaan dasar dari warisan pribadi yang ditunjukkan di C ++ FAQ (ditautkan di komentar orang lain), Anda dapat menggunakan kombinasi warisan pribadi dan virtual untuk menyegel kelas (dalam terminologi .NET) atau untuk membuat kelas final (dalam terminologi Java) . Ini bukan penggunaan umum, tapi bagaimanapun juga menurut saya ini menarik:
class ClassSealer {
private:
friend class Sealed;
ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{
// ...
};
class FailsToDerive : public Sealed
{
// Cannot be instantiated
};
Sealed dapat dipakai. Ini berasal dari ClassSealer dan dapat memanggil konstruktor pribadi secara langsung karena ini adalah teman.
FailsToDerive tidak akan dikompilasi karena harus memanggil konstruktor ClassSealer secara langsung (persyaratan warisan virtual), tetapi tidak bisa karena bersifat pribadi di kelas Sealed dan dalam hal ini FailsToDerive bukan teman ClassSealer .
EDIT
Disebutkan dalam komentar bahwa ini tidak dapat dibuat generik pada saat menggunakan CRTP. Standar C ++ 11 menghilangkan batasan itu dengan menyediakan sintaks yang berbeda untuk berteman dengan argumen template:
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
Tentu saja ini semua bisa diperdebatkan, karena C ++ 11 menyediakan final
kata kunci kontekstual untuk tujuan ini:
class Sealed final // ...
Saya menggunakannya sepanjang waktu. Beberapa contoh di luar kepala saya:
Contoh tipikal diturunkan secara pribadi dari wadah STL:
class MyVector : private vector<int>
{
public:
// Using declarations expose the few functions my clients need
// without a load of forwarding functions.
using vector<int>::push_back;
// etc...
};
push_back
, MyVector
dapatkan secara gratis.
template<typename... Args> constexpr decltype(auto) f(Args && ... args) noexcept(noexcept(std::declval<Base &>().f(std::forward<Args>(args)...)) and std::is_nothrow_move_constructible<decltype(std::declval<Base &>().f(std::forward<Args>(args)...))>) { return m_base.f(std::forward<Args>(args)...); }
atau menulis menggunakan Base::f;
. Jika Anda menginginkan sebagian besar fungsionalitas dan fleksibilitas yang diberikan oleh private inheritance dan sebuah using
pernyataan, Anda memiliki monster itu untuk setiap fungsi (dan jangan lupa tentang const
dan volatile
kelebihan beban!).
Penggunaan kanonik warisan pribadi adalah hubungan "diimplementasikan dalam" (terima kasih kepada Scott Meyers 'C ++ Efektif' untuk kata-kata ini). Dengan kata lain, antarmuka eksternal dari kelas yang mewarisi tidak memiliki hubungan (terlihat) dengan kelas yang diwarisi, tetapi ia menggunakannya secara internal untuk mengimplementasikan fungsinya.
Salah satu kegunaan private inheritence adalah ketika Anda memiliki kelas yang mengimplementasikan antarmuka, yang kemudian didaftarkan dengan beberapa objek lain. Anda membuat antarmuka itu menjadi pribadi sehingga kelas itu sendiri harus mendaftar dan hanya objek tertentu yang didaftarkannya yang dapat menggunakan fungsi tersebut.
Sebagai contoh:
class FooInterface
{
public:
virtual void DoSomething() = 0;
};
class FooUser
{
public:
bool RegisterFooInterface(FooInterface* aInterface);
};
class FooImplementer : private FooInterface
{
public:
explicit FooImplementer(FooUser& aUser)
{
aUser.RegisterFooInterface(this);
}
private:
virtual void DoSomething() { ... }
};
Oleh karena itu, kelas FooUser dapat memanggil metode pribadi FooImplementer melalui antarmuka FooInterface, sedangkan kelas eksternal lainnya tidak bisa. Ini adalah pola yang bagus untuk menangani callback tertentu yang didefinisikan sebagai antarmuka.
Saya pikir bagian penting dari C ++ FAQ Lite adalah:
Penggunaan jangka panjang yang sah untuk warisan pribadi adalah saat Anda ingin membangun kelas Fred yang menggunakan kode di kelas Wilma, dan kode dari kelas Wilma perlu memanggil fungsi anggota dari kelas baru Anda, Fred. Dalam kasus ini, Fred memanggil non-virtual di Wilma, dan Wilma memanggil (biasanya virtual murni) dengan sendirinya, yang diganti oleh Fred. Ini akan lebih sulit dilakukan dengan komposisi.
Jika ragu, Anda sebaiknya memilih komposisi daripada warisan pribadi.
Saya merasa berguna untuk antarmuka (yaitu kelas abstrak) yang saya warisi di mana saya tidak ingin kode lain menyentuh antarmuka (hanya kelas yang mewarisi).
[diedit dalam contoh]
Ambil contoh yang ditautkan di atas. Mengatakan itu
[...] kelas Wilma perlu memanggil fungsi anggota dari kelas baru Anda, Fred.
adalah mengatakan bahwa Wilma meminta Fred untuk dapat menjalankan fungsi anggota tertentu, atau, lebih tepatnya mengatakan bahwa Wilma adalah sebuah antarmuka . Karenanya, seperti yang disebutkan dalam contoh
warisan pribadi tidak jahat; itu hanya lebih mahal untuk dipelihara, karena meningkatkan kemungkinan seseorang akan mengubah sesuatu yang akan merusak kode Anda.
komentar tentang efek yang diinginkan dari pemrogram yang harus memenuhi persyaratan antarmuka kami, atau memecahkan kode. Dan, karena fredCallsWilma () dilindungi hanya teman dan kelas turunan yang dapat menyentuhnya, yaitu antarmuka yang diwariskan (kelas abstrak) yang hanya dapat disentuh oleh kelas yang mewarisi (dan teman).
[diedit di contoh lain]
Halaman ini secara singkat membahas antarmuka pribadi (dari sudut lain).
Kadang-kadang saya merasa berguna untuk menggunakan private inheritance ketika saya ingin mengekspos antarmuka yang lebih kecil (misalnya koleksi) di antarmuka lain, di mana implementasi collection memerlukan akses ke status kelas yang mengekspos, dengan cara yang mirip dengan kelas dalam di Jawa.
class BigClass;
struct SomeCollection
{
iterator begin();
iterator end();
};
class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};
Kemudian jika SomeCollection perlu mengakses BigClass, itu bisa static_cast<BigClass *>(this)
. Tidak perlu ada anggota data tambahan yang menghabiskan ruang.
BigClass
apakah ada dalam contoh ini? Menurut saya ini menarik, tetapi wajah saya menjerit keras.
Saya menemukan aplikasi bagus untuk warisan pribadi, meskipun penggunaannya terbatas.
Misalkan Anda diberi C API berikut:
#ifdef __cplusplus
extern "C" {
#endif
typedef struct
{
/* raw owning pointer, it's C after all */
char const * name;
/* more variables that need resources
* ...
*/
} Widget;
Widget const * loadWidget();
void freeWidget(Widget const * widget);
#ifdef __cplusplus
} // end of extern "C"
#endif
Sekarang tugas Anda adalah mengimplementasikan API ini menggunakan C ++.
Tentu saja kita bisa memilih gaya implementasi C-ish seperti ini:
Widget const * loadWidget()
{
auto result = std::make_unique<Widget>();
result->name = strdup("The Widget name");
// More similar assignments here
return result.release();
}
void freeWidget(Widget const * const widget)
{
free(result->name);
// More similar manual freeing of resources
delete widget;
}
Tetapi ada beberapa kelemahan:
struct
kesalahanstruct
Kami diizinkan menggunakan C ++, jadi mengapa tidak menggunakan kekuatan penuhnya?
Masalah di atas pada dasarnya semua terkait dengan manajemen sumber daya manual. Solusi yang terlintas dalam pikiran adalah mewarisi dari Widget
dan menambahkan instance pengelolaan sumber daya ke kelas turunan WidgetImpl
untuk setiap variabel:
class WidgetImpl : public Widget
{
public:
// Added bonus, Widget's members get default initialized
WidgetImpl()
: Widget()
{}
void setName(std::string newName)
{
m_nameResource = std::move(newName);
name = m_nameResource.c_str();
}
// More similar setters to follow
private:
std::string m_nameResource;
};
Ini menyederhanakan implementasi sebagai berikut:
Widget const * loadWidget()
{
auto result = std::make_unique<WidgetImpl>();
result->setName("The Widget name");
// More similar setters here
return result.release();
}
void freeWidget(Widget const * const widget)
{
// No virtual destructor in the base class, thus static_cast must be used
delete static_cast<WidgetImpl const *>(widget);
}
Seperti ini kami memperbaiki semua masalah di atas. Namun klien masih bisa melupakan penyetel WidgetImpl
dan menetapkan keWidget
anggota secara langsung.
Untuk merangkum Widget
anggota kami menggunakan warisan pribadi. Sayangnya kita sekarang membutuhkan dua fungsi tambahan untuk digunakan di antara kedua kelas:
class WidgetImpl : private Widget
{
public:
WidgetImpl()
: Widget()
{}
void setName(std::string newName)
{
m_nameResource = std::move(newName);
name = m_nameResource.c_str();
}
// More similar setters to follow
Widget const * toWidget() const
{
return static_cast<Widget const *>(this);
}
static void deleteWidget(Widget const * const widget)
{
delete static_cast<WidgetImpl const *>(widget);
}
private:
std::string m_nameResource;
};
Ini membuat adaptasi berikut diperlukan:
Widget const * loadWidget()
{
auto widgetImpl = std::make_unique<WidgetImpl>();
widgetImpl->setName("The Widget name");
// More similar setters here
auto const result = widgetImpl->toWidget();
widgetImpl.release();
return result;
}
void freeWidget(Widget const * const widget)
{
WidgetImpl::deleteWidget(widget);
}
Solusi ini menyelesaikan semua masalah. Tidak ada manajemen memori manual dan Widget
dikemas dengan baik sehingga WidgetImpl
tidak memiliki anggota data publik lagi. Itu membuat implementasi mudah digunakan dengan benar dan sulit (tidak mungkin?) Untuk digunakan salah.
Potongan kode membentuk contoh kompilasi di Coliru .
Jika kelas turunan - perlu menggunakan kembali kode dan - Anda tidak dapat mengubah kelas dasar dan - melindungi metodenya menggunakan anggota basis di bawah kunci.
maka Anda harus menggunakan warisan pribadi, jika tidak, Anda akan menghadapi bahaya metode dasar tidak terkunci yang diekspor melalui kelas turunan ini.
Private Inheritance untuk digunakan jika relasi bukan "adalah", tetapi kelas baru dapat "diimplementasikan dalam istilah kelas yang sudah ada" atau kelas baru "berfungsi seperti" kelas yang sudah ada.
Contoh dari "standar pengkodean C ++ oleh Andrei Alexandrescu, Herb Sutter": - Pertimbangkan bahwa dua kelas Square dan Rectangle masing-masing memiliki fungsi virtual untuk mengatur tinggi dan lebarnya. Maka Square tidak dapat mewarisi dengan benar dari Rectangle, karena kode yang menggunakan Rectangle yang dapat dimodifikasi akan mengasumsikan bahwa SetWidth tidak mengubah ketinggian (apakah Rectangle secara eksplisit mendokumentasikan kontrak itu atau tidak), sedangkan Square :: SetWidth tidak dapat mempertahankan kontrak itu dan invarian kuadratnya sendiri di waktu yang sama. Tetapi Persegi juga tidak dapat mewarisi dengan benar dari Persegi, jika klien Persegi mengasumsikan misalnya bahwa luas persegi adalah lebar kuadratnya, atau jika mereka bergantung pada beberapa properti lain yang tidak berlaku untuk Persegi.
Persegi "adalah-a" persegi panjang (secara matematis) tetapi Persegi bukanlah Persegi (secara perilaku). Akibatnya, daripada "is-a", kami lebih memilih untuk mengatakan "works-like-a" (atau, jika Anda lebih suka, "usable-as-a") untuk membuat deskripsi tidak terlalu rentan terhadap kesalahpahaman.
Kelas memiliki invarian. Invarian ditetapkan oleh konstruktor. Namun, dalam banyak situasi, akan berguna untuk memiliki pandangan tentang status representasi objek (yang dapat Anda kirimkan melalui jaringan atau simpan ke file - DTO jika Anda mau). REST paling baik dilakukan dalam hal AggregateType. Ini terutama benar jika Anda benar konst. Mempertimbangkan:
struct QuadraticEquationState {
const double a;
const double b;
const double c;
// named ctors so aggregate construction is available,
// which is the default usage pattern
// add your favourite ctors - throwing, try, cps
static QuadraticEquationState read(std::istream& is);
static std::optional<QuadraticEquationState> try_read(std::istream& is);
template<typename Then, typename Else>
static std::common_type<
decltype(std::declval<Then>()(std::declval<QuadraticEquationState>()),
decltype(std::declval<Else>()())>::type // this is just then(qes) or els(qes)
if_read(std::istream& is, Then then, Else els);
};
// this works with QuadraticEquation as well by default
std::ostream& operator<<(std::ostream& os, const QuadraticEquationState& qes);
// no operator>> as we're const correct.
// we _might_ (not necessarily want) operator>> for optional<qes>
std::istream& operator>>(std::istream& is, std::optional<QuadraticEquationState>);
struct QuadraticEquationCache {
mutable std::optional<double> determinant_cache;
mutable std::optional<double> x1_cache;
mutable std::optional<double> x2_cache;
mutable std::optional<double> sum_of_x12_cache;
};
class QuadraticEquation : public QuadraticEquationState, // private if base is non-const
private QuadraticEquationCache {
public:
QuadraticEquation(QuadraticEquationState); // in general, might throw
QuadraticEquation(const double a, const double b, const double c);
QuadraticEquation(const std::string& str);
QuadraticEquation(const ExpressionTree& str); // might throw
}
Pada titik ini, Anda mungkin hanya menyimpan koleksi cache dalam container dan mencarinya saat konstruksi. Berguna jika ada pemrosesan nyata. Perhatikan bahwa cache adalah bagian dari QE: operasi yang didefinisikan pada QE mungkin berarti cache sebagian dapat digunakan kembali (misalnya, c tidak mempengaruhi jumlah); namun, jika tidak ada cache, ada baiknya untuk mencarinya.
Warisan pribadi hampir selalu dapat dimodelkan oleh anggota (menyimpan referensi ke pangkalan jika diperlukan). Tidak selalu layak untuk membuat model seperti itu; terkadang warisan adalah representasi yang paling efisien.
Jika Anda membutuhkan std::ostream
dengan beberapa perubahan kecil (seperti dalam pertanyaan ini ) Anda mungkin perlu
MyStreambuf
yang berasal daristd::streambuf
dan terapkan perubahan di sanaMyOStream
yang diturunkan dari std::ostream
itu juga menginisialisasi dan mengelola sebuah instance MyStreambuf
dan meneruskan pointer ke instance itu ke konstruktorstd::ostream
Ide pertama mungkin untuk menambahkan MyStream
instance sebagai anggota data ke MyOStream
kelas:
class MyOStream : public std::ostream
{
public:
MyOStream()
: std::basic_ostream{ &m_buf }
, m_buf{}
{}
private:
MyStreambuf m_buf;
};
Tapi kelas dasar dibangun sebelum anggota data apa pun sehingga Anda meneruskan pointer ke std::streambuf
instance yang belum dibangunstd::ostream
yang yang merupakan perilaku tidak terdefinisi.
Solusinya diusulkan dalam jawaban Ben atas pertanyaan yang disebutkan di atas , cukup mewarisi dari buffer aliran terlebih dahulu, kemudian dari aliran dan kemudian menginisialisasi aliran dengan this
:
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Namun kelas yang dihasilkan juga bisa digunakan sebagai std::streambuf
instance yang biasanya tidak diinginkan. Beralih ke warisan pribadi memecahkan masalah ini:
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Hanya karena C ++ memiliki fitur, bukan berarti itu berguna atau harus digunakan.
Saya akan mengatakan Anda tidak boleh menggunakannya sama sekali.
Jika Anda tetap menggunakannya, pada dasarnya Anda melanggar enkapsulasi, dan menurunkan kohesi. Anda meletakkan data di satu kelas, dan menambahkan metode yang memanipulasi data di kelas lain.
Seperti fitur C ++ lainnya, ini dapat digunakan untuk mencapai efek samping seperti menyegel kelas (seperti yang disebutkan dalam jawaban dribeas), tetapi ini tidak menjadikannya fitur yang bagus.