インタフェースの継承と継承を区別する-契約34

7241 ワード

表面的にストレートなpublicは概念を継承し,厳密な検査を経た後,関数インタフェース(function interfaces)継承と関数実装(function implementations)継承の2つの部分から構成されていることがわかりました.この2つの継承の違いは、本書で説明した関数宣言と関数定義の違いに似ています.class設計者として、derived classesがメンバー関数のみを継承するインタフェースを望んでいる場合があります.(つまり宣言);derived classesが関数のインタフェースと実装を同時に継承したい場合もありますが、上書きしたい場合もあります.(override)継承されたインプリメンテーション.derived classesが関数のインタフェースとインプリメンテーションを同時に継承し、何も上書きすることを許さない場合があります.上記の選択の違いをよりよく感じるために、図形描画プログラムのジオメトリを示すclass継承システムを考えてみましょう.
class Shape {
    public:
    virtual void draw() const = 0;
    virtual void error(const std::string& msg);
    int objectID() const;
    ...
};
class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };

Shapeは抽象classです.そのpure virtual関数drawは抽象classになります.したがって、お客様はShape classのエンティティを作成できず、derived classesのエンティティのみを作成できます.それでも、Shapeはpublic形式で継承されたderived classesに強く影響しました.
  • メンバー関数のインタフェースは常に継承されます.条項32で述べたように、public継承はis-a(一種)を意味するため、base classを真とすることは必ずderived classesを真とする.したがって、ある関数があるclassに実行可能であれば、必ずそのderived classにも実行可能である.
  • Shape classは3つの関数を宣言した.1つ目はdrawで、ある隠喩のスクリーンに現在のオブジェクトを描きます.2つ目はerrorで、「エラーを報告する必要がある」メンバー関数を呼び出す準備をしています.3つ目はobjectIDで、現在のオブジェクトのユニークな整数識別コードを返します.各関数の宣言方法は異なります.drawはpure virtual関数です.errorは単純(非純粋)impure virtual関数であり、objectIDはnon-virtual関数である.これらの異なる声明はどのような暗示をもたらすのだろうか.
    まずpure virtual関数drawを考慮します.
    class Shape {
        public:
        virtual void draw() const = 0;
        ...
    };

    pure virtual関数には、2つの最も顕著な特性があります.これらは、抽象classでは通常定義されていない「継承された」任意の具象classによって再宣言される必要があります.この2つの性質を一緒に置くと、次のことがわかります.
  • pure virtual関数を宣言する目的は、derived classesに関数インタフェースのみを継承させることです.

  • これはShape::draw関数はもっと合理的ですが、すべてのShapオブジェクトが描くことができるはずなので、これは合理的な要求です.しかし、Shape classはこの関数に合理的なデフォルト実装を提供することはできません.楕円形描画法は矩形描画法とは異なります.Shape::drawの宣言式は、derived classesの設計者に対して、「draw関数を提供しなければなりませんが、私はあなたがどのように実現するかに干渉しません」と言いました.
    意外なことに、pure virtual関数を定義することができます.つまり、Shape::drawに実装コードを供給することができます.C++は文句を言いませんが、呼び出す唯一の方法は「呼び出すときにclass名を明確に指摘する」ことです.
    Shape *ps = new Shape;          //   !Shape    
    Shape *ps1 = new Rectangle;     //    
    ps1->draw();                    //   Rectangle::draw
    Shape *ps2 = new Ellipse;       //    
    ps2->draw();                    //   Ellipse::draw
    ps1->Shape::draw();             //   Shape::draw
    ps2->Shape::draw();             //   Shape::draw

    シンプルなimpure virtual関数の背後にある物語とpure virtual関数は少し違います.通常、derived classesは関数インタフェースを継承しますが、impure virtual関数は実装コードを提供します.derived classesは上書きする可能性があります.少し考えてみると、次のことがわかります.
  • シンプル(非純粋)impure virtual関数を宣言する目的は、derived classesに関数のインタフェースとデフォルトの実装を継承させることです.
  • Shape::errorという例を考えます.
    class Shape {
    public:
    	virtual void error(const std::string& msg);
    	...
    };

    Shape::errorの宣言式はderived classesの設計者に、「error関数をサポートする必要がありますが、自分で書きたくない場合は、Shape classが提供するデフォルトバージョンを使用することができます」と伝えました.
    しかし、impure virtual関数が関数宣言と関数デフォルトの動作を同時に指定できるようにすると、危険にさらされる可能性があります.原因を検討するには、XYZ航空が設計した航空機継承システムを考えてみましょう.同社はA型とB型の2種類の飛行機しかなく、どちらも同じ方法で飛行している.したがってXYZはこのような継承体系を設計した.
    class Airport { ... };    //       
    class Airplane {
    	public:
    	virtual void fly(const Airport& destination);
    	...
    };
    void Airplane::fly(const Airport& destination)
    {
    	//     ,          
    }
    class ModelA:public Airplane { ... };
    class ModelB:public Airplane { ... };

    すべての飛行機が必ず飛べることを示し、「異なる飛行機は原則的に異なるfly実現が必要だ」と明らかにするため、Airplane::flyはvirtualと宣言された.しかし、ModelAとModelBで同じコードを書くことを避けるために、デフォルトの飛行動作はAirplane::flyによって提供され、ModelAとModelBによって継承されます.
    これは典型的なオブジェクト向けの設計です.2つのclassesは同じ性質(すなわちflyを実現する方法)を共有するため、共通の性質はbase classに移され、その後、2つのclassesによって継承される.
    現在、XYZの黒字が大幅に増加したと仮定し、新型C型機の購入を決定した.C型とA型、B型はいくつか違います.より明確に言えば、その飛行方法は違います.
    MXYZ社のプログラマーは継承システムでC型飛行機にclassを追加したが、彼らは急いで新しい飛行機をオンラインサービスさせるため、fly関数を再定義することを忘れた.
    class ModelC:public Airplane {
    	...        //    fly  
    };

    コードには次のようなアクションがあります.
    Airport PDX(...);               // PDX        
    Airplane* pa = new ModelC;
    ... 
    pa->fly(PDX);                   //   Airplane::fly

    これは大きな災難になるだろう.このプログラムはModelAまたはModelBの飛行方式でModelCを飛ばそうとした.問題はAirplane::flyにデフォルトの動作があるのではなく、ModelCが「私が望む」と言ったことが分からないままデフォルトの動作を継承していることです.幸いなことに、私たちは簡単に「derived classesにデフォルトの実装を提供することができますが、それらが要求を理解していない限り、話をしません」ことができます.このテクニックは、virtual関数インタフェースとデフォルトインプリメンテーションとの接続を切断することです.次の方法があります.
    class Airplane {
    public:
    	virtual void fly(const Airport& destination) = 0;
    	...
    protected:
    	void defaultFly(const Airport& destination);
    };
    void Airplane::defaultFly(const Airport& destination)
    {
    	//     ,          
    }

    Airplane::flyはpure virtual関数に変更され、飛行インタフェースのみが提供されています.そのデフォルトの動作はAirplane classにも表示されますが、今回は独立関数defaultFlyの姿勢で表示されます.デフォルトインプリメンテーション(ModelAやModelBなど)を使用するには、fly関数でdefaultFlyをinline呼び出します(ただし、条項30で説明するinlineとvirtual関数の相互関係に注意してください):
    class ModelA:public Airplane {
    	public:
    	virtual void fly(const Airport& destination)
    	{ defaultFly(destination); }
    	...
    };
    class ModelB:public Airplane {
    	public:
    	virtual void fly(const Airport& destination)
    	{ defaultFly(destination); }
    	...
    };

    現在、ModelC classは、Airplaneのpure virtual関数がModelCに独自のflyバージョンを提供しなければならないため、誤ったfly実装コードを意外に継承することはできません.
    class ModelC:public Airplane {
    	public:
    	virtual void fly(const Airport& destination);
    	...
    };
    void ModelC::fly(const Airport& destination)
    {
    	//  C          
    }

    これはほとんど前の設計とそっくりですが、pure virtual関数Airplane::flyは独立関数Airplane::defaultFlyを置き換えています.本質的には現在のflyは2つの基本要素に分割されています.その生命部分はインタフェース(derived classesが使用する必要があります)を表し、定義部分はデフォルトの動作(derived classesが使用可能ですが、それらが明確に申請した場合にのみ)を示します.flyとdefaultFlyを統合すると、「2つの関数に異なる保護レベルを持たせる」ことがなくなります.チャンス習慣的にprotectedに設定された関数(defaultFly)は、flyの中にあるためpublicになった.
    最後にShapeのnon-virtual関数objectIDを見てみましょう.
    class Shape {
    public:
    	int objectID() const;
    	...
    };

    メンバー関数がnon-virtual関数である場合、derived classesで異なる動作をするつもりはありません.実際にはnon-virtualメンバー関数の不変性がその特異性を凌駕している.derived classがどんなに特異化しても、その行為は変えられないことを示しているからだ.それ自体については、
  • non-virtual関数を宣言する目的は、derived classesに関数のインタフェースと強制的な実装を継承させることである.

  • pure virtual関数、impure virtual関数、non-virtual関数の違いにより、derived classesが継承したいものを正確に指定できます.インタフェースのみを継承するか、インタフェースとデフォルトの実装を継承するか、インタフェースと強制実装を継承します.これらの異なるタイプの宣言は、まったく意味の異なることを意味するため、メンバー関数を宣言する場合は、慎重に選択する必要があります.もしあなたが確かに履行すれば、経験不足のclass設計者が最もよく犯す2つの間違いを避けることができるはずです.
    最初のエラーは、すべての関数をnon-virtualと宣言することです.これによりderived classesは余裕のない空間で特化作業を行うことができる.non-virtual構造関数は特に問題をもたらします(条項7を参照).virtual関数のコストに関係する場合は、80-20の法則を紹介させていただきます(条項30も参照).この法則は、典型的なプログラムの実行時間の80%が20%のコードに費やされていることを意味します.この法則は、平均的にあなたの関数呼び出しの80%がプログラムのほぼ効率に衝撃を与えることなくvvirtualであることを意味するため、virtual関数のコストを負担する能力があるかどうかを心配する前に、そこに心を置いてください.20%のコードを軽く挙げることが本当の鍵です.
    2つ目の一般的なエラーは、すべてのメンバー関数をvirtualとして宣言することです.条項31のinterface classesのように、このようにするのが正しい場合があります.しかし、これはclass設計者が確固たる立場に欠けている前兆かもしれない.いくつかの関数はderived classで再定義されるべきではありません.果たして、それらの関数をnon-virtualと宣言する必要があります.
    覚えておいて
  • インタフェースの継承と実装は異なります.public継承の下で、derived classesは常にbase classのインタフェースを継承します.
  • pure virtual関数は、インタフェース継承のみを具体的に指定します.
  • 単純(非純粋)impure virtual関数は、インタフェース継承、すなわちデフォルト実装継承を具体的に指定する.
  • non-virtual関数は、インタフェースの継承および強制的な継承を具体的に指定します.