C++ コア ガイドライン:クラス階層

この投稿では、クラス階層の一般的なルールと特定のルールについて話しましょう。 C++ コア ガイドラインには、合計で約 30 のルールがあります。そのため、話したいことがたくさんあります。

まず、クラス階層とは何ですか? C++ コア ガイドラインは明確な答えを示しています。言い換えさせてください。クラス階層は、階層的に編成された一連の概念を表します。基本クラスは通常、インターフェイスとして機能します。これらは、インターフェイスの 2 つの用途です。 1 つは実装の継承と呼ばれ、もう 1 つはインターフェイスの継承と呼ばれます。

最初の 3 行はより一般的で、別の言い方をすれば、より詳細な規則の要約です。

クラス階層ルールの概要:

  • C.120:クラス階層を使用して、固有の階層構造を持つ概念を表現します (のみ)
  • C.121:基本クラスをインターフェースとして使用する場合は、純粋な抽象クラスにします
  • C.122:インターフェースと実装を完全に分離する必要がある場合は、抽象クラスをインターフェースとして使用してください

C.120:クラス階層を使用して、固有の階層構造を持つ概念を表現する(のみ)

これは明らかです。本質的に階層構造を持つコードで何かをモデル化する場合は、階層を使用する必要があります。私にとって、自分のコードを推論する最も簡単な方法は、コードと世界が自然に一致しているかどうかです。

たとえば、複雑なシステムをモデル化する必要がありました。このシステムは、多くのサブシステムで構成される除細動器のファミリーでした。たとえば、1 つのサブシステムはユーザー インターフェイスでした。要件は、除細動器がキーボード、タッチ スクリーン、またはいくつかのボタンなどのさまざまなユーザー インターフェイスを使用することでした。このサブシステムのシステムは本質的に階層的でした。したがって、階層的な方法でモデル化しました。大きな利点は、実際のハードウェアとソフトウェアの間にこの自然な一致があったため、トップダウン方式でソフトウェアを説明するのが非常に簡単だったことです.

しかしもちろん、グラフィカル ユーザー インターフェイス (GUI) の設計で階層を使用する典型的な例です。これは、C++ コア ガイドラインが使用している例です。

class DrawableUIElement {
public:
 virtual void render() const = 0;
// ...
};
class AbstractButton : public DrawableUIElement {
public:
 virtual void onClick() = 0;
// ...
};
class PushButton : public AbstractButton {
 virtual void render() const override;
 virtual void onClick() override;
// ...
};
class Checkbox : public AbstractButton {
// ...
};

何かが本質的に階層的でない場合、階層的な方法でモデル化するべきではありません。こちらをご覧ください。

template<typename T>
class Container {
public:
 // list operations:
 virtual T& get() = 0;
 virtual void put(T&) = 0;
 virtual void insert(Position) = 0;
 // ...
 // vector operations:
 virtual T& operator[](int) = 0;
 virtual void sort() = 0;
 // ...
 // tree operations:
 virtual void balance() = 0;
 // ...
};

例が悪いのはなぜですか?コメントを読むだけです。クラス テンプレート Container は、リスト、ベクトル、およびツリーをモデル化するための純粋仮想関数で構成されます。つまり、Container をインターフェースとして使用する場合、3 つの分離概念を実装する必要があります。

C.121:基本クラスはインターフェースとして使用され、純粋な抽象クラスにします

抽象クラスは、少なくとも 1 つの純粋仮想関数を持つクラスです。純粋仮想関数 (virtual void function() =0 ) は、クラスが抽象化されるべきでない場合、派生クラスによって実装されなければならない関数です。

完全を期すためだけに。抽象クラスは、純粋仮想関数の実装を提供できます。したがって、派生クラスはこれらの実装を使用できます。

インターフェイスは通常、公開されている純粋仮想関数と、デフォルト/空の仮想デストラクタ (virtual ~My_interface() =default) で構成する必要があります。ルールに従わないと、何か悪いことが起こるかもしれません。

