いつC ++プライベート継承を使用する必要がありますか?


116

保護された継承とは異なり、C ++プライベート継承は主流のC ++開発に取り入れられました。しかし、私はまだそれをうまく使う方法を見つけていません。

いつ使用しますか?

c++  oop 

回答:


60

回答承認後の注意:これは完全な回答ではありません。質問に興味がある場合は、ここ(概念的)およびここ(理論的および実践的)のような他の回答を読んでください。これは、プライベート継承で実現できる、ちょっとしたトリックです。それはですが空想それは質問への答えではありません。

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 // ...

それは素晴らしいテクニックです。ブログに書きます。

1
質問:仮想継承を使用しなかった場合、FailsToDeriveはコンパイルされます。正しい?

4
+1。@Sasha:正しい、ほとんどの派生クラスは常にすべての仮想継承クラスのコンストラクターを直接呼び出すため、仮想継承が必要です。これは、単純な継承の場合とは異なります。
j_random_hacker 2009年

5
これは、シールしたいクラスごとにカスタムClassSealerを作成することなく、汎用的にすることができます。確認してください:class ClassSealer {protected:ClassSealer(){}}; それで全部です。

+1イランビランジャ、とてもかっこいい!ところで、CRTPの使用に関する以前のコメント(現在は削除済み)を見ました。実際に機能するはずです。テンプレートの友達の構文を正しく理解するのは簡単ではありません。しかし、いずれにせよ、非テンプレートソリューションの方がはるかに優れています:)
j_random_hacker '18 / 03/09

138

いつも使っています。私の頭の上のいくつかの例:

  • 基本クラスのインターフェースのすべてではなく一部を公開したい場合。Liskovの代用性が壊れているので、パブリック継承は嘘です。一方、コンポジションは転送関数の束を書くことを意味します。
  • 仮想デストラクタなしの具象クラスから派生したい場合。パブリック継承は、クライアントへのポインターからベースへの削除を招待し、未定義の動作を呼び出します。

典型的な例は、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...  
};
  • アダプターパターンを実装する場合、Adaptedクラスからプライベートに継承すると、囲まれたインスタンスに転送する必要がなくなります。
  • プライベートインターフェイスを実装します。これは、オブザーバーパターンで頻繁に発生します。通常、私のオブザーバークラス、MyClassは、サブジェクトをサブスクライブします。次に、MyClassのみがMyClass-> Observer変換を行う必要があります。システムの残りの部分はそれを知る必要がないため、プライベート継承が示されます。

4
@クルスナ:実はそうは思いません。ここに理由が1つだけあります。遅延を除いて、遅延は回避するのが難しくなります。
Matthieu M.11年

11
それほど怠惰ではありません(良い意味で意味する場合を除きます)。これにより、余分な作業なしに公開された関数の新しいオーバーロードを作成できます。C ++ 1xで3つの新しいオーバーロードをに追加した場合、それらは無料push_backMyVector取得されます。
David Stone、

@DavidStone、テンプレートメソッドでそれを行うことはできませんか?
Julien__

5
@Julien__:はい、書く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ステートメントが提供する機能性と柔軟性のほとんどが必要な場合は、各関数にそのモンスターがあります(そしてconstvolatileオーバーロードを忘れないでください!)。
David Stone

2
usingステートメントバージョンにはない追加のmoveコンストラクターを1つ呼び出しているため、ほとんどの機能を使用できます。一般に、これは最適化されると予想されますが、関数は理論的には移動不可能な型を値で返す可能性があります。転送機能テンプレートには、追加のテンプレートのインスタンス化とconstexprの深さもあります。これにより、プログラムが実装制限に達する可能性があります。
David Stone

31

プライベート継承の標準的な使用法は、「実装された」関係です(Scott Meyersの「Effective C ++」に感謝します)。言い換えると、継承クラスの外部インターフェイスは、継承クラスとの(目に見える)関係はありませんが、内部的に使用して機能を実装します。


