Korunan kalıtımın aksine, C ++ özel kalıtım ana akım C ++ geliştirmede yolunu buldu. Ancak yine de bunun için iyi bir kullanım bulamadım.
Ne zaman kullanıyorsunuz?
Korunan kalıtımın aksine, C ++ özel kalıtım ana akım C ++ geliştirmede yolunu buldu. Ancak yine de bunun için iyi bir kullanım bulamadım.
Ne zaman kullanıyorsunuz?
Yanıtlar:
Cevabı kabul ettikten sonra not: Bu tam bir cevap DEĞİLDİR. Soruyla ilgileniyorsanız, burada (kavramsal olarak) ve burada (hem teorik hem de pratik) gibi diğer yanıtları okuyun . Bu sadece özel mirasla elde edilebilecek süslü bir numaradır. Öyle olsa fantezi o sorunun cevabı değildir.
C ++ SSS'de gösterilen (başkalarının yorumlarında bağlantılı olan) yalnızca özel mirasın temel kullanımının yanı sıra, bir sınıfı mühürlemek (.NET terminolojisinde) veya bir sınıfı son haline getirmek (Java terminolojisinde) için özel ve sanal mirasın bir kombinasyonunu kullanabilirsiniz. . Bu yaygın bir kullanım değil, ama yine de ilginç buldum:
class ClassSealer {
private:
friend class Sealed;
ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{
// ...
};
class FailsToDerive : public Sealed
{
// Cannot be instantiated
};
Mühürlü somutlaştırılabilir. Bu kaynaklanmaktadır ClassSealer ve bir arkadaşı olarak doğrudan özel yapıcı çağırabilir.
FailsToDerive o çağırmalıdır olarak derlemek olmaz ClassSealer doğrudan yapıcı (sanal miras gereksinimi), ama buna özel değil olarak can Mühürlü sınıf ve bu durumda FailsToDerive arkadaş değildir ClassSealer .
DÜZENLE
Yorumlarda bunun CRTP kullanılarak jenerik hale getirilemeyeceği belirtilmişti. C ++ 11 standardı, şablon bağımsız değişkenleriyle arkadaş olmak için farklı bir sözdizimi sağlayarak bu sınırlamayı ortadan kaldırır:
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
Elbette bunların hepsi tartışmalı, çünkü C ++ 11 final
tam da bu amaç için bağlamsal bir anahtar kelime sağlıyor:
class Sealed final // ...
Ben her zaman kullanırım. Aklıma gelen birkaç örnek:
Tipik bir örnek, bir STL konteynerinden özel olarak türetilmesidir:
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
bunları ücretsiz alır.
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)...); }
veya kullanarak yazabilirsin Base::f;
. Özel kalıtımın ve bir using
ifadenin size sağladığı işlevsellik ve esnekliğin çoğunu istiyorsanız, her işlev için o canavara sahipsiniz (ve unutmayın const
ve volatile
aşırı yükler!).
Özel mirasın kanonik kullanımı, ilişkinin "bağlamında uygulanmış" şeklidir (bu ifade için Scott Meyers'in "Etkili C ++" sayesinde). Başka bir deyişle, miras alan sınıfın dış arabiriminin devralınan sınıfla (görünür) ilişkisi yoktur, ancak işlevselliğini uygulamak için dahili olarak kullanır.
Özel kalıtımın yararlı bir kullanımı, bir arabirim uygulayan ve daha sonra başka bir nesneyle kaydedilen bir sınıfa sahip olduğunuz zamandır. Bu arabirimi özel yaparsınız, böylece sınıfın kendisinin kaydolması gerekir ve yalnızca kayıtlı olduğu belirli nesne bu işlevleri kullanabilir.
Örneğin:
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() { ... }
};
Bu nedenle, FooUser sınıfı, FooInterface arabirimi aracılığıyla FooImplementer özel yöntemlerini çağırabilirken, diğer harici sınıflar bunu yapamaz. Bu, arabirimler olarak tanımlanan belirli geri aramaları işlemek için harika bir modeldir.
Bence C ++ FAQ Lite'daki kritik bölüm :
Özel miras için meşru, uzun vadeli bir kullanım, bir Wilma sınıfında kod kullanan bir Fred sınıfı oluşturmak istediğinizde ve Wilma sınıfından gelen kodun, yeni sınıfınız Fred'in üye işlevlerini çağırması gerektiğidir. Bu durumda, Fred, Wilma'daki sanal olmayanları çağırır ve Wilma, Fred tarafından geçersiz kılınan kendi içinde (genellikle saf sanallar) çağırır. Bunu kompozisyonla yapmak çok daha zor.
Şüpheniz varsa, özel miras yerine kompozisyonu tercih etmelisiniz.
Diğer kodun arayüze dokunmasını istemediğim yerlerde (yalnızca miras alan sınıf) miras aldığım arayüzler (yani soyut sınıflar) için yararlı buluyorum.
[bir örnekte düzenlenmiştir]
Yukarıda bağlantılı örneği ele alalım . Bunu söylüyorum
[...] sınıf Wilma, yeni sınıfınız Fred'in üye işlevlerini çağırmalıdır.
Wilma'nın, Fred'in belirli üye işlevlerini çağırabilmesini istediğini söylemek, ya da daha ziyade Wilma'nın bir arayüz olduğunu söylemektir . Dolayısıyla, örnekte belirtildiği gibi
özel miras kötü değildir; Birisinin kodunuzu kıracak bir şeyi değiştirme olasılığını artırdığı için bakımı daha pahalıdır.
arayüz gereksinimlerimizi karşılamaya ihtiyaç duyan veya kodu kıran programcıların istenen etkisine ilişkin yorumlar. Ve, fredCallsWilma () korumalı olduğundan, sadece arkadaşlar ve türetilmiş sınıflar ona dokunabilir, yani sadece miras alan sınıfın (ve arkadaşların) dokunabileceği miras alınmış bir arayüz (soyut sınıf).
[başka bir örnekte düzenlenmiştir]
Bu sayfada özel arayüzler kısaca tartışılmaktadır (başka bir açıdan).
Bazen, başka bir arabirimin arabiriminde daha küçük bir arabirim (örneğin bir koleksiyon) göstermek istediğimde özel kalıtımı kullanmayı yararlı buluyorum; Java.
class BigClass;
struct SomeCollection
{
iterator begin();
iterator end();
};
class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};
Sonra SomeCollection'ın BigClass'a erişmesi gerekiyorsa, bunu yapabilir static_cast<BigClass *>(this)
. Fazladan bir veri üyesinin yer kaplamasına gerek yok.
BigClass
Bu örnekte var olduğuna dair ileri beyana gerek yok mu? Bunu ilginç buluyorum, ama yüzüme haince çığlık atıyor.
Sınırlı bir kullanımı olmasına rağmen özel miras için güzel bir uygulama buldum.
Aşağıdaki C API'sinin size verildiğini varsayalım:
#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
Şimdi işiniz bu API'yi C ++ kullanarak uygulamak.
Elbette şu şekilde bir C-ish uygulama stili seçebiliriz:
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;
}
Ancak birkaç dezavantaj var:
struct
Yanlış ayarlamak çok kolaystruct
C ++ kullanmamıza izin verildi, öyleyse neden tüm güçlerini kullanmayalım?
Yukarıdaki sorunların tümü temelde manuel kaynak yönetimine bağlıdır. Akla gelen çözüm, her değişken için Widget
türetilmiş sınıfa bir kaynak yönetimi örneğini miras almak ve buna eklemektir WidgetImpl
:
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;
};
Bu, uygulamayı şu şekilde basitleştirir:
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);
}
Bunun gibi yukarıdaki tüm sorunları çözdük. Ancak bir müşteri, kurucuları unutabilir WidgetImpl
ve Widget
üyelere doğrudan atayabilir .
Widget
Üyeleri kapsüllemek için özel miras kullanıyoruz. Ne yazık ki artık iki sınıf arasında dönüşüm yapmak için iki ekstra işleve ihtiyacımız var:
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;
};
Bu, aşağıdaki uyarlamaları gerekli kılar:
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);
}
Bu çözüm tüm sorunları çözer. Manuel bellek yönetimi yoktur ve Widget
güzel bir şekilde kapsüllenmiştir, böyleceWidgetImpl
artık herhangi bir genel veri üyesi yok. Uygulamanın doğru kullanılmasını kolaylaştırır, yanlış kullanımını zorlaştırır (imkansız?).
Kod parçacıkları Coliru'da bir derleme örneği oluşturur .
Türetilmiş sınıf - kodu yeniden kullanması gerekiyorsa ve - temel sınıfı değiştiremezsiniz ve - temelin üyelerini bir kilit altında kullanarak yöntemlerini koruyorsa.
o zaman özel kalıtımı kullanmalısınız, aksi takdirde bu türetilmiş sınıf aracılığıyla dışa aktarılan kilidi açılmış temel yöntemler tehlikesi yaşarsınız.
Özel Miras, ilişki "bir" olmadığında kullanılacak, Ancak Yeni sınıf "var olan sınıf açısından uygulanabilir" veya yeni sınıf "mevcut sınıf gibi" çalışabilir.
"Andrei Alexandrescu, Herb Sutter'ın C ++ kodlama standartlarından" örnek: - Kare ve Dikdörtgen gibi iki sınıfın her birinin yükseklik ve genişliklerini ayarlamak için sanal işlevlere sahip olduğunu düşünün. O zaman Square, Dikdörtgenden doğru bir şekilde miras alamaz, çünkü değiştirilebilir bir Dikdörtgen kullanan kod SetWidth'in yüksekliği değiştirmediğini varsayar (Rectangle açıkça daralır veya daralmaz), oysa Square :: SetWidth bu sözleşmeyi ve kendi karesel değişmezliğini koruyamaz Aynı zaman. Ancak Dikdörtgen, Square istemcileri, örneğin bir Square'in alanının genişliğinin karesi olduğunu varsayarsa veya Dikdörtgenler için geçerli olmayan başka bir özelliğe güvenirlerse, Square'den de doğru bir şekilde miras alamaz.
Bir kare "is-a" dikdörtgen (matematiksel olarak), ancak Kare bir Dikdörtgen değildir (davranışsal olarak). Sonuç olarak, açıklamayı yanlış anlaşılmaya daha az eğilimli hale getirmek için "is-a" yerine "çalışır-a-gibi" (veya tercih ederseniz, "a olarak kullanılabilir") demeyi tercih ederiz.
Bir sınıf bir değişmez içerir. Değişmez, kurucu tarafından belirlenir. Bununla birlikte, birçok durumda nesnenin temsil durumunun bir görünümüne sahip olmak yararlıdır (bunu ağ üzerinden iletebilir veya bir dosyaya kaydedebilirsiniz - isterseniz DTO). REST, bir AggregateType açısından en iyi şekilde yapılır. Bu, özellikle haklıysanız geçerlidir. Düşünmek:
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
}
Bu noktada, önbellek koleksiyonlarını konteynırlarda depolayabilir ve inşaata bakabilirsiniz. Gerçek bir işlem varsa kullanışlı. Önbelleğin QE'nin bir parçası olduğuna dikkat edin: QE'de tanımlanan işlemler, önbelleğin kısmen yeniden kullanılabilir olduğu anlamına gelebilir (örneğin, c toplamı etkilemez); yine de, önbellek olmadığında, bakmaya değer.
Özel kalıtım hemen hemen her zaman bir üye tarafından modellenebilir (gerekirse tabana referans depolanır). Bu şekilde modellemeye her zaman değmez; bazen miras, en etkili temsildir.
std::ostream
Bazı küçük değişikliklere ihtiyacınız varsa ( bu soruda olduğu gibi ) yapmanız gerekebilir
MyStreambuf
türetilmiştirstd::streambuf
Oradaki değişiklikleri ve uygulayanMyOStream
türetilen bir sınıf oluşturun std::ostream
, ayrıca bir örneğini başlatır ve yönetir MyStreambuf
ve işaretçiyi bu örneğe yapıcısına iletir.std::ostream
İlk fikir, MyStream
örneği MyOStream
sınıfa bir veri üyesi olarak eklemek olabilir :
class MyOStream : public std::ostream
{
public:
MyOStream()
: std::basic_ostream{ &m_buf }
, m_buf{}
{}
private:
MyStreambuf m_buf;
};
Ancak temel sınıflar herhangi bir veri üyesinden önce oluşturulur, bu nedenle henüz yapılandırılmamış bir std::streambuf
örneğe bir işaretçiyistd::ostream
bu nedenle tanımlanmamış davranış olan geçirirsiniz.
Çözüm, Ben'in yukarıda belirtilen soruya verdiği yanıtta önerilmiştir , basitçe önce akış tamponundan, sonra akıştan devralın ve ardından akışı şu şekilde başlatın this
:
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Bununla birlikte, ortaya çıkan sınıf std::streambuf
, genellikle istenmeyen bir örnek olarak da kullanılabilir . Özel mirasa geçmek bu sorunu çözer:
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Sırf C ++ 'nın bir özelliği olması, yararlı olduğu veya kullanılması gerektiği anlamına gelmez.
Hiç kullanmamalısın derim.
Yine de kullanıyorsanız, temelde kapsüllemeyi ihlal ediyorsunuz ve uyumu düşürüyorsunuz. Bir sınıfa veri koyuyorsunuz ve başka bir sınıfa verileri işleyen yöntemler ekliyorsunuz.
Diğer C ++ özellikleri gibi, bir sınıfı mühürlemek gibi yan etkiler elde etmek için kullanılabilir (dribeas'ın cevabında belirtildiği gibi), ancak bu onu iyi bir özellik yapmaz.