C ++:スマートポインター、生のポインター、ポインターなし?[閉まっている]


48

C ++でのゲーム開発の範囲内で、ポインターの使用に関して望ましいパターンは何ですか(なし、生、スコープ付き、共有、またはスマートとダムの間)。

あなたが考慮するかもしれません

  • オブジェクト所有権
  • 使いやすさ
  • コピーポリシー
  • オーバーヘッド
  • 循環参照
  • ターゲットプラットフォーム
  • コンテナで使用

回答:


32

さまざまなアプローチを試みた後、今日私はGoogle C ++スタイルガイドと調和していることに気付きました。

実際にポインターのセマンティクスが必要な場合、scoped_ptrは素晴らしいです。std :: tr1 :: shared_ptrは、オブジェクトをSTLコンテナで保持する必要がある場合など、非常に特殊な条件下でのみ使用してください。auto_ptrを使用しないでください。[...]

一般的に、明確なオブジェクト所有権を使用してコードを設計することを好みます。オブジェクトをフィールドまたはローカル変数として直接使用することで、ポインターをまったく使用せずに、オブジェクトの所有権が最も明確になります。[..]

推奨されていませんが、参照カウントポインターは、問題を解決するための最も簡単で最も洗練された方法である場合があります。


14
今日、scoped_ptrの代わりにstd :: unique_ptrを使用したい場合があります。
クライム

24

また、私は「強い所有権」の思考の流れに従います。必要に応じて、「このクラスがこのメンバーを所有している」ことを明確に描写したいと思います。

私はめったに使用しませんshared_ptr。使用する場合はweak_ptr、参照カウントを増やすのではなく、オブジェクトへのハンドルのように扱うことができるように、可能な限り自由に使用します。

私はあちこちで使っていscoped_ptrます。明らかな所有権を示しています。そのようなオブジェクトをメンバーにしないのは、scoped_ptrにあるオブジェクトを前方宣言できるからです。

オブジェクトのリストが必要な場合は、を使用しますptr_vector。を使用するよりも効率的で副作用が少ないvector<shared_ptr>です。あなたはptr_vectorで型を前方宣言できないかもしれません(しばらくの間)が、その意味論は私の意見では価値があります。基本的に、リストからオブジェクトを削除すると、自動的に削除されます。これは明らかな所有権も示しています。

何かへの参照が必要な場合は、ネイキッドポインターではなく参照にしようとします。これは実用的でない場合があります(つまり、オブジェクトの構築後に参照が必要な場合)。いずれにせよ、参照は明らかにあなたがオブジェクトを所有していないことを示しており、他の場所で共有ポインタのセマンティクスを使用している場合、通常、裸のポインタは追加の混乱を引き起こしません(特に「手動削​​除なし」ルールに従う場合) 。

この方法で、私が取り組んだiPhoneゲームの1つは1回しかdelete呼び出すことができず、それは私が書いたObj-CからC ++へのブリッジにありました。

一般的に、記憶管理は人間に任せるには重要すぎると私は考えています。削除を自動化できる場合は、そうする必要があります。shared_ptrのオーバーヘッドが実行時に高すぎる場合(スレッドのサポートをオフにしたなど)、おそらく他の何か(バケットパターンなど)を使用して動的割り当てを下げる必要があります。


1
素晴らしい要約。smart_ptrについて言及するのではなく、実際にはshared_ptrを意味しますか?
jmp97

はい、shared_ptrを意味しました。それを修正します。
テトラッド

10

ジョブに適したツールを使用します。

プログラムで例外をスローできる場合は、コードが例外に対応していることを確認してください。スマートポインター、RAIIの使用、および2フェーズ構成の回避は、出発点として適切です。

明確な所有権セマンティクスのない循環参照がある場合は、ガベージコレクションライブラリの使用またはデザインのリファクタリングを検討できます。

優れたライブラリを使用すると、タイプではなくコンセプトにコーディングできるため、ほとんどの場合、リソース管理の問題を超えてどの種類のポインターを使用してもかまいません。

