タイプ セーフ - よりタイプ セーフなゼロ オーバーヘッド ユーティリティ

2 週間前、エラーを防ぐために C++ の型システムを使用することについてブログを書きました。この投稿は多くの議論を引き起こしたので、私が得た回答のいくつかに対処したいと思いました。また、投稿の最後で、テクニックの実装に役立つライブラリを作成する予定でした.ライブラリは完成しました.type_safeはGithubにありますが、動機と機能の概要についての議論を読んでください.

ガイドライン II:適切な引数の型を使用する

前回の投稿のガイドライン II についてもう一度話しましょう。これはより重要なガイドラインであり、前回の投稿で少し説明を省略したためです。全体的な目標は、前提条件エラーを最小限に抑えることです。これを行う最も効率的な方法は、前提条件を最小限に抑えることです。 作るチャンスが少ない

これはしないことに注意してください 04 のように人為的に契約を広げることを意味します 18 の代わりに無効なインデックスの例外を処理します の UB です。これは単に、適切な引数 type を選択することを意味します -無効な値を表現できないもの。その場合、可能性のある前提条件エラーは 型エラー です コンパイラによってキャッチされます!

例を挙げました。次の関数があるとします:

/// \requires `ptr` must not be null.
void foo(int* ptr)
{
 assert(ptr);
}

22 前提条件があります - 37 を渡してはいけません .この前提条件は文書化されており、それを検証するためのアサーションがあります。

これが前提条件を伝える最良の方法だと言う人もいます。

いいえ、そうではありません。

前提条件を伝える最善の方法は、コード を使用することです .コメントが必要なコードは、明確であるがコメントを使用しないコードよりも定義上悪い.

この場合、問題に対する答えは簡単です:参照を使用してください。

void foo(int& ref);

参照を null にすることはできないため、前提条件を文書化する必要はありません。技術的には、null ポインターを逆参照することで null を渡すことができますが、それは呼び出し側の誤りです。さらに、null ポインターまたはそのためのポインターを誤って渡すことはできません。問題.コンパイラは参照がポインタではないことを訴えるので、呼び出し元はポインタを逆参照する必要があります.すべてのC++プログラマは、48を書くたびに自動的に考えるように訓練する必要があります - このポインターが null である可能性はありますか?確認する必要がありますか? 57 と書くだけでは発生しません。 .したがって、型を変更することで、前提条件を排除し、実行時バグの可能性とコンパイル時エラーを交換しました。

しかし、別の例を挙げました:

/// \requires `i >= 0`.
void foo(int i)
{
 assert(i >= 0);
}

こちら 60 の引数は負であってはなりません。したがって、同じガイドラインに従って、型を変更して、前提条件エラーが発生しないようにし、実行時にクラッシュする代わりにコンパイラがエラーを思い出させるようにする必要があります。

負でない整数を表す型は?正確には 70 :

void foo(unsigned i);

現在、負の値を渡すことはできず、そうするとコンパイラは文句を言います。

そうでない場合を除いて:

int i = 42;
foo(i); // works
i = -37;
foo(i); // works
foo(10); // works
foo(-10); // works

奇妙な理由で、誰かが黙ってするのは良い考えだと判断しました そして喜んで すべての整数を 83 に変換します

型エラーの可能性を防ぐ代わりに、バグが隠され、代わりに関数が巨大な値で呼び出されるようになりました。 107 を使用してはならないという Bjarne 自身 (!) によるガイドラインに導かれました。

ただし:壊れている場合は、修正 使用をやめて、存在しないふりをしないでください!

ありがたいことに、C++ は C の誤りを継承しただけでなく、修正する方法も提供してくれました。

それが私がしたことです。

119 - より良い整数型

ライブラリはクラス テンプレート 121 を提供します .これは、整数型 130 のラッパーです。 、しかしより良い。

単純な古い 146 の代わりにそれを使用しましょう :

void foo(ts::integer<unsigned> i);

よし、今使ってみよう:

int i = 42;
foo(i); // error, i is not unsigned
i = -37;
foo(i); // error, i is not unsigned
foo(10); // error, 10 is not unsigned
foo(-10); // error, -10 is not unsigned

foo(10u); // alright, 10u is unsigned
foo(ts::integer<unsigned>(-42)); // haha, nice try
foo(-ts::integer<unsigned>(37)); // of course not (unary minus doesn't exist for unsigned)

コンパイル エラーについて話していることに注意してください。 here.これが 159 の方法です すべき そもそも振る舞いなさい!

