C++ コア ガイドライン:クラス階層に関するその他の規則

前回の投稿では、最新の C++ でのクラス階層への規則から旅を始めました。最初のルールは非常に一般的なものでした。今回も旅を続けます。ここで、ルールに焦点を当てます。

クラス階層のルールは次のとおりです。

  • C.126:通常、抽象クラスはコンストラクタを必要としません
  • C.127:仮想関数を持つクラスには、仮想または保護されたデストラクタが必要です
  • C.128:仮想関数は virtual のいずれかを指定する必要があります 、 override 、または final
  • C.129:クラス階層を設計するときは、実装の継承とインターフェイスの継承を区別してください
  • C.130:基本クラスのコピーを再定義または禁止します。仮想 clone を好む 代わりに関数
  • C.131:単純なゲッターとセッターを避ける
  • C.132:関数を virtual にしないでください 理由もなく
  • C.133:protected を避ける データ
  • C.134:すべての非 const を確認してください データ メンバーは同じアクセス レベルを持っています
  • C.135:多重継承を使用して複数の異なるインターフェースを表す
  • C.136:多重継承を使用して実装属性の和集合を表す
  • C.137:virtual を使用 過度に一般的な基本クラスを避けるための基本
  • C.138:using を使用して、派生クラスとそのベースのオーバーロード セットを作成する
  • C.139:final を使用 控えめに
  • C.140:仮想関数とオーバーライドに異なるデフォルト引数を提供しない

4 番目のものを続けましょう。

C.129:クラス階層を設計するときは、実装の継承とインターフェイスの継承

まず、実装継承とインターフェース継承の違いは何ですか?ガイドラインは明確な答えを与えます。引用させてください。

  • インターフェースの継承 継承を使用してユーザーを実装から分離することです。特に、基本クラスのユーザーに影響を与えずに派生クラスを追加および変更できるようにすることです。
  • 実装の継承 継承を使用して、関連する新しい操作の実装者が便利な操作を利用できるようにすることで、新しい機能の実装を簡素化します (「違いによるプログラミング」とも呼ばれます)。

純粋なインターフェイスの継承は、インターフェイス クラスに純粋な仮想関数しかない場合です。対照的に、基本クラスにデータ メンバーまたは実装された関数がある場合は、実装の継承があります。ガイドラインは、両方の概念を混在させる例を示しています。

class Shape { // BAD, mixed interface and implementation
public:
 Shape();
 Shape(Point ce = {0, 0}, Color co = none): cent{ce}, col {co} { /* ... */}

 Point center() const { return cent; }
 Color color() const { return col; }

 virtual void rotate(int) = 0;
 virtual void move(Point p) { cent = p; redraw(); }

 virtual void redraw();

 // ...
public:
 Point cent;
 Color col;
};

class Circle : public Shape {
public:
 Circle(Point c, int r) :Shape{c}, rad{r} { /* ... */ }

 // ...
private:
 int rad;
};

class Triangle : public Shape {
public:
 Triangle(Point p1, Point p2, Point p3); // calculate center
 // ...
};

クラス Shape が悪いのはなぜですか?

  • クラスが大きくなるほど、さまざまなコンストラクターを維持することが難しくなり、エラーが発生しやすくなります。
  • Shape クラスの関数は使用できません。
  • Shape クラスにデータを追加すると、再コンパイルが発生する可能性があります。

Shape が純粋な仮想関数のみで構成される純粋なインターフェイスである場合、コンストラクターは必要ありません。もちろん、純粋なインターフェースでは、すべての機能を派生クラスに実装する必要があります。

インターフェイス階層を持つ安定したインターフェイスと、実装の継承によるコードの再利用という 2 つの世界のベストを得るにはどうすればよいでしょうか。考えられる答えの 1 つは、二重継承です。これは、それを行うための非常に洗練されたレシートです。

<強い>1.クラス階層のベース Shape を純粋なインターフェースとして定義