マルチスレッド環境で作業している場合、オブジェクトがスレッド間で共有される可能性があるかどうかを必ず確認してください。boost :: shared_ptrまたはstd :: tr1 :: shared_ptrの使用を検討する主な理由の1つは、スレッドセーフな参照カウントを使用するためです。

参照カウントの個別の割り当てが心配な場合は、これを回避する方法がたくさんあります。boost :: shared_ptrライブラリを使用して、参照カウンターをプールするか、boost :: make_shared(my preference)を使用して、オブジェクトと参照カウントを単一の割り当てで割り当てることで、キャッシュミスに関するほとんどの懸念を軽減できます。最上位レベルでオブジェクトへの参照を保持し、オブジェクトへの直接参照を渡すことにより、パフォーマンスが重要なコードの参照カウントを更新することによるパフォーマンスの低下を回避できます。

共有所有権が必要であるが、参照カウントまたはガベージコレクションのコストを支払いたくない場合は、不変オブジェクトまたはコピーオンライトイディオムの使用を検討してください。

パフォーマンスの最大の勝利は、アーキテクチャレベルであり、その後にアルゴリズムレベルが続くことを念頭に置いてください。これらの低レベルの懸念は非常に重要ですが、主要な問題に対処した後にのみ対処する必要があります。キャッシュミスのレベルでパフォーマンスの問題を処理している場合は、多くの問題が発生します。これらの問題は、たとえば、ポインタごとに関係のない偽共有のように注意する必要があります。

テクスチャやモデルなどのリソースを共有するためだけにスマートポインターを使用している場合は、Boost.Flyweightなどのより特殊なライブラリを検討してください。

新しい標準が採用されると、移動セマンティクス、右辺値参照、および完全な転送により、高価なオブジェクトおよびコンテナの操作がはるかに簡単かつ効率的になります。それまでは、auto_ptrやunique_ptrなどの破壊的なコピーセマンティクスを持つポインターをコンテナー(標準の概念)に保存しないでください。Boost.Pointer Containerライブラリを使用するか、共有所有権スマートポインターをContainersに保存することを検討してください。パフォーマンスが重要なコードでは、Boost.Intrusiveのような侵入型コンテナーを優先して、これらの両方を回避することを検討できます。

ターゲットプラットフォームが決定にあまり影響を与えてはいけません。組み込みデバイス、スマートフォン、ダム電話、PC、およびコンソールはすべて、コードを問題なく実行できます。厳密なメモリバジェットやロード前後の動的割り当てがないなどのプロジェクト要件は、より有効な懸念事項であり、選択に影響するはずです。


3
コンソールでの例外処理は少し危険な場合があります。特にXDKは一種の例外敵対的です。
Crashworks

1
ターゲットプラットフォームは、実際に設計に影響するはずです。データを変換するハードウェアは、ソースコードに大きな影響を与える場合があります。PS3アーキテクチャは、ハードウェアを使用してリソースとメモリ管理、およびレンダラーの設計に本当に必要な具体例です。
サイモン

特にGCに関しては、私はわずかに同意しません。ほとんどの場合、循環参照は参照カウント方式の問題ではありません。一般に、これらの循環的な所有権の問題は、人々がオブジェクトの所有権について適切に考えなかったために生じます。オブジェクトが何かを指す必要があるからといって、オブジェクトがそのポインターを所有する必要があるという意味ではありません。一般的に引用されている例は、ツリー内のバックポインターですが、ツリー内のポインターの親は、安全性を犠牲にすることなく、安全に生のポインターにすることができます。
ティムセギーン

4

C ++ 0xを使用している場合は、を使用しますstd::unique_ptr<T>

std::shared_ptr<T>参照カウントのオーバーヘッドとは異なり、パフォーマンスのオーバーヘッドはありません。unique_ptr そのポインターを所有しており、C ++ 0xのmoveセマンティクスを使用して所有権を転送できます。コピーすることはできません-移動するだけです。