162 176 と同じ符号の整数のみを受け入れます サイズが 180 以下 .そして、「受け入れる」は単にコンストラクターを指すのではなく、すべてを指します:

ts::integer<int> a(0); // btw, no default constructor
ts::integer<long long> b(10);
ts::integer<unsigned> c(0u); // have to use "u" suffix

b += a; // alright
a += b; // no, possible lossy conversion

a + b; // alright, result is `ts::integer<long long>`

c += 42; // nope, 42 is not unsigned

a = -1;
if (a < c) // haha, nice try, you may not compare!

これらの「正気の」変換に加えて、199 の実装 207213 の別の問題も修正します タイプ:225 のオーバー/アンダーフロー いつも 未定義の動作。実際には、これは次のことを意味します:

ts::integer<unsigned> u(0);
--u;

デバッグ モードで実行時エラーが発生し、アサーションが無効になっている場合、コンパイラは符号付き整数型と同様の最適化を実行できます。信じられませんか?自分の目で確かめてください。

235245

完全を期すために、ライブラリは 255 も提供しています タイプと 263 .しかし、これらは 271 を介した危険な変換のない「ただの」ラッパーです。 と浮動小数点型です。

283 では演算できないことに注意してください または 298 を比較します 301 と等しい .

311324

もちろん、危険な型の間で変換したい場合もあります。そのために 335 があります。 :

ts::integer<short> i = ts::narrow_cast<short>(42);
ts::floating_point<float> f = ts::narrow_cast<float>(0.1);

バグを見つけましたか?

345 354 です リテラルなので、型安全な 368 に割り当てることはできません

しかし 371 は、IEEE-754 では損失なく表現できません。そのため、380 からの変換は 397 へ 精度が失われます。これは実行時にデバッグ モードでチェックされ、エラーが発生します。損失の可能性が本当に必要な場合は、さらに詳細にする必要があります。

ts::floating_point<float> f(static_cast<float>(0.1));

403 の場合 はリテラルではありません:

ts::floating_point<float> f(static_cast<float>(static_cast<double>(d)));

さて、それはたくさんのタイピングです!

414 に注意してください 428 間の変換はまだ許可されていません と 433 .そのためには、444 を使用する必要があります 関数:

ts::integer<unsigned> u(…);
ts::integer<int> i = ts::make_signed(u);
// likewise with make_unsigned()

ここでも、デバッグ モードで値がターゲット タイプに適合するかどうかをチェックします。457 もあります。 戻り値の型は対応する 469 です 473 .

485

ガイドラインに戻ります。

493 で s バグを隠すことなく安全に追跡できます。負の可能性のある値を渡そうとすると、もう一度コンパイラが通知し、考えさせられます。

ただし、組み込み型で表現できない型にはいくつかの制約があります。それらについては、500 があります。 :

using non_empty_string = ts::constrained_type<std::string, ts::constraints::non_empty>;

void foo(const non_empty_string& str);

516 520 のみを受け入れます それは空ではありません。この制約はコンパイル時に明らかにチェックできませんが、コンパイラは があることを知らせてくれます。 一部 制約:

foo("Hello world")); // error: const char* is not a non_empty_string
foo(std::string("Hello world")); // error: std::string is not a non_empty_string
foo(non_empty_string("Hello world")); // there ya go

型の不一致に関するコンパイル エラーの前と同様に、その制約が満たされているかどうかを考えるように促すことを願っています。満たされていない場合でも、心配する必要はありません。デバッグ アサーションが待っています。

530だから には制約があります。直接変更することはできません。545 があります。 関数ですが、551 を返します .変更するには、565 を使用する必要があります :

auto modifier = str.modify();
modifier.get() += "bar";
modifier.get().clear();
modifier.get() = "foo";
// destructor of modifier checks constraint again

ラムダが好きなら、 572 も使えます :

ts::with(str, [](std::string& s)
{
 …
});

583 は単純な述語ですが、静的チェックも実行できます。これは、GSL の 598 の単純な実装です。 :

using non_null_ptr = ts::constrained_type<int*, ts::constraints::non_null>;

non_null_ptr p(nullptr); // compilation error

一部の制約はチェックできないか、チェックするのにコストがかかりすぎます。そのために 603 があります :

using owning_ptr = ts::tagged_type<int*, ts::constraints::owner>;

615 は実際には述語ではなく、単なるタグ タイプです。これにより、Ben Deane がファントム タイプを呼び出す手法が可能になります。

ガイドライン I:適切な戻り値の型を使用する