6
この場合に使用される理由の1つに言及する価値があります。これにより、空の基本クラスの最適化を実行できます。これは、クラスが基本クラスの代わりにメンバーであった場合は発生しません。
2009年

2
その主な用途は、ポリシーが制御する文字列クラスや圧縮ペアなど、本当に重要な場所でのスペース消費を減らすことです。実際、boost :: compressed_pa​​irは保護された継承を使用していました。
Johannes Schaub-litb 2009年

jalf:ねえ、私はそれを知りませんでした。クラスの保護されたメンバーにアクセスする必要がある場合、非公開の継承は主にハッキングとして使用されたと思いました。しかし、コンポジションを使用するとき、空のオブジェクトがスペースをとるのはなぜでしょうか。おそらく普遍的なアドレス可能性のために...

3
クラスをコピー不可にすることも便利です-コピーできない空のクラスから単純にプライベートに継承します。これで、プライベートコピーコンストラクターと代入演算子を宣言するだけでなく定義するという煩雑な作業を行う必要がなくなりました。マイヤーズもこれについて話します。
Michael Burr

この質問が実際には保護された継承ではなくプライベートの継承についてであることに気づきませんでした。ええ、私はそれにかなりのアプリケーションがあると思います。保護された継承の多くの例を考えることはできませんが、:/はほとんど役に立ちません。
Johannes Schaub-litb 2009年

23

プライベート継承の便利な使い方の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のプライベートメソッドを呼び出すことができますが、他の外部クラスはできません。これは、インターフェイスとして定義されている特定のコールバックを処理するための優れたパターンです。


1
実際、プライベート継承はプライベートIS-Aです。
curiousguy

18

C ++ FAQ Liteの重要なセクションは次のとおりです。

プライベート継承の正当かつ長期的な使用は、クラスWildのコードを使用するクラスFredを構築する場合で、クラスWilmaのコードは新しいクラスFredのメンバー関数を呼び出す必要があります。この場合、フレッドはウィルマで非バーチャルを呼び出し、ウィルマはそれ自体で(通常は純粋なバーチャル)を呼び出します。これらはフレッドによってオーバーライドされます。これは作曲で行うのがはるかに難しいでしょう。

疑問がある場合は、プライベート継承よりもコンポジションを優先する必要があります。


4

他のコードがインターフェイス(継承クラスのみ)に触れたくない場所で継承しているインターフェイス(つまり、抽象クラス)には便利です。

[例で編集]

上記にリンクされている例を見てみましょう。と言って

[...]クラスWilmaは、新しいクラスFredからメンバー関数を呼び出す必要があります。

ウィルマはフレッドが特定のメンバー関数を呼び出すことができることを要求していると言うことです、またはむしろウィルマはインターフェースであると言っています。したがって、例で述べたように

プライベート継承は悪ではありません。誰かがあなたのコードを壊す何かを変更する可能性が高くなるので、それは維持するのにただより高価です。

インターフェース要件を満たす必要がある、またはコードを壊す必要があるプログラマーの望ましい効果に関するコメント。また、fredCallsWilma()は保護されているため、フレンドと派生クラスだけがタッチできます。つまり、継承クラス(抽象クラ​​ス)だけがタッチできる継承クラス(抽象クラ​​ス)です。

[別の例で編集]

このページでは、プライベートインターフェイスについて(別の観点から)簡単に説明します。


本当に便利な音はありません...あなたは一例を投稿することができます

私はあなたがどこに行くのかを見たと思います...典型的なユースケースは、ウィルマがフレッドで仮想関数を呼び出す必要があるある種のユーティリティクラスであるかもしれません、しかし他のクラスはフレッドが実装されていることを知る必要はありません-ウィルマの。正しい?
j_random_hacker 2009年