<オール>
    class Shape { // pure interface
    public:
     virtual Point center() const = 0;
     virtual Color color() const = 0;
    
     virtual void rotate(int) = 0;
     virtual void move(Point p) = 0;
    
     virtual void redraw() = 0;
    
     // ...
    };
    

    <強い>2. Shape から純粋なインターフェイス Circle を派生

    class Circle : public virtual ::Shape { // pure interface
    public:
     virtual int radius() = 0;
     // ...
    };
    

    <強い>3.実装クラス Impl::Shape の提供

    class Impl::Shape : public virtual ::Shape { // implementation
    public:
     // constructors, destructor
     // ...
     Point center() const override { /* ... */ }
     Color color() const override { /* ... */ }
    
     void rotate(int) override { /* ... */ }
     void move(Point p) override { /* ... */ }
    
     void redraw() override { /* ... */ }
    
     // ...
    };
    

    <強い>4.インターフェイスと実装から継承してクラス Impl::Circle を実装します

    class Impl::Circle : public virtual ::Circle, public Impl::Shape { // implementation
    public:
     // constructors, destructor
    
     int radius() override { /* ... */ }
     // ...
    };
    

    <強い>5.クラス階層を拡張したい場合は、インターフェイスと実装から派生する必要があります

    クラス Smiley は、Circle から派生した純粋なインターフェースです。クラス Impl::Smiley は新しい実装であり、Smiley と Impl::Circle から派生した public です。

    class Smiley : public virtual Circle { // pure interface
    public:
     // ...
    };
    
    class Impl::Smiley : public virtual ::Smiley, public Impl::Circle { // implementation
    public:
     // constructors, destructor
     // ...
    }
    

    2 つの階層の全体像をもう一度示します。

    • インターフェース:スマイリー -> 円 -> シェイプ
    • 実装:Impl::Smiley -> Imply::Circle -> Impl::Shape

    最後の行を読んで、既視感があったかもしれません。あなたが正しいです。この多重継承の手法は、多重継承で実装されるアダプター パターンに似ています。アダプタ パターンは、有名なデザイン パターン ブックからのものです。

    アダプター パターンの考え方は、インターフェイスを別のインターフェイスに変換することです。これを実現するには、新しいインターフェイスからパブリックを継承し、古いインターフェイスからプライベートを継承します。これは、古いインターフェースを実装として使用することを意味します。

    C.130:再定義またはコピー禁止基本クラスの場合。仮想 clone を好む 代わりに関数

    かなり短くできます。規則 C.67 は、この規則について適切な説明を提供します。

    C.131:単純なゲッターとセッターを避ける

    単純なゲッターまたはセッターがセマンティック値を提供しない場合は、データ項目をパブリックにします。単純な getter と setter の 2 つの例を次に示します。

    class Point { // Bad: verbose
     int x;
     int y;
    public:
     Point(int xx, int yy) : x{xx}, y{yy} { }
     int get_x() const { return x; }
     void set_x(int xx) { x = xx; }
     int get_y() const { return y; }
     void set_y(int yy) { y = yy; }
     // no behavioral member functions
    };
    

    x と y には任意の値を指定できます。これは、Point のインスタンスが x と y に対して不変条件を維持しないことを意味します。 x と y は単なる値です。構造体を値のコレクションとして使用する方が適切です。

    struct Point {
     int x {0};
     int y {0};
    };
    

    C.132:関数を作成しない virtual 理由もなく

    これは明らかです。仮想機能は、無料では入手できない機能です。

    仮想関数

    • 実行時間とオブジェクトのコードサイズが増加します
    • 派生クラスで上書きされる可能性があるため、間違いの可能性があります

    C.133:protected を避ける データ

    保護されたデータにより、プログラムが複雑になり、エラーが発生しやすくなります。保護されたデータを基本クラスに入れると、派生クラスを分離して推論することはできず、したがってカプセル化を破ることになります。クラス階層全体について常に推論する必要があります。

    つまり、少なくともこれら 3 つの質問に答える必要があります。

    <オール>
  1. 保護されたデータを初期化するためにコンストラクタを実装する必要がありますか?
  2. 保護されたデータを使用した場合、そのデータの実際の価値は?
  3. 保護されたデータを変更すると誰が影響を受けますか?
  4. これらの質問に答えるのは、クラス階層が大きくなるほど難しくなります。

    考えてみると、保護されたデータは、クラス階層のスコープ内の一種のグローバル データです。ご存知のように、非 const グローバル データは良くありません。

    これは、保護されたデータで強化されたインターフェイス Shape です。

    class Shape {
    public:
     // ... interface functions ...
    protected:
     // data for use in derived classes:
     Color fill_color;
     Color edge_color;
     Style st;
    };
    

    次のステップ

    クラス階層のルールはまだ終わっていないので、次の投稿でツアーを続けます。

    個人的な告白をしなければなりません。 C++ コア ガイドライン ルールを言い換えて、必要に応じて背景情報を提供することで、多くのことを学びました。同じことがあなたにも当てはまることを願っています。コメントをいただければ幸いです。あなたの意見は?