C++ コア ガイドライン:階層内のオブジェクトへのアクセス

クラス階層内のオブジェクトにアクセスするには、9 つ​​の規則があります。詳しく見てみましょう。

これが 9 つのルールです。

階層内のオブジェクトへのアクセス ルールの概要:

  • C.145:ポインタと参照を介してポリモーフィック オブジェクトにアクセスする
  • C.146:05 を使用 クラス階層のナビゲーションが避けられない場所
  • C.147:12 を使用 必要なクラスが見つからないことがエラーと見なされる場合の参照型へ
  • C.148:29 を使用 必要なクラスが見つからないことが有効な代替と見なされる場合のポインター型へ
  • C.149:35 を使用 または 49 51 を忘れないように 64 を使用して作成されたオブジェクト
  • C.150:71 を使用 84 が所有するオブジェクトを構築する
  • C.151:91 を使用 102 が所有するオブジェクトを構築する
  • C.152:派生クラス オブジェクトの配列へのポインターを、そのベースへのポインターに割り当てないでください
  • C.153:キャストよりも仮想関数を優先

私を信じてください。スライスは、多くの C++ コードベースの問題です。

C.145:ポインタと参照を介してポリモーフィック オブジェクトにアクセスする

仮想関数にアクセスする場合、どのクラスがその機能を提供するかわかりません。したがって、ポインターまたは参照を使用する必要があります。これは、具体的な例では、両方の d がスライスされていることを意味します。

struct B{ 
 int a; 
 virtual int f(); 
};

struct D : B{ 
 int b; 
 int f() override; 
};

void use(B b)
{
 D d;
 B b2 = d; // slice
 B b3 = b;
}

void use2()
{
 D d;
 use(d); // slice
}

最初と 2 番目のスライスにより、D の B 部分のみがコピーされます。

スライスについてもっと知りたいですか? C.67:基本クラスはコピーを抑制し、「コピー」が必要な場合は代わりに仮想クローンを提供する必要があります。この問題について説明します。

