Effective C++第六章--継承とオブジェクト向け設計ノート

10267 ワード

  • 条項32あなたのpublic継承モールドにおけるis-a関係
  • を確定します.
  • 条項33継承された名称を隠すことを避ける
  • 条項34インタフェース継承と実装継承を区別する
  • 条項35 virtual関数以外の選択を考慮する
  • Non-Virtual Interface手法によりTemplate Methodモード
  • を実現する.
  • Funciton PointersによるStrategyポリシーモード
  • の実装
  • tr 1 functionによりStrategyモード
  • が完了する.
  • クラシックStrategyモード
  • 条項36は、継承されたnon-virtual関数
  • を再定義しない.
  • 条項37は、継承されたデフォルトパラメータ値
  • を再定義しない.
  • 条項38は、複合成形型におけるhas-aまたはある物に基づいて
  • を実現する.
  • 条項39 private継承
  • を賢明かつ慎重に使用
  • 条項40賢明かつ慎重な多重継承
  • の使用


    条項32:あなたのpublic継承モールドにおけるis-a関係を確定する
  • 「public継承」はis-aを意味する.各ベースクラスに適用されるすべてのことは、各派生クラスオブジェクトもベースクラスオブジェクトであるため、派生クラスにも適用されます. 

  • 条項33:継承された名称を隠すことを避ける
    派生クラスはベースクラスから継承され、名前付き役割ドメインネストが生成されます.派生クラスの内部には役割ドメインが作成されます.派生クラスが名前を使用する場合は、まず派生クラスの名前を付けて役割ドメインを検索します.派生クラスの内部に名前がない場合、ベースクラスの役割ドメインクラスで名前が検索されます.ベースクラスがない場合は、namespace、さらにグローバル役割ドメインに行きます.
    派生クラスがベースクラスから継承されると、関数が定義され、関数名がベースクラスの関数名と同じになると、ベースクラスの同名の関数はすべて上書きされます.パラメータが一致するかどうかにかかわらず.これは命名規則だからです.
    派生クラスがベースクラスを継承する場合、ベースクラスには同名で複数回再ロードされる関数があり、派生クラスはその一部を再定義または上書きし、usingを使用します.USingを使用すると、ベースクラスのすべての同名関数が上書きされないように、ベースクラスの一部と派生クラスのリロード関数を同時に存在させることができます.
    次のようになります.
    class base {
    public:
        void fun(int i) { std::cout<<"base"<<std::endl; }
    };
    
    class derived : public base{
    public:
        using base::fun;
        void fun() { std::cout<<"derived"<<std::endl; }
    };

    このような使い方では、すべてのオーバーライドは発生しません.派生クラスは、異なるパラメータに基づいて呼び出しベースクラスまたは独自の関数を選択できます.
    派生クラスはベースクラスのすべての関数を継承したくない場合があります.この場合、公有継承はis-aであるため、公有継承は使用できません.ベースクラス内のすべての同名関数が派生クラスに表示されるため、usingを使用することはできません.private継承プラスパス関数を使用して実装する必要があります.
    class derived : private base {  //    ,     
    public:
        virtual void fun() { base::fun(); }
    };

    上のコードは概略であり、このような形式で書くと、ベースクラスの関数を呼び出すことができます.
  • 派生クラス内の名前は、ベースクラス内の名前を隠す.公有継承の下でこれを望んだ人はいなかった.
  • マスク名を日に戻すには、using宣言式または転送関数(forwarding functions)を使用します. 

  • 条項34:インタフェースの継承と継承の実現を区別する
  • インタフェースの継承と実装は異なります.共通継承の下で、派生クラスは常にベースクラスのインタフェースを継承します.
  • 純虚関数は、インタフェース継承
  • のみを具体的に指定する.
  • 虚関数(非純)インタフェース継承およびデフォルト実装継承を具体的に指定します.(虚関数自体がデフォルト実装を定義していることを意味し、サブクラスが書き換えなければ、このベースクラスの実装が使用されます).
  • 一般関数は、インタフェースの継承および強制的な継承を具体的に指定する.(公有継承下では一般関数は書き換えられず、文法的誤りはないがis-aに合致しないため強制的に実現される).
    条項35:virtual関数以外の選択肢を考慮する
    Non-Virtual Interface手法によるTemplate Methodモードの実現
    virtual関数はprivate関数としてnon-virtual関数を使用してprivate virtual関数を呼び出します.
    例:
    class game_character {
    public:
        int health_value() const {
            ...           //       
            int ret = do_health_value();   //      
            ...           //       
            return ret; 
        }
    private:
        virtual int do_health_value() const {  //         ,       
            ...
        }
    };

    この設計は、non-virtual interface(NVI)手法とも呼ばれる.いわゆるTemplate Method設計モードの一種である.上のnon-virtual関数はwrapperと呼ぶことができる.
    NVI手法の1つの利点は、仕事ができる前、後の仕事です.例えば,事前ロック,事後ロック解除,assert検証などである. 
    Funciton PointersによるStrategy(ポリシー)モードの実装
    実際には,異なるニーズに対して異なる関数ポインタを用いてコールバックを行い,言うまでもない. 
    tr 1::functionでStrategyモードを完了
    これは実際にstd::function、あるいはboost::functionです.この本は古いので、当時はまだありませんでした.
    std::function相対関数ポインタの利点は全能にある!関数だけでなく、関数オブジェクト、メンバー関数も使用できます.ただしstd::bindとの連携に注意して、メンバー関数をバインドするには、このオブジェクトポインタを変更する必要があります. 
    クラシックなStrategyモード
    純粋なコードでポリシー・モードを実現するには、次の手順に従います.
    class game_character; //    
    
    class health_calc_func {
    public:
        virtual int calc(const game_character& gc) const 
        { ... }
    };
    health_calc_func default_health_calc;
    
    class game_character {
    public:
        explicit game_character(health_calc_func* phcf = &default_health_cal) : health_calc_(phcf)
        {}
        int health_value() const {
            return health_calc_->calc(*this); 
        }
    private:
        health_calc_func* health_calc_;
    };

    以上がポリシーモード(ポリシーモードは本当に親しみやすい)であり、上記の方法でhealth_calc_func継承システムに派生クラスを組み込むだけで、新しいアルゴリズムを追加することができる. 
    条項36:継承されたnon-virtual関数を再定義しない
    これは言うまでもなく、is-aに合致しなければならないからです. 
    条項37:継承されたデフォルトパラメータ値を再定義しない
    コード検証:
    class base {
    public:
        virtual void fun(int i = 3) { std::cout<<i<<std::endl; }
    };
    
    class derived : public base {
    public:
        virtual void fun(int i = 2)  {  std::cout<<"derived"<<std::endl; std::cout<<i<<std::endl; }
    };
    
    int main()
    {
        base *b = new derived;
        b->fun();
        return 0;
    }

    上記のコード印刷の結果は次のとおりです.
    derived  3

    これは完全にエラーの結果、派生クラス関数が呼び出されたのに、ベースクラスのデフォルト値が印刷されます.
  • 継承されたデフォルトパラメータ値を再定義しないでください.デフォルトパラメータ値は静的バインドであり、virtual関数--上書きすべき唯一のものは動的バインドです. 

  • 条項38:複合成形型におけるhas-aまたは「あるものに基づいて実現する」
    複合にはhas−aと「is−implement−in−terms−of」の2種類がある.has-aはある物にあるメンバーで、例えば教室、メンバー変数は机などで、これは理解しやすいです.主に後者は、あるものによって実現されることを意味する.例えば、自分でデータ構造の集合setを実現し、チェーンテーブルlistを使用してその下位実装を行うと、listから継承された(is-aを構成し、明らかに一致しない)、listをsetのメンバー変数として使用するべきではありません.メンバー関数はチェーンテーブル上で対応する簡単な操作を行えばsetの機能を実現できるのでsetはlistに基づいて実現される.
  • 複合(composition)の意味はpublic継承とは全く異なる.
  • アプリケーションドメイン(application domain)では、複合はhas-a(ある)を意味する.実装ドメイン(implementation domain)では、is-implemented-in-terms-of(あるものに基づいて実現される)を複合する.
    条項39:private継承を賢明かつ慎重に使用
  • private継承は、「あるものに基づいて実現される」ことを意味する.通常、複合(composition)よりもレベルが低いです.しかし、derived classがprotected base classのメンバーにアクセスする必要がある場合、または継承されたvirtual関数を再定義する必要がある場合、この設計は合理的です.
  • と複合(composition)は異なり、private継承はempty baseの最適化をもたらします.これは、「オブジェクトサイズの最小化」に取り組んでいるライブラリ開発者にとって重要かもしれません.(これは、ベースクラスが空クラスであれば、非空派生クラスがベースクラスを継承し、ベースクラスのchar占有バイトが削除されるという意味です.実際には、この本は古すぎてprivate継承を使用しなくても、現在すべてのコンパイラがこのような状況に対して最適化されており、公有継承も同様です.したがって、ここでは間違っています.)
  • プライベート継承の代わりに複合を使用できます.次のようにします.
    class base {
    private:
        virtual void timer() { std::cout<<"base"<<std::endl; }
    };
    
    class derived {
    public:
        void call() { timer_.timer(); }
    private:
        class inside_class : public base {
        public:
            virtual void timer() { std::cout<<"inside_timer"<<std::endl; }
        };  
        inside_class timer_;
        //  derived   base,      :
        //virtual void timer() { std::cout<<"derived"<<std::endl; } 
        //      derived     base,          :    ,            ,        。 
    };
    
    int main()
    {
        derived d;
        d.call();
        return 0;
    }

    派生クラスで定義できない虚関数を実現するには(注記の削除に注意):クラスAがクラスを継承する場合、その虚関数を実装するには、メンバークラスに共通して継承され、そのメンバークラスのオブジェクトが含まれているメンバークラスを使用します.メンバークラスで虚関数を書き換え、メンバークラスオブジェクトを介して虚関数を呼び出すことができます.その後、クラスBがクラスAを継承する場合、クラスAは定義できません.クラスAのプライベートオブジェクトの関数であるため、ダミー関数です. 
    条項40:賢明かつ慎重な多重継承の使用
  • 多重継承は単一継承よりも複雑である.新しい曖昧性とvirtual継承の必要性を引き起こす可能性があります.
  • virtual継承は、サイズ、速度、初期化(および付与)の複雑さなどのコストを増加させます.virtual base classesがデータを持たない場合、最も実用的な価値があります.
  • 多重継承は確かに正当な用途がある.シナリオ設計の1つ「publicはInterface classを継承する」と「privateは実現に協力するclassを継承する」の2つを結合します.