C++ コア ガイドライン:インターフェイス I

インターフェイスは、サービス プロバイダーとサービス コンシューマーの間の契約です。 C++ コア ガイドラインには、それらを正しくするための 20 のルールがあります。これは、「インターフェイスは、おそらくコード編成の最も重要な単一の側面である」ためです。

ルールについて詳しく説明する前に、20 のルールの概要を説明します。

  • I.1:インターフェースを明示的にする
  • I.2:グローバル変数を避ける
  • I.3:シングルトンを避ける
  • I.4:インターフェースを正確かつ強く型付けする
  • I.5:状態の前提条件 (ある場合)
  • I.6:Expects() を優先 前提条件を表現するため
  • I.7:状態事後条件
  • I.8:Ensures() を優先 事後条件の表現用
  • I.9:インターフェースがテンプレートの場合、概念を使用してそのパラメーターを文書化します
  • I.10:例外を使用して、必要なタスクの実行に失敗したことを知らせる
  • I.11:生のポインターによって所有権を譲渡しない (T* )
  • I.12:null であってはならないポインターを not_null として宣言する
  • I.13:配列を単一のポインタとして渡さない
  • I.22:グローバル オブジェクトの複雑な初期化を避ける
  • I.23:関数の引数の数を少なく保つ
  • I.24:同じ型の関連性のないパラメータを隣接させない
  • I.25:クラス階層へのインターフェースとして抽象クラスを好む
  • I.26:クロスコンパイラ ABI が必要な場合は、C スタイルのサブセットを使用してください
  • I.27:安定したライブラリ ABI については、Pimpl イディオムを検討してください
  • I.30:ルール違反をカプセル化する

ルールが多すぎるため、ルールの説明はあまり詳しく説明しません。私の考えでは、最初の 10 のルールについてはこの投稿で書き、残りの 10 のルールについては次の投稿で書きます。では、始めましょう。

I.1:インターフェイスを明示的にする

この規則は、正確さと手段に関するものです。仮定はインターフェースで述べる必要があります。そうしないと、見過ごされやすく、テストが難しくなります。

int round(double d)
{
 return (round_up) ? ceil(d) : d; // don't: "invisible" dependency
}

たとえば、関数 round は、その結果が目に見えない依存関係 round_up に依存することを表現していません。

I.2:グローバル変数を避ける

このルールは明らかですが、可変グローバル変数に重点が置かれています。グローバル定数は、関数に依存関係を導入できず、競合状態の影響を受けないため、問題ありません。

I.3:シングルトンを避ける

シングルトンは内部のグローバル オブジェクトであるため、避ける必要があります。

I.4:インターフェイスを正確かつ厳密に型指定する

このルールの理由は次のとおりです。「型は最も単純で最良のドキュメントであり、明確に定義された意味を持ち、コンパイル時にチェックされることが保証されています。」

例を見てみましょう:

void draw_rect(int, int, int, int); // great opportunities for mistakes
draw_rect(p.x, p.y, 10, 20); // what does 10, 20 mean?

void draw_rectangle(Point top_left, Point bottom_right);
void draw_rectangle(Point top_left, Size height_width);

draw_rectangle(p, Point{10, 20}); // two corners
draw_rectangle(p, Size{10, 20}); // one corner and a (height, width) pair

関数 draw_rect を間違った方法で使用するのはどれくらい簡単ですか?これを関数 draw_rectangle と比較してください。コンパイラは、引数が Point または Size オブジェクトであることを保証します。

したがって、多くの組み込み型引数を持つ関数や、さらに悪いことに、パラメーターとして void* を受け入れる関数については、コード改善のプロセスを調べる必要があります。


I.5:状態の前提条件 (ある場合)

可能であれば、double sqrt(double x) の x が非負でなければならないなどの前提条件は、アサーションとして表現する必要があります。

ガイドライン サポート ライブラリ (GSL) の Expects() を使用すると、前提条件を直接表現できます。

double sqrt(double x) { Expects(x >= 0); /* ... */ }

事前条件、事後条件、およびアサーションで構成されるコントラクトは、次の C++20 標準の一部になる可能性があります。提案 p03801.pdf を参照してください。

I.6:前提条件の表現に Expects() を優先する

これは前のルールと似ていますが、別の側面に重点が置かれています。たとえば、if 式、コメント、または assert() ステートメントではなく、前提条件を表現するために Expects() を使用する必要があります。

int area(int height, int width)
{
 Expects(height > 0 && width > 0); // good
 if (height <= 0 || width <= 0) my_error(); // obscure
 // ...
}

式 Expects() は見つけやすく、今後の C++20 標準でチェックできる可能性があります。

I.7:事後条件の状態、I.8:保証の優先() 事後条件を表す

関数の引数に従って、その結果について考える必要があります。したがって、事後条件ルールは以前の事前条件ルールと非常によく似ています。

I.9:インターフェースがテンプレート、概念を使用してそのパラメータを文書化

C++20 の概念で高い確率で得られます。コンセプトは、コンパイル時に評価できるテンプレート パラメーターの述語です。概念によって、テンプレート パラメーターとして受け入れられる引数のセットが制限される場合があります。概念には他にもたくさんあるので、概念については既に 4 つの投稿を書きました。

C++ コア ガイドラインのルールは非常に簡単です。それらを適用する必要があります。

template<typename Iter, typename Val>
requires InputIterator<Iter> && EqualityComparable<ValueType<Iter>>, Val>
Iter find(Iter first, Iter last, Val v)
{
 // ...
}

汎用検索アルゴリズムでは、テンプレート パラメーター Iter が InputIterator であり、テンプレート パラメーター Iter の基になる値が EqualityComparable である必要があります。この要件を満たさないテンプレート引数を使用して検索アルゴリズムを呼び出すと、読みやすく理解しやすい エラー メッセージ。


I. 10:例外を使用して、必要なタスクの実行に失敗したことを知らせる

その理由は次のとおりです。「システムや計算が未定義の (または予期しない) 状態になる可能性があるため、エラーを無視することはできません。」

この規則は悪い例と良い例を提供します。

int printf(const char* ...); // bad: return negative number if output fails

template <class F, class ...Args>
// good: throw system_error if unable to start the new thread
explicit thread(F&& f, Args&&... args);

悪いケースでは、例外を無視でき、プログラムは未定義の動作をします。

例外を使用できない場合は、値のペアを返す必要があります。 C++17 機能の構造化バインディングのおかげで、非常にエレガントに行うことができます。

auto [val, error_code] = do_something();
if (error_code == 0) {
 // ... handle the error or exit ...
}
// ... use val ...

次は?

それは簡単に推測できます。次の投稿では、ポインター、グローバル オブジェクトの初期化、関数パラメーター、抽象クラス、および ABI (アプリケーション バイナリ インターフェイス) に対する残りの規則について書きます。優れたインターフェース設計について知っておくべきことはたくさんあります。