*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 つはその中に入ります。
4 つは Java や C# にもある検査と同じ意味で、競合の検出はどちらにも無く、*C++ が上回る所です。
| Java や C# では | 外すと、C++ で起きること | 効く場面 | |
|---|---|---|---|
| null 検査 | NullPointerException / NullReferenceException | null を辿って異常終了する場所が、判らなくなります。 | 参照のたびに 1 回の比較 |
| 境界検査 | ArrayIndexOutOfBoundsException / IndexOutOfRangeException | 隣のメモリを、検出されずに壊します。 | 添字のたびに 1 回の比較多次元は、次元の数だけ比較が増えます。 |
| ゼロクリア | フィールドは既定値(0)で初期化されます。 | 未初期化の値を読みます。 | 確保のたびに埋めます |
| 型の検査 | ClassCastException / InvalidCastException | 別の型として読み進み、離れた場所で壊れます。 | cast のたびに、その場の比較 |
| 競合の検出 | どちらにもありません。 | 2 つのスレッドが同じ場所を同時に書いて、壊します。 | 同時アクセスを追います最も重い検査です。 |
不正な操作は、その瞬間にその場所で、理由を出して止まります。ゼロクリアだけは、止める検査ではなく初期化です。外すのは、全て自分で初期化している部品だけです。
不正な操作の後も進むと、症状はその場所から離れて出ます。
型の取り違えが、その典型です。C++ の static_cast や void* からの変換は、実行時に型を検査しません。
実際の型が違っても検査なしに読み進み、別のメンバの値として、別の関数の呼び出しとして、離れた場所で壊れます。
どこで取り違えたかは、その時には判りません。
*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 つずつ外せます。
「選べる」ではなく「選び直せる」。これは効率性の側と同じです(= 効率性 のページ)。
※ リリースモードで外した検査は、不正な操作があっても止まりません。安全は、テストモードで通した範囲に依ります。