保護された継承とは異なり、C ++プライベート継承は主流のC ++開発に取り入れられました。しかし、私はまだそれをうまく使う方法を見つけていません。
いつ使用しますか?
保護された継承とは異なり、C ++プライベート継承は主流のC ++開発に取り入れられました。しかし、私はまだそれをうまく使う方法を見つけていません。
いつ使用しますか?
回答:
回答承認後の注意:これは完全な回答ではありません。質問に興味がある場合は、ここ(概念的)およびここ(理論的および実践的)のような他の回答を読んでください。これは、プライベート継承で実現できる、ちょっとしたトリックです。それはですが空想それは質問への答えではありません。
C ++ FAQ(他のコメントでリンクされている)に示されているプライベート継承のみの基本的な使用法に加えて、プライベート継承と仮想継承の組み合わせを使用して、クラスをシールする(.NET用語で)か、クラスをfinalにする(Java用語で) 。これは一般的な使用法ではありませんが、とにかく面白いと思いました:
class ClassSealer {
private:
friend class Sealed;
ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{
// ...
};
class FailsToDerive : public Sealed
{
// Cannot be instantiated
};
Sealedはインスタンス化できます。これはClassSealerから派生し、フレンドであるため、プライベートコンストラクターを直接呼び出すことができます。
FailsToDeriveは、それが呼び出す必要がありますとしてコンパイルされませんClassSealerの直接コンストラクタ(仮想継承の要件を)が、それは民間ではないようすることができ封印されたクラスと、この場合のFailsToDeriveはの友人ではありませんClassSealer。
編集
現時点では、CRTPを使用してこれを一般化することはできないとコメントで言及されていました。C ++ 11標準では、テンプレートの引数に対応する別の構文を提供することにより、この制限を取り除いています。
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
もちろん、C ++ 11はfinal
まさにこの目的のためのコンテキストキーワードを提供しているので、これはすべて意味がないものです。
class Sealed final // ...
いつも使っています。私の頭の上のいくつかの例:
典型的な例は、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
取得されます。
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)...); }
ことができますBase::f;
。プライベート継承とusing
ステートメントが提供する機能性と柔軟性のほとんどが必要な場合は、各関数にそのモンスターがあります(そしてconst
、volatile
オーバーロードを忘れないでください!)。
プライベート継承の標準的な使用法は、「実装された」関係です(Scott Meyersの「Effective C ++」に感謝します)。言い換えると、継承クラスの外部インターフェイスは、継承クラスとの(目に見える)関係はありませんが、内部的に使用して機能を実装します。
プライベート継承の便利な使い方の1つは、インターフェイスを実装するクラスがあり、それを他のオブジェクトに登録する場合です。クラス自体を登録する必要があり、登録された特定のオブジェクトのみがそれらの関数を使用できるように、そのインターフェースをプライベートにします。
例えば:
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() { ... }
};
したがって、FooUserクラスはFooInterfaceインターフェイスを介してFooImplementerのプライベートメソッドを呼び出すことができますが、他の外部クラスはできません。これは、インターフェイスとして定義されている特定のコールバックを処理するための優れたパターンです。
C ++ FAQ Liteの重要なセクションは次のとおりです。
プライベート継承の正当かつ長期的な使用は、クラスWildのコードを使用するクラスFredを構築する場合で、クラスWilmaのコードは新しいクラスFredのメンバー関数を呼び出す必要があります。この場合、フレッドはウィルマで非バーチャルを呼び出し、ウィルマはそれ自体で(通常は純粋なバーチャル)を呼び出します。これらはフレッドによってオーバーライドされます。これは作曲で行うのがはるかに難しいでしょう。
疑問がある場合は、プライベート継承よりもコンポジションを優先する必要があります。
他のコードがインターフェイス(継承クラスのみ)に触れたくない場所で継承しているインターフェイス(つまり、抽象クラス)には便利です。
[例で編集]
上記にリンクされている例を見てみましょう。と言って
[...]クラスWilmaは、新しいクラスFredからメンバー関数を呼び出す必要があります。
ウィルマはフレッドが特定のメンバー関数を呼び出すことができることを要求していると言うことです、またはむしろウィルマはインターフェースであると言っています。したがって、例で述べたように
プライベート継承は悪ではありません。誰かがあなたのコードを壊す何かを変更する可能性が高くなるので、それは維持するのにただより高価です。
インターフェース要件を満たす必要がある、またはコードを壊す必要があるプログラマーの望ましい効果に関するコメント。また、fredCallsWilma()は保護されているため、フレンドと派生クラスだけがタッチできます。つまり、継承クラス(抽象クラス)だけがタッチできる継承クラス(抽象クラス)です。
[別の例で編集]
このページでは、プライベートインターフェイスについて(別の観点から)簡単に説明します。
コレクションの実装が公開クラスの状態へのアクセスを必要とする、別のインターフェイスでより小さなインターフェイス(たとえば、コレクション)を公開する場合は、プライベート継承を使用すると便利な場合があります。 Java。
class BigClass;
struct SomeCollection
{
iterator begin();
iterator end();
};
class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};
そうすれば、SomeCollectionがBigClassにアクセスする必要がある場合、それが可能になりますstatic_cast<BigClass *>(this)
。余分なデータメンバーが領域を占有する必要はありません。
BigClass
この例では、の前方宣言は必要ありませんか?私はこれを面白いと思いますが、それは私の顔にハックを叫びます。
用途が限られていますが、私はプライベート継承のための素晴らしいアプリケーションを見つけました。
次のC APIが与えられているとします。
#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
次に、C ++を使用してこのAPIを実装します。
もちろん、次のようなC風の実装スタイルを選択することもできます。
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;
}
ただし、いくつかの欠点があります。
struct
間違って設定するのは簡単ですstruct
C ++の使用が許可されているので、そのフルパワーを使用してみませんか?
上記の問題は、基本的にすべて手動のリソース管理に関連しています。頭に浮かぶソリューションは、各変数のWidget
派生クラスからリソース管理インスタンスを継承して追加すること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;
};
これにより、実装が次のように簡素化されます。
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);
}
このようにして、上記の問題をすべて修正しました。しかし、クライアントはまだセッターを忘れWidgetImpl
てWidget
メンバーに直接割り当てることができます。
Widget
メンバーをカプセル化するには、プライベート継承を使用します。残念ながら、両方のクラス間でキャストするには、2つの追加の関数が必要になります。
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;
};
これにより、次の調整が必要になります。
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);
}
このソリューションはすべての問題を解決します。手動のメモリ管理はなくWidget
、うまくカプセル化されているため、WidgetImpl
パブリックデータメンバーはもうありません。これにより、実装が正しく使いやすくなり、間違った使い方が難しくなります(不可能?)。
コードスニペットは、Coliruでのコンパイル例を形成します。
派生クラスの場合-コードを再利用する必要があり、ベースクラスを変更できず、ロックされたベースのメンバーを使用してメソッドを保護している場合。
次に、プライベート継承を使用する必要があります。そうしないと、この派生クラスを介してエクスポートされたロック解除された基本メソッドの危険があります。
リレーションが「is a」でない場合に使用されるプライベート継承ですが、新しいクラスは「既存のクラスの観点から実装」することも、新しいクラスを既存のクラスと「機能する」ようにすることもできます。
「Andrei Alexandrescu、Herb SutterによるC ++コーディング標準」の例:-SquareとRectangleの2つのクラスには、それぞれ高さと幅を設定するための仮想関数があると考えてください。変更可能な四角形を使用するコードはSetWidthが高さを変更しないと想定するため(Rectangleがその縮小を明示的に文書化するかどうかにかかわらず)、Square :: SetWidthはその縮小とその独自の直角度不変を保持できないため、SquareはRectangleを正しく継承できません。同時に。ただし、Squareのクライアントが、たとえばSquareの領域がその幅の2乗であると想定している場合や、Rectangleを保持しない他のプロパティに依存している場合、RectangleはSquareを正しく継承できません。
正方形は(数学的に)「長方形」ですが、正方形は(動作的に)長方形ではありません。したがって、「is-a」の代わりに「works-like-a」(または「usable-as-a」)を使用して、説明を誤解しにくくします。
クラスは不変条件を保持します。不変条件はコンストラクターによって確立されます。ただし、多くの状況では、オブジェクトの表現状態を表示すると便利です(ネットワーク経由で送信したり、ファイルに保存したりできます-必要に応じてDTO)。RESTは、AggregateTypeの観点から行うのが最適です。これは、constが正しい場合に特に当てはまります。考慮してください:
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
}
この時点で、キャッシュのコレクションをコンテナーに格納し、構築時にそれを調べるだけです。実際の処理がある場合に便利です。キャッシュはQEの一部であることに注意してください。QEで定義された操作は、キャッシュが部分的に再利用可能であることを意味します(たとえば、cは合計に影響しません)。まだ、キャッシュがない場合は、それを調べる価値があります。
プライベート継承はほとんどの場合、メンバーによってモデル化できます(必要に応じて、ベースへの参照を格納します)。そのようにモデル化することは常に価値があるとは限りません。時には継承が最も効率的な表現です。
あなたが必要な場合std::ostream
(のようにいくつかの小さな変更でこの質問を)あなたがする必要があるかもしれません
MyStreambuf
から派生しstd::streambuf
、変更を実装するクラスを作成するMyOStream
派生したクラスを作成し、std::ostream
そのインスタンスを初期化および管理し、MyStreambuf
そのインスタンスへのポインタをのコンストラクタに渡します。std::ostream
最初のアイデアは、MyStream
インスタンスをデータメンバーとしてMyOStream
クラスに追加することです。
class MyOStream : public std::ostream
{
public:
MyOStream()
: std::basic_ostream{ &m_buf }
, m_buf{}
{}
private:
MyStreambuf m_buf;
};
ただし、基本クラスはデータメンバーの前に構築されるため、未定義の動作である、まだ構築されていないstd::streambuf
インスタンスへのポインターを渡しますstd::ostream
。
解決策は、前述の質問に対するベンの回答で提案されています。最初にストリームバッファから継承し、次にストリームから継承してから、次のようにストリームを初期化しますthis
。
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
ただし、結果のクラスstd::streambuf
は、通常は望ましくないインスタンスとしても使用できます。プライベート継承に切り替えると、この問題が解決します。
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
C ++に機能があるからといって、それが有用である、または使用する必要があるという意味ではありません。
まったく使わない方がいいと思います。
とにかくそれを使用している場合、まあ、あなたは基本的にカプセル化に違反し、凝集度を下げています。あるクラスにデータを入れ、別のクラスのデータを操作するメソッドを追加しています。
他のC ++機能と同様に、それはクラスのシーリングなどの副作用を達成するために使用できます(ドリベスの回答で述べたように)が、これは良い機能にはなりません。