また、コンテナ内で使用することもできます。たとえばstd::vector<std::unique_ptr<T>>、バイナリ互換であり、パフォーマンスと同じですが、std::vector<T*>要素を消去したりベクトルをクリアしてもメモリリークは発生しません。これは、STLアルゴリズムとの互換性も優れていますptr_vector

多くの目的のためのIMOは、これが理想的なコンテナです。ランダムアクセス、例外セーフ、メモリリークの防止、ベクトルの再割り当てのオーバーヘッドの削減(舞台裏でポインタをシャッフルするだけ)。多くの目的に非常に役立ちます。


3

どのクラスがどのポインターを所有しているかを文書化することをお勧めします。できれば、通常のオブジェクトのみを使用し、ポインタは使用しないようにしてください。

ただし、リソースを追跡する必要がある場合は、ポインターを渡すことが唯一のオプションです。いくつかのケースがあります:

  • 他の場所からポインタを取得しますが、管理はしません。通常のポインタを使用してドキュメントを作成し、削除しようとした後にコーダーがいないようにします。
  • 他の場所からポインタを取得し、それを追跡します:scoped_ptrを使用します。
  • 他の場所からポインタを取得し、それを追跡しますが、それを削除するには特別なメソッドが必要です:カスタム削除メソッドでshared_ptrを使用します。
  • STLコンテナーにポインターが必要です。ポインターはコピーされるため、boost :: shared_ptrが必要です。
  • 多くのクラスがポインターを共有しており、誰がそれを削除するかは明確ではありません:shared_ptr(上記のケースは実際にはこのポイントの特別なケースです)。
  • 自分でポインタを作成し、必要なのはそれだけです。実際に通常のオブジェクトを使用できない場合は、scoped_ptrを使用します。
  • ポインターを作成し、他のクラスshared_ptrと共有します。
  • ポインターを作成して渡します。通常のポインターを使用してインターフェースを文書化し、新しい所有者が自分でリソースを管理する必要があることを認識できるようにします。

私は今、自分のリソースをどのように管理するかをほぼカバーしていると思います。shared_ptrのようなポインターのメモリコストは、通常、通常のポインターのメモリコストの2倍です。このオーバーヘッドが大きすぎるとは思いませんが、リソースが少ない場合は、スマートポインターの数を減らすようにゲームを設計することを検討してください。他のケースでは、上記の箇条書きのような良い原則に合わせて設計するだけで、プロファイラーはどこにもっとスピードが必要かを教えてくれます。


1

ブーストのポインターに関しては、その実装が必要なものと正確に一致しない限り、避けるべきだと思います。彼らは、誰もが最初に期待するよりも大きいコストで来ます。これらは、メモリおよびリソース管理の重要で重要な部分をスキップできるインターフェイスを提供します。

ソフトウェア開発に関しては、あなたのデータについて考えることが重要だと思います。メモリ内でデータがどのように表されるかは非常に重要です。これは、CPUアクセス速度がメモリアクセス時間よりもずっと速い速度で増加しているためです。これにより、多くの場合、メモリキャッシュが最新のコンピューターゲームの主なボトルネックになります。アクセス順序に従ってメモリ内でデータを線形に整列させることにより、キャッシュがより使いやすくなります。この種のソリューションは、多くの場合、よりクリーンなデザイン、よりシンプルなコード、より明確にデバッグしやすいコードにつながります。スマートポインターを使用すると、リソースの頻繁な動的メモリ割り当てが容易に発生するため、メモリ全体に散在します。

これは時期尚早な最適化ではなく、可能な限り早期に行うことができる、また行うべき健全な決定です。これは、ソフトウェアが実行されるハードウェアのアーキテクチャー理解の問題であり、重要です。

編集:共有ポインターのパフォーマンスに関して考慮すべきことがいくつかあります。

  • 参照カウンターはヒープに割り当てられます。
  • スレッドセーフを有効にした場合、参照カウントはインターロックされた操作を介して行われます。
  • 値でポインタを渡すと、参照カウントが変更されます。これは、メモリ内でランダムアクセスを使用する可能性が最も高いインターロック操作(ロック+キャッシュミスの可能性)を意味します。