はい。私の理解では、「インターフェース」という用語はJavaではより一般的に使用されていることを指摘しておきます。最初にそれを聞いたとき、もっと良い名前が付けられているのではないかと思いました。というのも、この例では、私たちが通常その言葉について考えるような方法で、誰もインターフェースしないインターフェースがあるからです。
バイアス

@Noos:はい、「ウィルマはインターフェースです」という文は少し曖昧だと思います。ほとんどの人はこれをウィルマがウィルマとの契約ではなくフレッドが世界に提供するつもりのインターフェースであることを意味すると解釈します。
j_random_hacker

@j_だからインターフェースは悪い名前だと思う。インターフェースという用語は、世間が思うように意味する必要はありませんが、機能を保証するものです。実際、私はプログラムデザインクラスのインターフェイスという用語に異議を唱えていました。しかし、与えられたものを使用します...
バイアス

2

コレクションの実装が公開クラスの状態へのアクセスを必要とする、別のインターフェイスでより小さなインターフェイス(たとえば、コレクション)を公開する場合は、プライベート継承を使用すると便利な場合があります。 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この例では、の前方宣言は必要ありませんか?私はこれを面白いと思いますが、それは私の顔にハックを叫びます。
Thomas Eding、2011年

2

用途が限られていますが、私はプライベート継承のための素晴らしいアプリケーションを見つけました。

解決する問題

次の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っぽいアプローチ

もちろん、次のような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っぽいです

C ++のアプローチ

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);
}

このようにして、上記の問題をすべて修正しました。しかし、クライアントはまだセッターを忘れWidgetImplWidgetメンバーに直接割り当てることができます。

民間継承がステージに入る

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でのコンパイル例を形成します。


1

派生クラスの場合-コードを再利用する必要があり、ベースクラスを変更できず、ロックされたベースのメンバーを使用してメソッドを保護している場合。

次に、プライベート継承を使用する必要があります。そうしないと、この派生クラスを介してエクスポートされたロック解除された基本メソッドの危険があります。


1

時にはそれがに代わる可能性が集約あなたが集計をしたい場合、たとえば、しかしaggregableエンティティの変更行動を(仮想関数をオーバーライド)。

しかし、あなたの言う通り、現実世界からの例はあまりありません。


0

リレーションが「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」)を使用して、説明を誤解しにくくします。


0

クラスは不変条件を保持します。不変条件はコンストラクターによって確立されます。ただし、多くの状況では、オブジェクトの表現状態を表示すると便利です(ネットワーク経由で送信したり、ファイルに保存したりできます-必要に応じて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は合計に影響しません)。まだ、キャッシュがない場合は、それを調べる価値があります。

プライベート継承はほとんどの場合、メンバーによってモデル化できます(必要に応じて、ベースへの参照を格納します)。そのようにモデル化することは常に価値があるとは限りません。時には継承が最も効率的な表現です。


0

あなたが必要な場合std::ostream(のようにいくつかの小さな変更でこの質問を)あなたがする必要があるかもしれません

  1. そこMyStreambufから派生しstd::streambuf、変更を実装するクラスを作成する
  2. それから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 }
    {}
};

-1

C ++に機能があるからといって、それが有用である、または使用する必要があるという意味ではありません。

まったく使わない方がいいと思います。

とにかくそれを使用している場合、まあ、あなたは基本的にカプセル化に違反し、凝集度を下げています。あるクラスにデータを入れ、別のクラスのデータを操作するメソッドを追加しています。

他のC ++機能と同様に、それはクラスのシーリングなどの副作用を達成するために使用できます(ドリベスの回答で述べたように)が、これは良い機能にはなりません。


あなたは皮肉ですか?私が持っているのは-1だけです!とにかく、これは-100票を得ても削除しません
2009年

9
基本的にカプセル化に違反しています」例を挙げていただけますか?
curiousguy

1
そこに複数の行動クラスとクライアントのこと、彼らは、彼らが望むものを満足させる必要があるか1つを選択することができ、柔軟性の増加のような別の音で1クラスと行動のデータ、以来
makar
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.