web-dev-qa-db-ja.com

Rust C ++の所有権パラダイムを有効にする方法

システムプログラミング言語Rustは、所有権パラダイムを使用して、コンパイル時に、リソースを解放する必要があるランタイムのコストをゼロにすることを保証します( "Rust Book on Ownership" を参照)。 =)。

C++では、通常、スマートポインターを使用して、リソース割り当ての管理の複雑さを隠すという同じ目標を達成します。ただし、いくつかの違いがあります。

  • Rustでは、所有者は常に1人だけですが、C++ shared_ptrは、所有権を簡単に漏らす可能性があります。
  • Rustでは、所有していない参照を借用できますが、C++のunique_ptrはweak_ptrとlock()を介して安全な方法で共有できません。
  • Shared_ptrの参照カウントにはコストがかかります。

私の質問は、次の制約内でC++の所有権パラダイムをどのようにエミュレートできるかです。

  • 常に1人の所有者のみ
  • リソースがスコープ外になることを恐れずに、ポインターを借用して一時的に使用する可能性( observer_ptr はこれには役に立ちません)
  • 可能な限り多くのコンパイル時チェック。

編集:これまでのコメントから、次のように結論付けることができます。

  • コンパイラでは、これに対するコンパイル時のサポートはありません(私が知らないdecltype/templateの魔法を期待していました)。他の場所で静的分析を使用して可能かもしれません(汚染?)
  • 参照カウントなしでこれを取得する方法はありません。
  • 所有または借用のセマンティクスを持つshared_ptrsを区別するための標準的な実装はありません
  • Shared_ptrとweak_ptrの周りにラッパータイプを作成することで、独自のロールを作成できます。

    • owned_ptr:コピー不可、移動セマンティクス、shared_ptrのカプセル化、borrowed_ptrへのアクセス
    • borrowed_ptr:コピー可能、weak_ptrをカプセル化、ロックメソッド
    • locked_ptr:コピー不可、移動セマンティクス、weak_ptrのロックからshared_ptrをカプセル化します
26

コンパイル時のチェックではこれを行うことはできません。 C++型システムには、オブジェクトがスコープ外になる、移動される、または破棄される時期を推論する方法がありません。ましてや、これを型制約に変えることはできません。

実行時にアクティブな「借用」の数のカウンターを保持する_unique_ptr_のバリアントを使用することができます。 get()が生のポインターを返す代わりに、構築時にこのカウンターをインクリメントし、破棄時にデクリメントするスマートポインターを返します。カウントがゼロ以外のときに_unique_ptr_が破棄された場合、少なくともどこかで誰かが何か間違ったことをしたことがわかります。

ただし、これは絶対確実なソリューションではありません。それを防ぐためにどれだけ懸命に努力しても、基になるオブジェクトへの生のポインターを取得する方法は常にあり、その生のポインターはスマートポインターと_unique_ptr_よりも簡単に長持ちする可能性があるためゲームオーバーです。生のポインターを必要とするAPIと対話するために、生のポインターを取得する必要がある場合もあります。

さらに、所有権はポインタに関するものではありませんBox/_unique_ptr_を使用すると、オブジェクトをヒープに割り当てることができますが、同じオブジェクトをスタック(または別のオブジェクト内、またはその他の場所)に配置する場合と比較して、所有権や存続期間などについては何も変わりません。本当に)。 C++のこのようなシステムから同じマイレージを得るには、_unique_ptr_ sだけでなく、あらゆる場所のすべてのオブジェクトに対してこのような「借用カウント」ラッパーを作成する必要があります。そして、それはかなり非現実的です。

それでは、コンパイル時オプションをもう一度見てみましょう。 C++コンパイラは私たちを助けることはできませんが、おそらくリントは助けることができますか?理論的には、型システムの全ライフタイム部分を実装し、(独自のコードに加えて)使用するすべてのAPIにアノテーションを追加すると、機能する可能性があります。

ただし、プログラム全体で使用されるすべての関数に注釈が必要です。サードパーティライブラリのプライベートヘルパー機能を含みます。そして、ソースコードが利用できないもの。また、実装が複雑すぎてリンターが理解できない場合(Rustの経験から)、何かが安全である理由が微妙すぎて、ライフタイムの静的モデルで表現できない場合があります。コンパイラを支援するために少し異なる方法で記述されています)最後の2つでは、リンターはアノテーションが実際に正しいことを確認できないため、プログラマーを信頼することに戻ります。さらに、一部のAPI(つまり、APIの条件) Rustが使用するため、ライフタイムシステムでは実際にはうまく表現できません。