2
あなたは「どんな犠牲を払っても避けられた」で私を失いました。次に、実際のゲームではめったに気にしないタイプの最適化について説明します。ほとんどのゲーム開発は、CPUキャッシュパフォーマンスの不足ではなく、開発の問題(遅延、バグ、プレイアビリティなど)によって特徴付けられます。したがって、このアドバイスは時期尚早な最適化ではないという考えには強く反対します。
kevin42

2
データレイアウトの初期設計に同意する必要があります。最新のコンソール/モバイルデバイスのパフォーマンスを引き出すことは重要であり、見落とされることはありません。
オリー

1
これは、私が働いているAAAスタジオの1つで見た問題です。また、Insomniac Gamesのヘッドアーキテクト、マイクアクトンを聴くこともできます。ブーストが悪いライブラリだと言っているのではなく、高性能なゲームに適しているだけではありません。
サイモン

1
@ kevin42:キャッシュの一貫性は、おそらく今日のゲーム開発における低レベルの最適化の主な原因です。@Simon:ほとんどのshared_ptr実装は、LinuxおよびWindows PCを含む比較とスワップをサポートするプラットフォームでのロックを回避します。Xboxを含むと思います。

1
@Joe Wreschnig:それは事実です。共有ポインターの初期化(コピー、弱いポインターからの作成など)を引き起こす可能性がありますが、キャッシュミスは依然として最も可能性が高いです。最新のPCでのL2キャッシュミスは200サイクルのようであり、PPC(xbox360 / ps3)ではより高いです。強烈なゲームでは、各ゲームオブジェクトがかなりの数のリソースを持つことができるため、最大1000のゲームオブジェクトが存在する可能性があります。これは、スケーリングが大きな懸念事項である問題を検討しています。これにより、開発サイクルの終了時に問題が発生する可能性が高くなります(大量のゲームオブジェクトにヒットする場合)。
サイモン

0

私はどこでもスマートポインターを使用する傾向があります。これが完全に良いアイデアであるかどうかはわかりませんが、私は怠け者であり、本当の欠点はありません(Cスタイルのポインター演算を行いたい場合を除く)。boost :: shared_ptrを使用するのは、コピーできることを知っているからです。2つのエンティティが画像を共有している場合、一方が死んでも他方が画像を失うことはありません。

これの欠点は、あるオブジェクトが指し示し所有しているものを削除するが、別のオブジェクトもそれを指している場合、削除されないことです。


1
私もほぼすべての場所でshare_ptrを使用していますが、今日は、データの一部に対して共有所有権が実際に必要かどうかを考えようとしています。そうでない場合は、そのデータを親データ構造への非ポインターメンバーにするのが妥当かもしれません。所有権を明確にすると、設計が簡素化されると思います。
jmp97

0

優れたスマートポインターによって提供されるメモリ管理とドキュメントの利点は、それらを定期的に使用することを意味します。ただし、プロファイラーがパイプを使用して、特に使用量がかかることを教えてくれた場合は、より新石器時代のポインター管理に戻ります。


0

私は年寄りで、オールドスクールで、サイクルカウンターです。私自身の仕事では、実行時に生のポインタを使用し、動的割り当ては使用しません(プール自体を除く)。すべてがプールされ、所有権は非常に厳格であり、譲渡することはできません。本当に必要な場合は、カスタムの小さなブロックアロケーターを作成します。すべてのプールがゲームをクリアするための状態になっていることを確認します。物事が毛むくじゃらになったら、オブジェクトをハンドルでラップして再配置できるようにしますが、そうではありません。コンテナはカスタムであり、非常にむき出しの骨です。また、コードを再利用しません。
すべてのスマートポインター、コンテナー、イテレーターなどの美徳について議論することは決してありませんが、非常に高速にコーディングできることで知られています(そして、かなり信頼できる-ある程度明白な理由で他の人が私のコードに飛び込むことはお勧めできませんが、心臓発作や恒久的な悪夢のような)。

