株式会社空想科学テクノロジー

メモリの安全性

*C++ のメモリ安全の不具合は、書けないか、起きないか、その場で止まります。止まった理由も、そこで出します。

安全性か高速性か

安全全て有効で動きます。null、境界、ゼロクリア、型、競合の 5 つの検査が全て効き、不正な操作は、その瞬間にその場所で止まります。言語が保証するのは、この姿です。
速度要らない検査を、外せます。外した分だけ速く動きます。外すのはプログラマーの選択で、言語が保証する範囲外です。外した検査は、不正な操作があっても止まりません。

検査を外すのは、速くするための調整です。 言語の側は、全て有効な姿だけを保証します。

※ 速さは、お使いになる環境によって変わります。

基盤は効率性で決まる

効率性とは、メモリ管理をどこまで仕組みに任せるかの段階です(= 効率性 のページ)。

オブジェクトの寿命は、仕組みが管理します。 解放したものを使う、二度解放する、返し忘れる。この 3 つは、寿命をプログラマーが管理しているために起きます。 *C++ では仕組みが寿命を管理しますので、起きません。 これは検査ではなく、構造です。効率性の段階を下げれば、その分だけ弱くなります(= 段階 0 では、いまの C++ と同じです)。

寿命を任せた型には、書き方の制約が 4 つ付きます(= 実体を直接置く、生ポインタで持つ、データを持つ基底を 2 つ以上継承する、自分で delete する)。 この制約で、次の不具合は *C++ で書いた部分では書けない形になります。これも検査ではなく、構造です。

いまの C++ で起きる不具合どう起きるか消す制約
スコープを抜けた実体を指し続けるローカルの実体への参照を返す。ラムダが捕まえたローカルが先に消える。実体を直接置けない
切り捨て(slicing)派生を基底の値へコピーして、派生の分が落ちる。実体を直接置けない(= 値のコピーも無い)
ポインタ演算隣の番地を読む・書く。生ポインタで持てない
基底のずれ2 経路の基底や、違う基底経由の cast で番地がずれる。データを持つ基底を 2 つ以上継承できない
new と delete の対の不一致new[] で取ったものを delete で返す。独自アロケータの取り違え。自分で delete できない

※ Microsoft は 2019 年に、自社が毎年 CVE(= 公開された脆弱性 1 件ごとに付く、業界共通の番号) を付ける脆弱性のうち約 70% がメモリ安全の問題だと公表しています。Google も、Chromium の深刻な脆弱性の約 70% が同じ問題だと公表しています。解放したものを使う、二度解放する、の 2 つはその中に入ります。

5 つの検査項目

4 つは Java や C# にもある検査と同じ意味で、競合の検出はどちらにも無く、*C++ が上回る所です。

Java や C# では外すと、C++ で起きること効く場面
null 検査NullPointerExceptionNullReferenceExceptionnull を辿って異常終了する場所が、判らなくなります。参照のたびに 1 回の比較
境界検査ArrayIndexOutOfBoundsExceptionIndexOutOfRangeException隣のメモリを、検出されずに壊します。添字のたびに 1 回の比較多次元は、次元の数だけ比較が増えます。
ゼロクリアフィールドは既定値(0)で初期化されます。未初期化の値を読みます。確保のたびに埋めます
型の検査ClassCastExceptionInvalidCastException別の型として読み進み、離れた場所で壊れます。cast のたびに、その場の比較
競合の検出どちらにもありません。2 つのスレッドが同じ場所を同時に書いて、壊します。同時アクセスを追います最も重い検査です。

不正な操作は、その瞬間にその場所で、理由を出して止まります。ゼロクリアだけは、止める検査ではなく初期化です。外すのは、全て自分で初期化している部品だけです。

なぜ、止まる方がよいのか

不正な操作の後も進むと、症状はその場所から離れて出ます。

型の取り違えが、その典型です。C++ の static_castvoid* からの変換は、実行時に型を検査しません。 実際の型が違っても検査なしに読み進み、別のメンバの値として、別の関数の呼び出しとして、離れた場所で壊れます。 どこで取り違えたかは、その時には判りません。

*C++ は、cast の場所で止めます。なぜ止まったかも出します。 実行時にしか判らない検査でも、入れる価値はここにあります。

テストモードでは全て有効にしておき、リリースモードでは求める速さに応じて外します。 同じソースのまま、ビルドの仕方を変えるだけです。

他の言語との違い

安全の検査外せるか
C#(.NET)常に効きます外せませんunsafe の中では検査の無いポインタを使えますが、検査を外す形ではありません。
Java(JVM)常に効きます外せません
Go常に効きます外せません境界だけはコンパイラのオプションで一括で外せます。
Swift常に効きますnull は型で排除し、境界は実行時に検査します。まとめてなら外せます-Ounchecked で一括です。1 つずつではありません。
Rustコンパイル時に排除します所有権と借用の検査です。境界だけは実行時に検査します。外す設定はありません検査の無い操作を unsafe で 1 か所ずつ許します。
Fil-C常に効きます外せませんその分、遅くなります。
*C++実行時に効きますnull・境界・ゼロクリア・型・競合の 5 つ。1 つずつ、部品(= クラスの単位)ごとに外せます

Rust の unsafe は、コンパイラが証明できないことを人が保証するための方法で、安全を細かく外す仕組みではありません。 *C++ の検査は、速くするために 1 つずつ外せます。 「選べる」ではなく「選び直せる」。これは効率性の側と同じです(= 効率性 のページ)。

※ リリースモードで外した検査は、不正な操作があっても止まりません。安全は、テストモードで通した範囲に依ります。