ポリシー ベースの設計の問題

ポリシーベースの設計は、ライブラリの作成者がユーザーに柔軟性を提供する優れた方法です。特定の動作をハードコーディングする代わりに、ポリシーベースの設計はさまざまな ポリシー を提供します。 ユーザーは動作をカスタマイズすることを選択できます。適切に行われれば、ライブラリの作成者は単一の実装ですべてのユース ケースに対応できます。

そのため、私はポリシーベースの設計の大ファンです。トレードオフの可能性があり、複数のソリューションが可能であり、それぞれに独自の長所と短所がある場合はいつでも、ユーザーが決定できるようにします。代わりに特定のユースケースを好む場合、私はそれらすべてを好みます.これは、たとえば、バリアント実装で行ったことです.

ただし、ポリシーベースの設計は完璧ではありません。特に、これには大きな問題があります。さまざまな互換性のないタイプが大量に作成されます。

ポリシーベースの設計の概要

ポリシーベースの設計の最も一般的な例は、おそらく STL コンテナーのアロケーターです。Take 07 たとえば、要素の動的配列です。動的であるため、何らかの方法でメモリを割り当てる必要があります。しかし、メモリを割り当てるには多くの戦略があり、それぞれが特定の状況に最適です。メモリ割り当てがハードコードされている場合、 16 さまざまなパフォーマンスが重要なアプリケーションには使用できません。

幸いなことに、これはハードコーディングされていません。代わりに、24 という割り当てポリシーがあります。 - メモリの割り当て方法を制御します。

38 要素タイプ以外に 2 番目のテンプレート パラメータがあります。これは割り当てポリシーです。特定のメンバー関数を使用して独自のクラスを定義し、それをプラグインできます。次に 46 メモリを割り当てる方法を使用します。

ポリシーベースの設計のほとんどの場合、ほとんどの場合に問題のないポリシーの実装があります.それは 54 の場合です. 同様に.Using 67 メモリ割り当ては一般的なケースでは十分です。そのため、79 new を使用 - 86 - はデフォルトのポリシーです。他の 96 がない場合に使用されます。

したがって、通常のユーザーは 103 を使用できます アロケータについて心配する必要はありません。完全な制御を望む専門家だけがそれを気にする必要があります。それがポリシーベースの設計の美しさです。

ポリシーベースの設計の問題

ポリシーにテンプレート パラメーターを使用することは、ポリシー ベースの設計を実装する最も一般的な方法です。理由は簡単です。テンプレートはゼロ コストの抽象化であり、テンプレートの使用に関連するランタイム コストはありません。

Alexandrescu が言ったように、クラスはさまざまな実装のコード ジェネレーターになります。

ただし、テンプレートのインスタンス化が異なれば、も異なります .あなたの 110 126 とは異なる型です 両方とも 130 の動的配列ですが、

これは、142 を返す関数がある場合、 155 を取るもの 互換性がありません。さまざまなベクター型を変換する必要があり、コストがかかります。

これは、語彙の種類にとって特に大きな問題です。 - 状況を表す事実上の方法であることが意図されている型。 162 を取る たとえば、存在しない可能性のあるオブジェクトを表す事実上の方法であることが意図されています。または 175 - 型の結合を表します。

語彙型は API の構築に不可欠であり、そこで非常に役立ちます。

しかし、API 設計におけるボキャブラリー タイプのルールを考えると、異なるタイプの問題に遭遇しないことが最も重要です!プロジェクトに異なるバリアント実装がある場合、API は互換性がありません。

これは、ポリシーごとに異なるタイプがあるため、そこでポリシーベースの設計を使用するのが難しいことを意味します.

そのため、ポリシーベースの設計にはさまざまなタイプの作成が必要になることが多く、API の非互換性につながる可能性があります。これを回避したい場合は、あらゆる場所でテンプレートを使用する必要があります。

しかし、問題だけを話すのではなく、解決策を提示したい .では、どうすれば問題を解決できるでしょうか?

解決策 0:ポリシー ベースの設計を使用しない

最も明白な解決策は単純です。ポリシーベースの設計を使用しないことです。これは非常に強力ですが、強力なものは使いすぎになる傾向があります。

私の182を取ってください たとえば、これは実際には 193 です 空の状態を許可するかどうか、および移動コンストラクターがスローした場合に何が起こるかを制御するポリシーを使用します。これは私の 208 に対する大きな批判でした 、語彙タイプであるため。

そして後から考えると、私はおそらくそれでやり過ぎました:216 を提供するべきでした と 225 234 の場合 バリアントと空の状態のようなものです。これら 2 つは 248 のように実質的に異なる型であるため、問題はありません。 と 257 .

したがって、ポリシーベースの設計を実装する場合はいつでも、それが本当に価値があるかどうかを考えてください。自問してください:カスタマイズは本当にそれほど重要ですか? 99% のユーザーにとって十分な優れた一般的なソリューションはありますか? そして最も重要なこと:ポリシーはクラスの基本的な動作を変更しますか?

特定のポリシーを持つクラスに新しい名前を付けることができれば、これはそのポリシーが動作の根本的な変更であること、または実際にはポリシーに基づく設計ではなく単なる「コードの重複を防ぎたい」ということを示す良いヒントになります。後者ケースは問題ありませんが、「ポリシー」を非表示にして、2 つのクラスを共通のインターフェースを共有する別々の型として文書化することを検討してください。