仕事ではもちろん、プロトタイピングをしていない限り、すべてが異なります。


0

確かにこれは奇妙な答えであり、おそらく誰にも適しているとは言えません。

しかし、特定のタイプのすべてのインスタンスを中央のランダムアクセスシーケンス(スレッドセーフ)に保存し、代わりに32ビットインデックス(相対アドレス、つまり) 、絶対ポインターではなく。

はじめに:

  1. 64ビットプラットフォームでの類推ポインタのメモリ要件を半分にします。これまでのところ、特定のデータ型のインスタンスが42億9,000万個以上必要になることはありませんでした。
  2. これにより、特定のタイプのすべてのインスタンスがTメモリ内に散らばることがなくなります。これにより、すべての種類のアクセスパターンのキャッシュミスが減少する傾向があります。ノードがポインターではなくインデックスを使用してリンクされている場合、ツリーなどのリンクされた構造を横断することもあります。
  3. 並列データは、ツリーやハッシュテーブルの代わりに安価な並列配列(またはスパース配列)を使用して簡単に関連付けることができます。
  4. 集合交差は、たとえば並列ビットセットを使用して、線形時間以上で見つけることができます。
  5. インデックスを基数でソートし、非常にキャッシュに優しいシーケンシャルアクセスパターンを取得できます。
  6. 特定のデータ型のインスタンスがいくつ割り当てられたかを追跡できます。
  7. あなたがそのようなことを気にするなら、例外安全のようなものに対処しなければならない場所の数を最小限にします。

とはいえ、利便性は型の安全性だけでなく欠点でもあります。コンテナインデックスの両方にTアクセスしなければ、インスタンスにアクセスできません。そして、単純な古いものは、それがどのデータ型を参照しているかについて何も教えてくれません。へのインデックスを使用して、誤ってにアクセスしようとする可能性があります。2番目の問題を軽減するために、私はしばしばこのようなことをします。int32_tBarFoo

struct FooIndex
{
    int32_t index;
};

これはちょっとばかげているように見えますが、型エラーが発生するので、コンパイラエラーなしで誤ってBarインデックスを介してにアクセスしようとすることはありませんFoo。利便性のために、私はわずかな不便を受け入れます。

他の人にとって大きな不便なことは、OOPスタイルの継承ベースのポリモーフィズムを使用できないことです。これは、サイズと配置の要件が異なるすべての種類のサブタイプを指すことができるベースポインターを必要とするためです。しかし、最近では継承をあまり使用しません。ECSアプローチを好みます。

についてはshared_ptr、あまり使わないようにしています。ほとんどの場合、所有権を共有するのは理にかなっていないと思います。多くの場合、少なくとも高レベルでは、1つのものが1つのものに属する傾向があります。私が頻繁に使用shared_ptrしたいと思ったのは、スレッドが終了する前にオブジェクトが破壊されないようにするためのスレッドのローカル関数のように、所有権をあまり扱っていない場所でオブジェクトの寿命を延ばすことでしたそれを使用します。

その問題に取り組むために、shared_ptrまたはGCなどを使用する代わりに、スレッドプールから実行される短命のタスクを優先し、そのスレッドがオブジェクトの破棄を要求した場合、実際の破棄は安全に延期されるようにしますシステムが、スレッドが上記のオブジェクトタイプにアクセスする必要がないことを保証できる時間。

参照カウントを使用することもありますが、最後の手段として扱います。そして、永続的なデータ構造の実装のように、所有権を共有することが本当に理にかなっている場合がいくつかありますが、shared_ptrすぐに手を伸ばすことが完全に理にかなっていることがわかります。

とにかく、私は主にインデックスを使用し、生とスマートの両方のポインターを控えめに使用します。インデックスと、オブジェクトが連続して格納され、メモリ空間に散らばっていないことがわかっているときに開くインデックスの種類が好きです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.