次の 3 つのルールは、dynamic_cast に関するものです。 dynamic_cast について書く前に強調しておきますが、dynamic_cast を含むキャストはあまりにも頻繁に使用されます。 dynamic_cast の仕事は、「継承階層に沿って、クラスへのポインタと参照を上下左右に安全に変換する」ことです。 (http://en.cppreference.com/w/cpp/language/dynamic_cast)

C.146:117 を使用 クラス階層のナビゲーションが避けられない場所

C++ コア ガイドラインのユース ケースを次に示します。クラス階層をナビゲートしたい。

struct B { // an interface
 virtual void f();
 virtual void g();
};

struct D : B { // a wider interface
 void f() override;
 virtual void h();
};

void user(B* pb)
{
 if (D* pd = dynamic_cast<D*>(pb)) { // (1)
 // ... use D's interface ...
 }
 else {
 // ... make do with B's interface ...
 }
}

pb (1) の正しいタイプを検出するには 実行時に dynamic_cast が必要です。キャストが失敗すると、null ポインターが返されます。

パフォーマンス上の理由から、コンパイル時にキャストする必要があります。したがって、 static_cast はあなたの友達です。これで、プログラムの安全性の種類に違反することができます。

void user2(B* pb) // bad
{
 D* pd = static_cast<D*>(pb); // I know that pb really points to a D; trust me
 // ... use D's interface ...
}

void user3(B* pb) // unsafe
{
 if (some_condition) {
 D* pd = static_cast<D*>(pb); // I know that pb really points to a D; trust me
 // ... use D's interface ...
 }
 else {
 // ... make do with B's interface ...
 }
}

void f()
{
 B b;
 user(&b); // OK
 user2(&b); // bad error (1)
 user3(&b); // OK *if* the programmer got the some_condition check right (2)
}

B へのポインターを D (1) へのポインターにキャストするとエラーになります。これはおそらく最後の行 (2) にも当てはまります。

C.147:125 を使用 必要なクラスが見つからないことがエラーと見なされる場合の参照型へ

ポインターへの dynamic_cast を作成すると、失敗した場合に null ポインターが返されます。ただし、参照に対して dynamic_cast を作成すると、失敗します。具体的には、std::bad_cast 例外が発生します。

// badCast.cpp

struct A{
 virtual void f() {}
};
struct B : A {};

int main(){
 
 A a;
 B b;

 B* b1 = dynamic_cast<B*>(&a); // nullptr, because 'a' is not a 'B'
 B& b2 = dynamic_cast<B&>(a); // std::bad_cast, because 'a' is not a 'B' 
 
}

g++-6 コンパイラは両方の悪い dynamic_cast について文句を言い、ランタイムは参照の場合に予想される例外をスローします。

C.148:134 を使用 必要なクラスが見つからないことが有効な代替と見なされる場合のポインター型へ

ポインター型への dynamic_cast が失敗し、null ポインターが返される場合は、代替コード パスを選択することが有効なオプションである場合があります。

C.149:<を使用コード>143 または 157 166 を忘れないように 170 を使用して作成されたオブジェクト

std::unique_ptr または std::shared_ptr を使用することは非常に重要ですが、リソース リークを回避するための非常に明白なルールでもあります。ライブラリなどのインフラストラクチャではなく、アプリケーションを構築する場合は、次のように言い換えてみましょう。

このルールを適用すると、スマート ポインターの作成に std::make_unique と std::make_shared を使用する必要があります。

C.150:185 を使用 199 が所有するオブジェクトを構築する s、C.151:200 を使用 210 が所有するオブジェクトを構築する さ

どちらのルールも非常に似ています。したがって、私はそれらを一緒に扱うことができます。 std::make_unique と std::make_shared は、操作が決してインターリーブされないことを保証します。つまり、次の例では、メモリ リークは発生しません。

f(std::make_unique<Foo>(), bar());

この保証は、次の呼び出しには適用されません。

f(std::unique_ptr<Foo>(new Foo()), bar());

Foo が最初にヒープに割り当てられ、次に bar が呼び出されることがあります。 bar が例外をスローすると、Foo は破棄されず、メモリ リークが発生します。

std::shared_ptr を作成するための std::make_share についても同じことが言えます。 std::make_shared には、追加のパフォーマンス上の利点があります。 std::shared_ptr を作成するには、2 つのメモリ割り当てが必要です。 1 つはリソース用、もう 1 つはカウンター用です。 std::make_shared を使用すると、両方の高価な割り当てが 1 つのステップで行われます。パフォーマンスの違いは劇的です。私の投稿をご覧ください:スマート ポインターのメモリとパフォーマンスのオーバーヘッド。

C. 152:派生クラス オブジェクトの配列へのポインターを、そのベースへのポインターに割り当てない

これはそれほど頻繁には発生しないかもしれませんが、発生した場合、結果は非常に悪いものになる可能性があります.その結果、無効なオブジェクト アクセスまたはメモリの破損が発生する可能性があります。前の問題は例に示されています。

struct B { int x; };
struct D : B { int y; };

D a[] = {{1, 2}, {3, 4}, {5, 6}};
B* p = a; // bad: a decays to &a[0] which is converted to a B*
p[1].x = 7; // overwrite D[0].y

最後の代入は、B のインスタンスの x 属性を更新する必要がありますが、D の y 属性を上書きします。その理由は、B* に派生オブジェクト D の配列へのポインターが割り当てられているためです。

Decay は暗黙的な変換の名前で、左辺値から右辺値、配列からポインター、および関数からポインターへの変換を適用し、const および volatile 修飾子を削除します。つまり、具体的な例では、D の配列を持つ D* を受け入れる関数を呼び出すことができます。次の関数の引数 d は、D の最初の要素へのポインターを持ちます。D の配列の長さなどの貴重な情報は失われます。222 233

void use(D* d);
D d[] = {{1, 2}, {3, 4}, {5, 6}};

use(d);

C.153:キャストよりも仮想関数を優先

dynamic_cast を使用して、しばしばレイト バインディングとも呼ばれる仮想動作をシミュレートできます。しかし、それは見苦しく、エラーが発生しやすいものです。 null ポインターまたは std::bad_cast 例外が発生する場合があります (C.147 を参照)。仮想関数について詳しく知りたい場合は、投稿 C++ Core Guidelines:Rules for Copy and Move の規則 C67 をお読みください。 245

次は?

C++ では、関数、関数テンプレート、さらには演算子をオーバーロードできます。特に、演算子のオーバーロードは、非常に物議を醸す議論になることがよくあります。たとえば、C++ の安全なサブセットのガイドラインである MISRA C++ は、演算子のオーバーロードを禁止しています。実を言うと。なぜだろう? C++ コア ガイドラインには、オーバーロードに関する 10 のルールがあり、これについては次回の記事で取り上げます。