解決策 1:type-erasure を使用する

ポリシーベースの設計の問題に対する最も一般的な解決策は、型消去です。

たとえば、標準ライブラリのスマート ポインターを考えてみましょう。261 276 があります - オブジェクトの解放方法を制御するポリシー。これは別のテンプレート引数であるため、別の型を作成します。

しかし 280 291 がありません オブジェクトを解放する方法を定義するポリシーを渡すこともできますが、テンプレート引数を使用することはできません。これが可能になるのは、実装で type-erasure が使用されているためです。 301 を静的に格納する代わりに ,313 タイプ消去して保存し、動的メモリ割り当てと 324 で隠します 関数またはコールバック。

そして、これが type-erasure を使用することの欠点です:通常、テンプレート引数バージョンよりもコストがかかります.

標準ライブラリには、ポリシーに type-erasure を使用する適切なガイドラインがあります:何らかの形式の間接呼び出しが既に行われている場合は、type-erasure を使用してください。336 すでにヒープに制御ブロックがあり、そこにポリシーを簡単に格納することもできます。

しかし、それ以外の場合、type-erasure のオーバーヘッドは無視できます。たとえば、読み取り元のポリシーを持つ入力ストリームは、type-erasure を簡単に使用できます。ファイルからデータを読み取るオーバーヘッドは、間接関数に比べてはるかに大きくなります。

ポリシーベースの設計が不可欠で、状況によっては型消去がオーバーヘッドを大きくしすぎるようなものがある場合は、ポリシーベースの設計自体を使用して問題を解決することもできます!型消去を使用して転送するポリシーを定義するだけです。すべての API でタイプ消去ポリシーを使用します。

それが私の新しい 341 です メモリのモデルが使用している:デフォルトでは型消去を使用しませんが、351 があります any への参照を格納できます アロケータ。363 を使用できます 375 を持つエイリアス 型を変更せずに任意のアロケータを使用できます。

使用できる型消去の別の形式もあります。 384 を取る仮想関数を考えてみましょう。 繰り返しますが、関数が実際にコンテナーを変更する必要がない場合は、その上を歩いてください。私の 399 のようなものを使用できます .これは、任意の連続したメモリ ブロックへの参照です。次に、関数は連続するものすべてを受け入れることができるため、402 も 、つまり別のポリシーです。

解決策 2:ポリシーを自動的に適用する

私の type_safe でのオプションの実装も、ポリシーベースの設計を使用しています。416 があります。 保存ポリシーを受け入れます。このポリシーは、オプションの値が無効な場合などにどのように保存されるかを制御します。

もともと私は両方の 420 を簡単に実装するためにそれをしました - 「通常の」オプション タイプ - および 433 - ファンシー ポインター - コードの重複なし。これは 442 としては問題ありません はオプショナル型の語彙型で、450 タイプへのオプションの参照用。

ただし、コンパクトなオプション機能も実装しました。

しかし、誰かが 466 を使うかもしれません API で、他の誰かが通常の 478 を受け入れます 、ポリシーベースの設計の問題につながります.しかし、利用可能な解決策があります.

本当に必要なのは 488 型のオプションです .そして、そのオプションはさまざまな方法で実装される可能性があります.たとえば、 498 の場合 は参照です。509 を使用してください 、516の場合 525 です いくつかのコンパクトなオプションを使用し、それ以外の場合はデフォルトのものを使用してください。

API が特定のタイプに対して常に「適切な」ポリシーを使用する場合、問題は発生しません。適切なポリシーの選択は自動化できます。type_safe には 536 があります 、オプションのストレージポリシーをオーバーライドするために独自のタイプに特化できる特性.Then 542 その特性を使用して、型に最適なオプションの実装を選択します。

一般に:他のテンプレート パラメータに大きく依存するポリシーがある場合は、ポリシー選択プロセスを自動化することを検討してください。 オブジェクトは、指定された 564 に対して同じポリシーを使用します .このように、概念的に同じ型は実際には同じ型です。

解決策 3:テンプレートを使用しますか?

理想的な解決策は、単純にテンプレートを使用することです。ポリシーベースの設計でクラスを使用する場合はどこでも使用します。たとえば、577 とは決して記述しないでください。 しかし 588 、可能なすべてのポリシーをキャッチできます。

しかし、テンプレートを使用することには、すべてをヘッダー ファイルに含める必要がある、またはコードが肥大化するなどの技術的な欠点があります。いつか C++ にモジュール システムと優れたコンパイラが導入され、問題がなくなるかもしれません。

結論

これは、コードや一般的なアドバイスのない、かなり抽象的なブログ投稿でした。問題に対する優れた解決策を提示したいと思っていますが、存在しないため (私は知っています)、単にできません。

私ができる唯一の一般的なアドバイスは次のとおりです。

    <リ>

    本当に価値がある場合、または異なるポリシーを持つタイプがめったに混在しない場合にのみ、ポリシーベースの設計を使用してください。コードベース全体で 1 つのポリシーのみを使用する場合、問題はありません。

    <リ>

    ポリシーを非表示にするために、何らかの形式の (オプションの) タイプ消去を追加することを検討してください。

    <リ>

    何も混在しないように、特定のポリシーを自動的に適用することを検討してください。

ポリシー ベースの設計は優れており、ライブラリがより一般的になります。しかし残念ながら、これには避けられない問題もあります。