C++ コア ガイドライン:境界の安全性

今日の投稿は、C++ コア ガイドラインの 2 番目のプロファイルである境界の安全性に関するものです。プロファイル境界安全性の目標は、割り当てられたメモリの境界内で操作することです。

プロファイルは、境界の安全性のために、ポインター演算と配列インデックス付けの 2 つの敵に名前を付けます。さらに、ポインターを使用する場合は、単一のオブジェクトのみをアドレス指定する必要があり、配列はアドレス指定できません。プロファイル境界の安全性を完全なものにするには、それを規則と組み合わせて安全性と生涯安全性を入力する必要があります。型の安全性は、私の以前の 2 つの投稿 (C++ コア ガイドライン:型の安全性と C++ コアのガイドライン:設計による型の安全性) のトピックでした。次回の投稿のトピックは生涯安全です。

境界の安全性

境界の安全性は 4 つのルールで構成されます:

  • Bounds.1:ポインター演算を使用しない
  • Bounds.2:定数式を使用した配列への唯一のインデックス
  • Bounds.3:配列からポインタへの減衰なし
  • Bounds.4:境界チェックされていない標準ライブラリの関数と型を使用しない

安全性を制限するための 4 つのルールは、C++ コア ガイドラインの 3 つのルールに言及しています。プロフィールへの最後の投稿のように、必要に応じて追加します。

Bounds.1:ポインター演算を使用しないでください、 Bounds.2:定数式を使用した配列へのインデックスのみ、および Bounds.3:配列からポインターへの減衰なし

3 つのルールの理由は、3 つのすべきこと、つまり単一オブジェクトへのポインターの受け渡し (のみ)、ポインター演算をシンプルに保つこと、および std::span の使用に要約されます。最初の do は否定的に定式化することもできます:ポインターを配列に渡さないでください。 std::span を知らないと思います。 std::span は、連続したメモリの非所有範囲を表します。この範囲は、配列、サイズのあるポインター、または std::vector にすることができます。

ガイドラインの言葉を引用させてください:「複雑なポインター操作はエラーの主な原因です .". なぜ気にする必要があるのでしょうか? もちろん、私たちのレガシー コードには、次の例のような機能が満載です:

void f(int* p, int count)
{
 if (count < 2) return;

 int* q = p + 1; // BAD

 int n = *p++; // BAD

 if (count < 6) return;

 p[4] = 1; // BAD

 p[count - 1] = 2; // BAD

 use(&p[0], 3); // BAD
}

int myArray[100]; // (1)

f(myArray, 100), // (2)

このコードの主な問題は、呼び出し元が C 配列の正しい長さを提供する必要があることです。そうでない場合、未定義の動作が発生します。

最後の行 (1) と (2) について数秒間考えてみてください。配列から始めて、関数 f に渡すことでその型情報を削除します。このプロセスは配列からポインターへの減衰と呼ばれ、多くのエラーの原因となっています。たぶん、私たちは悪い日を過ごし、要素の数を間違って数えたり、C 配列のサイズを変更したりしました。とにかく、結果は同じです:未定義の動作です。同じ議論が C 文字列にも当てはまります。

私たちは何をすべきか?適切なデータ型を使用する必要があります。 C++20 は std::span をサポートしています。こちらをご覧ください:

void f(span<int> a) // BETTER: use span in the function declaration
{
 if (a.length() < 2) return;

 int n = a[0]; // OK

 span<int> q = a.subspan(1); // OK

 if (a.length() < 6) return;

 a[4] = 1; // OK

 a[count - 1] = 2; // OK

 use(a.data(), 3); // OK
}

罰金! std::span は実行時にその境界をチェックします。

しかし、あなたの不満を聞きます:私たちは C++20 を持っていません。問題ない。コンテナー std::array とメソッド std::array::at を使用して関数 f を書き直すのは非常に簡単です。

// spanVersusArray.cpp

#include <algorithm>
#include <array>

void use(int*, int){}

void f(std::array<int, 100>& a){

 if (a.size() < 2) return;

 int n = a.at(0); 

 std::array<int, 99> q;
 std::copy(a.begin() + 1, a.end(), q.begin()); // (1)

 if (a.size() < 6) return;

 a.at(4) = 1; 

 a.at(a.size() - 1) = 2;

 use(a.data(), 3); 
}

int main(){

 std::array<int, 100> arr{};

 f(arr);
 
}

std::array::at 演算子は、実行時にその境界をチェックします。 pos>=size() の場合、std::out_of_range 例外が発生します。 spanVersusArray.cpp プログラムを注意深く見ると、2 つの問題に気付くでしょう。 1 つ目は、式 (1) が std::span バージョンよりも冗長であり、2 つ目は、std::array のサイズが関数 f のシグネチャの一部になっていることです。これは悪いです。 f は std::array 型でのみ使用できます。この場合、関数内の配列サイズのチェックは不要です。

あなたの救助のために、C++ にはテンプレートがあります。したがって、タイプの制限を簡単に克服できますが、タイプ セーフを維持できます。

// at.cpp

#include <algorithm>
#include <array>
#include <deque>
#include <string>
#include <vector>

template <typename T>
void use(T*, int){}

template <typename T>
void f(T& a){

 if (a.size() < 2) return;

 int n = a.at(0); 

 std::array<typename T::value_type , 99> q; // (5)
 std::copy(a.begin() + 1, a.end(), q.begin()); 

 if (a.size() < 6) return;

 a.at(4) = 1; 

 a.at(a.size() - 1) = 2;

 use(a.data(), 3); // (6)
}

int main(){

 std::array<int, 100> arr{}; 
 f(arr); // (1)
 
 std::array<double, 20> arr2{};
 f(arr2); // (2)
 
 std::vector<double> vec{1, 2, 3, 4, 5, 6, 7, 8, 9};
 f(vec); // (3)
 
 std::string myString= "123456789";
 f(myString); // (4)
 
 // std::deque<int> deq{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 // f(deq); 
 
}

現在、関数 f は、さまざまなサイズとタイプの std::array (行 (1) と (2)) に対して機能しますが、std::vector (3) または std::string (4) に対しても機能します。これらのコンテナーには、データが連続したメモリ ブロックに格納されているという共通点があります。これは std::deque; を保持しません。したがって、式 (6) の a.data() の呼び出しは失敗します。 std::deque は、小さなメモリ ブロックの一種の二重リンク リストです。

式 T::value_type (5) は、各コンテナーの基になる値の型を取得するのに役立ちます。 T は関数テンプレート f の型パラメーターであるため、T はいわゆる従属型です。これが理由で、T::value_type が typename T::value_type であるというヒントをコンパイラに与える必要があります。

Bounds.4:標準を使用しないでください-境界チェックされていないライブラリ関数と型

C++ コア ガイドライン:境界エラーを回避するという記事を既に書いています。この投稿では、このルールの背景情報を提供し、すべきことを提供します。

次は?

3 番目のプロファイルの名前は、Lifetime Safety Profile です。次回の投稿のトピックであるこのプロファイルは、1 つのルールに集約されます:無効の可能性があるポインターを逆参照しないでください。