この質問は、ポインタの所有、消費ポインタ、スマートポインタ、ベクトル、アロケータに関するものです。
私はコードアーキテクチャについての私の考えに少し迷っています。さらに、この質問の回答がすでにどこかにある場合は、1。申し訳ありませんが、今のところ満足できる回答が見つかりません。2。指摘してください。
私の問題は次のとおりです:
私はいくつかの「もの」をベクターに格納し、それらの「消費者」をいくつか持っています。だから、私の最初の試みは次のようなものでした:
std::vector<thing> i_am_the_owner_of_things;
thing* get_thing_for_consumer() {
// some thing-selection logic
return &i_am_the_owner_of_things[5]; // 5 is just an example
}
...
// somewhere else in the code:
class consumer {
consumer() {
m_thing = get_thing_for_consumer();
}
thing* m_thing;
};
私のアプリケーションでは、「物事」はいずれにしても「消費者」よりも長生きするため、これは安全です。ただし、実行時にさらに「もの」を追加でき、std::vector<thing> i_am_the_owner_of_things;
が再割り当てされるとすべてのthing* m_thing
ポインターが無効になるため、これが問題になる可能性があります。
このシナリオの修正は、「もの」を直接ではなく、「もの」への一意のポインタを格納することです。つまり、次のようになります。
std::vector<std::unique_ptr<thing>> i_am_the_owner_of_things;
thing* get_thing_for_consumer() {
// some thing-selection logic
return i_am_the_owner_of_things[5].get(); // 5 is just an example
}
...
// somewhere else in the code:
class consumer {
consumer() {
m_thing = get_thing_for_consumer();
}
thing* m_thing;
};
ここでの欠点は、「もの」間のメモリの一貫性が失われることです。どういうわけかカスタムアロケーターを使用して、このメモリの一貫性を再確立できますか?たとえば、常に一度に10要素のメモリを割り当て、必要に応じて10要素サイズのメモリのチャンクを追加するアロケータのようなものを考えています。
例:
当初:
v =☐☐☐☐☐☐☐☐☐☐
その他の要素:
v =☐☐☐☐☐☐☐☐☐☐???? ☐☐☐☐☐☐☐☐☐☐
そしてまた:
v =☐☐☐☐☐☐☐☐☐☐???? ☐☐☐☐☐☐☐☐☐☐???? ☐☐☐☐☐☐☐☐☐☐
このようなアロケータを使用すると、std::unique_ptr
の再配置時に、既存の要素のメモリアドレスが変更されないため、std::vector
sの「もの」を使用する必要さえありません。
別の方法として、現在のstd::shared_ptr<thing> m_thing
ではなくthing* m_thing
を介して "consumer"の "thing"を参照することしか考えられませんが、これは私にとって最悪のアプローチのようです。 「コンシューマー」を所有してはならず、共有ポインターを使用して共有所有権を作成します。
それで、アロケータアプローチは良いものですか?もしそうなら、それをどのように行うことができますか?自分でアロケータを実装する必要がありますか、それとも既存のものですか?
thing
を値の型として扱うことができる場合は、そうしてください。それは物事を単純化します、あなたはポインター/参照無効化問題を回避するためのスマートポインターを必要としません。後者は別の方法で取り組むことができます:
thing
インスタンスがPush_front
およびPush_back
を介して挿入される場合は、std::deque
ではなくstd::vector
を使用してください。その後、このコンテナ内の要素へのポインタまたは参照は無効化されません(ただし、@ odyss-jiiが指摘してくれたことにより、イテレータは無効化されます)。 std::vector
の完全に連続したメモリレイアウトのパフォーマンス上の利点に大きく依存することが心配な場合は、ベンチマークとプロファイルを作成してください。thing
インスタンスがコンテナの中央に挿入される場合は、std::list
の使用を検討してください。コンテナー要素を挿入または削除するときに、ポインター/イテレーター/参照が無効になることはありません。 std::list
の反復はstd::vector
よりもはるかに低速ですが、あまり心配する前に、これが実際のシナリオの問題であることを確認してください。正確なアクセスパターンと必要なパフォーマンス特性に大きく依存するため、この質問に対する単一の正しい答えはありません。
そうは言っても、ここに私の推薦があります:
そのまま連続してデータを格納しますが、そのデータへのエイリアスポインターは格納しないでください。代わりに、使用する直前にIDに基づいてポインターをフェッチする、より安全な代替策(これは実証済みの方法です)を検討してください-補足として、マルチスレッドアプリケーションでは、基になるストアのサイズ変更の試行をロックできます。そのような弱い参照は生きています。
したがって、コンシューマーはIDを格納し、必要に応じて「ストア」からデータへのポインターをフェッチします。これにより、すべての「フェッチ」を制御できるため、それらを追跡したり、安全対策を実装したりできます。
void consumer::foo() {
thing *t = m_thing_store.get(m_thing_id);
if (t) {
// do something with t
}
}
または、マルチスレッドシナリオでの同期に役立つより高度な代替手段:
void consumer::foo() {
reference<thing> t = m_thing_store.get(m_thing_id);
if (!t.empty()) {
// do something with t
}
}
ここで、reference
は、スレッドセーフなRAIIの「弱いポインタ」です。
これを実装するには複数の方法があります。オープンアドレスのハッシュテーブルを使用し、IDをキーとして使用できます。これは、適切にバランスをとれば、おおよそO(1)アクセス時間になります。
別の代替手段(ベストケースO(1)、最悪ケースO(N))は、32ビットのIDと32ビットのインデックスを持つ「参照」構造を使用することです( 64ビットポインターと同じサイズ)-インデックスは一種のキャッシュとして機能します。フェッチするとき、インデックス内の要素に期待どおりのIDがある場合は、最初にインデックスを試します。それ以外の場合は、 「キャッシュミス」の場合、ストアの線形スキャンを実行してIDに基づいて要素を検索し、最後に既知のインデックス値を参照に格納します。