class Goof {
public:
// ...only pure virtual functions here ...
// no virtual destructor
};
class Derived : public Goof {
string s;
// ...
};
void use()
{
 unique_ptr<Goof> p {new Derived{"here we go"}};
 f(p.get()); // use Derived through the Goof interface 
} // leak

p が範囲外になると破棄されます。しかし、Goof には仮想デストラクタがありません。したがって、Derived ではなく Goof のデストラクタが呼び出されます。悪い影響は、文字列 s のデストラクタが呼び出されないことです。

C.122:抽象クラスをインターフェイスと実装を完全に分離する必要がある場合のインターフェイス

抽象クラスは、インターフェースと実装の分離に関するものです。インターフェイスのみに依存するため、実行時に次の例の Device の別の実装を使用できるという効果があります。

struct Device {
 virtual void write(span<const char> outbuf) = 0;
 virtual void read(span<char> inbuf) = 0;
};
class D1 : public Device {
// ... data ...
void write(span<const char> outbuf) override;
 void read(span<char> inbuf) override;
};
class D2 : public Device {
// ... different data ...
 void write(span<const char> outbuf) override;
 void read(span<char> inbuf) override;
};

パターンを設計するための私のセミナーでは、この規則をメタ設計パターンと呼ぶことがよくあります。これは、最も影響力のあるソフトウェアの本である Design Patterns:Elements of Reusable Object-Oriented Software.<の多くの設計パターンのベースとなっています。 /b>

階層の要約におけるクラスのルールの設計:

ここでは、より詳細なルールを要約します。ガイドラインには 15 個あります。

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

今日は最初の 3 つについて書きます。

C.126:通常、抽象クラスはコンストラクタを必要としません

通常、抽象クラスにはデータがないため、それらを初期化するためのコンストラクターは必要ありません。

C.127:仮想関数を持つクラス仮想または保護されたデストラクタが必要です

仮想関数を持つクラスは、ほとんどの場合、ポインターまたはベースへの参照を介して使用されます。ポインターまたはベースへの参照を介して、またはスマート ポインターを介して間接的に派生クラスを明示的に削除する場合は、派生クラスのデストラクタも呼び出されることを確認する必要があります。この規則は、純粋仮想関数について述べている規則 C.121 と非常によく似ています。

破壊の問題を解決する別の方法は、保護された非仮想基本クラスのデストラクタを使用することです。このデストラクタは、ベースへのポインタまたは参照を介して派生オブジェクトを削除できないことを保証します。

C.128:仮想関数は virtualoverride 、または final

C++11 では、オーバーライドを処理する 3 つのキーワードがあります。

  • バーチャル :派生クラスで上書きできる関数を宣言します
  • オーバーライド :関数が仮想であることを保証し、基本クラスの仮想関数を上書きします
  • 最終: 関数が仮想であり、派生クラスによってオーバーライドできないことを保証します

ガイドラインによると、3 つのキーワードの使用に関するルールは単純です。「virtual を使用する」 新しい仮想関数を宣言する場合のみ。 override を使用 オーバーライドを宣言する場合のみ。 final を使用 最終オーバーライドを宣言する場合のみ。"

struct Base{
 virtual void testGood(){}
 virtual void testBad(){}
};

struct Derived: Base{
 void testGood() final {}
 virtual void testBad() final override {}
};

int main(){
 Derived d;
}

クラス Derived のメソッド testBad() には、多くの冗長な情報があります。

  • 関数が仮想の場合は、final または override のみを使用してください。仮想をスキップ:void testBad() 最終オーバーライド{}
  • virtual キーワードなしでキーワード final を使用することは、関数が既に virtual である場合にのみ有効です。したがって、関数は基本クラスの仮想関数をオーバーライドする必要があります。オーバーライドをスキップ:void testBad() final {}

次は?

クラス階層に関する残りの 12 のルールが欠落しています。次の投稿でこのギャップを埋めます。