前回の投稿では、624 についても不満を述べました。 .それを誤用することは非常に簡単で、誤って前提条件に違反します。

戻り値の型が単に 637 でない場合は、より良い解決策になると私は主張しました しかし 647 .その後、関数は常に何かを返すことができ、前提条件は必要ありません。

しかし、人々は私がそれで「やり過ぎた」と不平を言いました。また、人為的に契約を拡大したのです。契約を拡大したことには同意しますが、人為的ではない .I は単純に、値を返せないことがある関数に適切な戻り値の型を使用するだけです。前提条件はまだあります。651 という 1 つの中心的な場所に移動しただけです。 オプションの機能。

665 の使用 これは、実行時エラーよりもコンパイル時エラーを優先するための一般的な C++ ガイドラインに過ぎません。C++ はそれを行うためのツールを提供するので、それらを使用してください!

Scott Meyers は繰り返し言いました:インターフェイスを正しく使いやすく、間違って使いにくいものにします。これは間違って使いやすいです:

char back(const std::string& str);

これは間違った使い方をするのが難しくなります:

std::optional<char> back(const std::string& str);

簡単に呼び出すことができるため、誤って使用するのが難しくなります。 あまり考えずに関数を使用できますが、できません あまり考えずに関数の値に簡単にアクセスできます。

670 そして 689

type_safe はオプションも提供します。標準バージョンと非常に似ていますが、いくつかの違いがあります。たとえば、アクセス関数のようなポインターは提供しません。しかし、さらにモナディックであり、698 を提供します。 ,700713 、およびその他の関数。

これらを使用すると、実際に 726 を呼び出す必要はありません オプションの機能であり、その前提条件には実行されません。たとえば、 730 のように 748 を提供します オプションが空の場合、値またはフォールバック値を返す関数。ただし、 750 もあります 関数:

ts::optional<int> opt = …;
ts::optional<char> mapped = opt.map([](int i) { return 'A' + i; });

764 の場合 空です、773 も空です。それ以外の場合は 789 文字 799 を含む .より効率的な 806 コピーを返さないものは 810 です :

ts::optional<int> opt = …;
ts::with(opt, [](int& i) { ++i; });

左辺値参照を取得し、コピーを返す代わりにオプションの値をその場で変更できるようにします。 821 で使用する関数 832 を返す 自分自身:

ts::optional<int> opt = …;
ts::optional<ts::optional<char>> mapped = opt.map([](int i) { return i > 26 ? ts::nullopt : 'A' + i; });
// a nested optional isn't nice but there's unwrap():
ts::optional<char> map_unwrap = mapped.unwrap();

840 ネストされたオプションのラップを解除します。外側のものが空の場合、結果も空ですが、ネストされた型になります。それ以外の場合は 856 です メンバー関数 868 878 と同等です .

888 関数は 899 を提供します .903 を呼び出します variant.A 914 に格納されているタイプ オプションも存在するため、926 の一般化です。 また、値が保存されていない場合は関数を呼び出し、932 を渡します .

944 もあります オプションの参照をモデル化します。基本的にはポインタのように動作します - 954 を割り当てることもできます 967 に加えて 空の状態を作成しますが、978 と同じインターフェースを持っています 同じ関数を使用できます。988 また、null の可能性がある参照が必要な引数にも役立ちます。ポインターは適切なモデリングの選択ではない可能性があります。

997 の他のすべてと同様に 実行時のオーバーヘッドはありません。

結論

C++ の型システムは驚くべきものです。組み込み型については驚くべきことではありません。しかし、ありがたいことに、それを修正する機能が提供されています。

私が示したテクニックは、C++ を Java のようにワイド コントラクトとあらゆる場所で例外を作成するものではありません。代わりに、ランタイム を作成します。 エラー 種類 Haskell のようなエラー言語が行います。適切な型の設計により、エラーのクラス全体を完全に取り除くことができます。もちろん、エラーはまだ発生する可能性がありますが、発生する可能性があるのは だけです。 プログラマーはコンパイラーによって思い出され、その可能性は低くなります。

さらに、十分にスマートなコンパイラ、つまり 1002 を使用した新しい GCC を考えると、 - オーバーヘッドがゼロまたは負の場合もあります。一部の手法は劇的で、奇妙に思えるかもしれません。しかし、これは、低レベルの C または C++ コードが通常書かれている方法ではないためです。これは、より「最新」の方法です。機能的パラダイムを使用して考えるのです。試してみたい場合は、type_safe をチェックしてください。