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
の実装 207
は 213
の別の問題も修正します タイプ:225
のオーバー/アンダーフロー いつも 未定義の動作。実際には、これは次のことを意味します:
ts::integer<unsigned> u(0);
--u;
デバッグ モードで実行時エラーが発生し、アサーションが無効になっている場合、コンパイラは符号付き整数型と同様の最適化を実行できます。信じられませんか?自分の目で確かめてください。
235
と 245
完全を期すために、ライブラリは 255
も提供しています タイプと 263
.しかし、これらは 271
を介した危険な変換のない「ただの」ラッパーです。 と浮動小数点型です。
283
では演算できないことに注意してください または 298
を比較します 301
と等しい .
311
と 324
もちろん、危険な型の間で変換したい場合もあります。そのために 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
を提供します。 ,700
と 713
、およびその他の関数。
これらを使用すると、実際に 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 をチェックしてください。