web-dev-qa-db-ja.com

保護された継承を使用して、実装されたインターフェイスをパブリックから隠します(ただし悪用します)か?

最近、クラスが特定の基本クラスから継承する(クライアントコードに対して)事実を隠蔽するために、保護された継承を使用するコードについて議論しましたが、実装でこの事実を悪用しています。

次のコードはこれを示しています。 GCCとclang ++の最新バージョンでコンパイルします(C++ 11機能を使用します):

#include <vector>
#include <iostream>

class IObserver
{
public:
    virtual void update() = 0;
};

class Model
{
    std::vector<IObserver*> m_observers;
    int m_number = 0;
public:
    void addObserver(IObserver& observer) {
        m_observers.Push_back(&observer);
    }
    void setNumber(int value) {
        m_number = value;
        notifyObservers();
    }
    int number() const {
        return m_number;
    }
protected:
    void notifyObservers() {
        for (auto pObserver : m_observers)
            pObserver->update();
    }
};

// We want to hide the fact class 'View' has 'IObserver' interface.
class View : protected IObserver
{
    Model* m_pModel;
public:
    View(Model& model) : m_pModel(&model) {
        model.addObserver(*this); // Exploit the fact we are an 'IObserver'.
    }
protected:
    void update() override {
        std::cout << m_pModel->number() << std::endl;
    }
};

int main(int argc, char *argv[])
{
    Model model;
    View view(model);

    //view.update(); // ERROR: 'update' is a protected member of 'View'.

    model.setNumber(1);
    model.setNumber(2);
}

'View'クラスは 'IObserver'インターフェイスを継承しますが、 'protected'修飾子を使用します。したがって、それに応じて、そのインターフェイスから継承されたパブリックメソッド「update」は保護されます。コンストラクターでは、クラス「View」がパラメーターとして渡された「Model」インスタンスにオブザーバーとして自分自身を追加します。実行可能ファイルを実行すると、「1」と「2」が2行に分かれて出力されるため、コードは期待どおりに実行されます。

現在、このソリューションは私たちのチームで激しく議論されましたが、最終的には、共通の合意では答えられないいくつかの質問がありました。

  1. このコードは実際にw.r.tで合法ですか? C++標準へ?ここでの問題は、コンストラクターで、「View」クラスがそれ自体への参照をメソッド「Model :: addOberver」に渡すことです。このメソッドは「パブリック」を持つ「IObserver」インスタンスへの参照を期待します。 update 'メソッド。ただし、「View」クラスは保護された継承を使用するため、このメソッドは保護されるようになりました。したがって、「Model :: notifyObservers」メソッドが「Model :: setNumber」メソッド内で呼び出されると、ビューの「update」メソッドは、実際には保護されているにもかかわらず、「外部から」呼び出されます。 ( C++ FAQ Lite 状態: "[保護された継承]により、保護された派生クラスの派生クラスが悪用保護された基本クラスとの関係 "したがって、上記のコードは対応するユースケースのように聞こえます。)
  2. これは合法的なコードだと思いますが、それも優れた設計ですか?よく話し合って共通の合意に至らなかったという事実は、そうではないというヒントかもしれません。一部の同僚は、クラスのインターフェイスはそのパブリックメソッドによってのみ定義されるという意見を持っています。また、「View」クラスが「IObserver」インスタンスを期待するメソッドにそれ自体への参照を渡したい場合は、パブリック継承を使用する必要があります。他の一部(私を含む)は、この厳密な定義に同意しませんでした。実装は、「IObserver」インターフェイスを備えている(そして提供できる)という事実を知っているので、なぜこの知識を悪用して、保護されたインターフェイスを必要なコードに公開しないのでしょうか。 C++では、「通常の」クライアントコード用のパブリックインターフェイスと、クラスを特殊化/拡張するクライアントコード用の保護されたインターフェイスの両方を定義できます。したがって、この観点から見ると、クラスのインターフェースは、そのパブリックandによって保護されたインターフェースによって定義されます。それは、どの種類のクライアントコードがクラスのインターフェースを「見る」かに依存します。
  3. 目標が「IObserver」インターフェースを公開から隠すことである場合、それだけの価値があるこの目的のためだけに pImplイディオム を使用していますか?私たちはに来ることができなかったので最初の2つの質問に関する一般的な結論は、実装クラスでpImplイディオムとパブリック継承を使用して「回避」することにしました。これで、「IObserver」クラスをまったく継承しない「View」クラスができました。代わりに、クラス「ViewImpl」が「IObserver」からパブリックに継承するようになりました。これは「優れた設計」であることに同意しましたが、実装がより複雑になりましたが、間接参照が1つ多くなり、2倍近くのコードを維持する必要があります。さらに、パブリッククラスと実装クラスの両方の継承階層を維持する必要があります。 (もちろん、これらはpImplイディオムを使用することによるよく知られた欠点です。)

これら3つの質問についてのあなたの意見をたくさん感謝します!!!事前に感謝します:-)

6
Jonny Dee
  1. コンパイルされているので、C++は合法(つまり有効)であると判断できます。あなたはGCCとclangでそれを使用しました、そして私はコンパイラ市場の大部分をカバーするようにMSVC++でまったく同じことをしました。

  2. これが、protected/private(ところで、私はprivateを使用します)修飾子が導入された理由です。それはあなたのクラスが内部で使用している他のクラスに必要なコールバックインターフェースを実装することを可能にします、しかしa)他の世界はあなたがそれらの他のクラスを使用していることを知る必要はなく、そしてb)他の世界はすべきではありませんこれらの関数は非表示にして内部でのみ使用することを目的としているため、これらの関数を呼び出すこともできます。

    C++からC#に移行したとき、実際にはプライベートインターフェイスの継承ができないことに不思議に思いました。何かが実装の詳細であり、一般に公開/使用されることを意図していない場合は、非表示にしておく必要があります。あなたが説明した方法は、それを行うための優れた簡単な方法だと思います。

  3. PImplを使用すると、オーバーヘッドが増えるだけです。オブジェクトを割り当てると(明らかに動的割り当てを想定しています)、ヒープに2倍の回数が行き着きます。また、ViewとViewImplがメモリ内の別のページにあり、キャッシュミスが増えるため、コードの局所性も削減します。また、すべてのパブリックインターフェイス呼び出しをViewからViewImplに転送しているため、作成する必要のあるボイラープレートコードの量が増えました。

これはすべて私の意見ですが、結局のところ、プライベートインターフェイスの継承を持たない理由は考えられません。私はこのようなコードを6年以上作成してきましたが、そのために読みやすさや保守性に問題はありませんでした。

6
DXM