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

C++ コア ガイドラインのクラス階層に関する 20 のルールを提示するには、3 つの投稿が必要でした。残りの 7 つのルールは次のとおりです。

素晴らしい写真を撮るために。これらはすべてクラス階層の特別な規則です。

  • 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:仮想関数とオーバーライドに異なるデフォルト引数を提供しない

ルール C.134 を続けましょう。

C.134:すべての非 const を保証する データ メンバーは同じアクセス レベルを持っています

以前のルール C.133 では、保護されたデータを避けるべきであると述べられていました。これは、すべての非 const データ メンバーがパブリックまたはプライベートである必要があることを意味します。オブジェクトは、オブジェクトの不変条件を規定しないデータ メンバを持つことができます。オブジェクトの不変条件を規定しない非 const データ メンバーは、パブリックにする必要があります。対照的に、非 const プライベート データ メンバーは、オブジェクトの不変条件に使用されます。注意:不変式を持つデータ メンバーは、基になる型のすべての値を持つことはできません。

クラス設計をより一般的に考えると、2 種類のクラスに気付くでしょう。

  • すべて公開 :データ メンバーには不変条件がないため、パブリック データ メンバーのみを持つクラス。正直なところ、構造体を使用する必要があります。
  • すべて非公開 :不変条件を確立したプライベート データ メンバーまたは const データ メンバーのみを持つクラス。

この観察に基づいて、非 const データ メンバーはすべて public または private にする必要があります。

public で非定数の不変条件を持つクラスがあるとします。これは、クラス階層全体を通じてデータ メンバーの不変性を維持する必要があることを意味します。クラスの不変条件を簡単に制御できないため、これは非常にエラーが発生しやすくなります。あるいは違う言い方をする。カプセル化を解除します。

C.135:多重継承を使用して複数の異なるインターフェースを表す

インターフェイスが設計の 1 つの側面のみをサポートすることをお勧めします。どういう意味ですか?純粋な仮想関数のみで構成される純粋なインターフェイスを提供する場合、具象クラスはすべての関数を実装する必要があります。これは、特にインターフェースがリッチすぎる場合、クラスは必要のない機能や意味をなさない機能を実装する必要があることを意味します。

2 つの異なるインターフェイスの例として、入力および出力ストリーム ライブラリの istream と ostream があります。

class iostream : public istream, public ostream { // very simplified
 // ...
};

入力操作用の istream と出力操作用の ostream の両方のインターフェイスを組み合わせることで、新しいインターフェイスを非常に簡単に作成できます。

C.136:多重継承を使用して実装属性の結合を表現する、C.137:virtual を使用する 過度に一般的な基本クラスを避けるための基本

どちらのルールも非常に特殊です。したがって、私はそれらをスキップします。ガイドラインでは、C.137 は比較的めったに使用されず、C.138 は C.129 に似ていると述べられています。129:クラス階層を設計するときは、実装の継承とインターフェースの継承を区別してください。

C.138:派生クラスのオーバーロード セットを作成するusing のベース

この規則は非常に明白であり、仮想関数と非仮想関数に適用されます。 using 宣言を使用しない場合、派生クラスのメンバー関数はオーバーロード セット全体を非表示にします。このプロセスはシャドーイングと呼ばれることもあります。このルールを破ると、かなり混乱することがよくあります。

ガイドラインの例は、このルールを明確に示しています。

class B {
public:
 virtual int f(int i) { std::cout << "f(int): "; return i; }
 virtual double f(double d) { std::cout << "f(double): "; return d; }
};
class D: public B {
public:
 int f(int i) override { std::cout << "f(int): "; return i + 1; }
};
int main()
{
 D d;
 std::cout << d.f(2) << '\n'; // prints "f(int): 3"
 std::cout << d.f(2.3) << '\n'; // prints "f(int): 3"
}

最後の行を見てください。 double 引数を持つ d.f(2.3) が呼び出されますが、クラス D の int オーバーロードが使用されます。したがって、double から int への縮小変換が発生します。それはほとんどの場合、あなたが望む動作ではありません。クラス B の二重オーバーロードを使用するには、D のスコープに導入する必要があります。

class D: public B {
public:
 int f(int i) override { std::cout << "f(int): "; return i + 1; }
 using B::f; // exposes f(double)
};

C.139:final を使用 控えめに

final は C++11 の新機能です。クラスまたは仮想関数に使用できます。

  • クラス ウィジェットからクラス My_widget final を派生させた場合、さらに My_widget からクラスを派生させることはできません。
class Widget { /* ... */ };

// nobody will ever want to improve My_widget (or so you thought)
class My_widget final : public Widget { /* ... */ };

class My_improved_widget : public My_widget { /* ... */ }; // error: can't do that

  • 仮想関数を final として宣言できます。つまり、派生クラスで関数をオーバーライドすることはできません。
    struct Base
    {
     virtual void foo();
    };
     
    struct A : Base
    {
     void foo() final; // A::foo is overridden and it is the final override
    };
     
    struct B final : A // struct B is final
    {
     void foo() override; // Error: foo cannot be overridden as it's final in A
    };
    

    final を使用する場合は、クラス ベースまたは仮想関数ベースでクラス階層を封印します。多くの場合、それはあなたが監視できない結果をもたらします。 final を使用することによる潜在的なパフォーマンス上の利点については、よく考えてください。

    C.140:異なるデフォルト引数を仮想関数とオーバーライド

    このルールに従わないと、多くの混乱が生じる可能性があります。ご覧ください。

    // overrider.cpp
    
    #include <iostream>
    
    class Base {
    public:
     virtual int multiply(int value, int factor = 2) = 0;
    };
    
    class Derived : public Base {
    public:
     int multiply(int value, int factor = 10) override {
     return factor * value;
     }
    };
    
    int main(){
    
     std::cout << std::endl;
    
     Derived d;
     Base& b = d;
    
     std::cout << "b.multiply(10): " << b.multiply(10) << std::endl; 
     std::cout << "d.multiply(10): " << d.multiply(10) << std::endl; 
    
     std::cout << std::endl;
    
    }
    

    これは、プログラムの非常に驚くべき出力です。

    何が起こっていますか?オブジェクト b と d の両方が同じ関数を呼び出します。これは、関数が仮想であるため、遅延バインディングが発生するためです。これは、デフォルト引数などのデータには当てはまりません。それらは静的にバインドされ、早期バインディングが発生します。

    次は?

    これで、クラス階層の設計が完了しました。クラス階層内のオブジェクトに誰がアクセスできるかという問題が残ります。もちろん、この質問には次の投稿でお答えします。