言い換えれば、これのための完全で実用的に有用なリンターは、失敗のリスクを伴う実質的な独自の研究になるでしょう。

たぶん、20%のコストで80%の利益を得る中間点があるかもしれませんが、あなたは厳しい保証が欲しいので(そして正直なところ、私もそれを望んでいます)、頑張ってください。 C++の既存の「グッドプラクティス」は、コンパイラの支援なしで、a Rustプログラマーのやり方を本質的に考えて(そして文書化して))、リスクを最小限に抑えるのにすでに大いに役立ちます。 C++の状態とそのエコシステムを考慮すると、それよりも大幅に改善されているかどうか。

tl; dr使用するだけRust ;-)

26
user395760

いくつかの厳密なコーディング規則を適用することで、Rustの利点のsomeを得ることができると思います(結局のところ、方法がないので、とにかくやらなければならないことです) 「テンプレートマジック」を使用して、コンパイラにnotコードをコンパイルするように指示します。notは「マジック」を使用します)。 .well ...kind of close、ただしシングルスレッドアプリケーションの場合のみ:

  • newを直接使用しないでください。代わりに、make_uniqueを使用してください。これは、ヒープに割り当てられたオブジェクトがRustのような方法で「所有」されるようにするための途中です。
  • 「借用」は、常に関数呼び出しへの参照パラメーターを介して表す必要があります。参照を受け取る関数は、参照されるオブジェクトへのあらゆる種類のポインタを決して作成する必要があります。 (場合によっては、参照の代わりに生のポインターをパラメーターとして使用する必要がありますが、同じルールを適用する必要があります。)
    • これは、ヒープ上のスタックまたは上のオブジェクトに対して機能することに注意してください。関数は気にしないでください。
  • 所有権のTransferは、もちろん、R値参照(&&)および/またはunique_ptrsへのR値参照によって表されます。

残念ながら、他の現存する参照がnoある場合にのみ、可変参照がシステム内のどこにでも存在できるというRustのルールを適用する方法は考えられません。

また、あらゆる種類の並列処理では、ライフタイムの処理を開始する必要があります。クロススレッドライフタイム管理(または共有メモリを使用したクロスプロセスライフタイム管理)を許可する唯一の方法は、独自の「 ptr-with-lifetime "ラッパー。ここでは、参照カウントが実際に重要になるため、これはshared_ptrを使用して実装できます。ただし、参照カウントブロックには実際には2参照カウンターがあるため(1つはオブジェクトを指すすべてのshared_ptrsに、もう1つはすべてのweak_ptrsに)、それでも少し不必要なオーバーヘッドがあります。また、少しです... oddshared_ptrシナリオでは、everybodyshared_ptrの所有権は「等しい」のに対し、「生涯借りる」シナリオでは、スレッドは1つだけです。/processは、実際にはメモリを「所有」する必要があります。

2
Kyle Strand

unique_ptrの拡張バージョン(一意の所有者を強制するため)とobserver_ptrの拡張バージョン(ダングリングポインターのNiceランタイム例外を取得するため、つまりunique_ptrを介して維持された元のオブジェクトがスコープ外になった場合)を使用できます。 Trilinos パッケージは、この拡張されたobserver_ptrを実装し、Ptrと呼びます。ここにunique_ptrの拡張バージョンを実装しました(私はそれをUniquePtrと呼びます): https://github.com/certik/trilinos/pull/1

最後に、オブジェクトをスタックに割り当てたいが、安全な参照を渡すことができる場合は、Viewableクラスを使用する必要があります。ここで私の最初の実装を参照してください: https:// github .com/certik/trilinos/pull/2

これにより、ポインタにRustと同じように、C++を使用できるようになります。ただし、Rustではコンパイル時エラーが発生し、C++では実行時例外が発生します。また、デバッグモードではランタイム例外のみが発生することに注意してください。リリースモードでは、クラスはこれらのチェックを行わないため、Rust(基本的に)と同じくらい高速です。生のポインタと同じくらい高速ですが、セグフォールトする可能性があります。したがって、テストスイート全体がデバッグモードで実行されていることを確認する必要があります。

0