37

私は数年前からポインターを扱ってきましたが、C++11 のスマート ポインター (つまり、一意、共有、および弱い) に移行することを決定したのはつい最近のことです。私はそれらについてかなりの調査を行い、これらが私が導き出した結論です:

  1. ユニークなポインタは素晴らしいです。それらは独自のメモリを管理し、生のポインターと同じくらい軽量です。可能な限り、生のポインターよりも unique_ptr を優先します。
  2. 共有ポインタは複雑です。参照カウントのため、かなりのオーバーヘッドがあります。それらをconst参照で渡すか、あなたのやり方の誤りを後悔してください。それらは悪ではありませんが、慎重に使用する必要があります。
  3. 共有ポインターはオブジェクトを所有する必要があります。所有権が必要ない場合は、ウィーク ポインターを使用します。weak_ptr をロックすると、shared_ptr コピー コンストラクターと同等のオーバーヘッドが発生します。
  4. とにかく廃止された auto_ptr の存在を無視し続けます。

したがって、これらの信条を念頭に置いて、コードベースを修正して新しい光沢のあるスマートポインターを利用することに着手し、できるだけ多くの生のポインターをボードにクリアすることを完全に意図しています。しかし、C++11 のスマート ポインターを最大限に活用する方法について、私は混乱してきました。

たとえば、単純なゲームを設計しているとしましょう。架空の Texture データ型を TextureManager クラスにロードすることが最適であると判断しました。これらのテクスチャは複雑であるため、値で渡すことはできません。さらに、ゲーム オブジェクトには、オブジェクトの種類 (車、ボートなど) に応じて特定のテクスチャが必要であると仮定します。

以前は、テクスチャをベクター (または unordered_map などの他のコンテナー) にロードし、これらのテクスチャへのポインターをそれぞれのゲーム オブジェクト内に格納して、レンダリングが必要なときにそれらを参照できるようにしていました。テクスチャがポインターよりも長く存続することが保証されていると仮定しましょう。

私の質問は、このような状況でスマート ポインターを最大限に活用する方法です。いくつかのオプションが表示されます:

  1. テクスチャをコンテナに直接格納してから、各ゲーム オブジェクトで unique_ptr を構築します。

    class TextureManager {
      public:
        const Texture& texture(const std::string& key) const
            { return textures_.at(key); }
      private:
        std::unordered_map<std::string, Texture> textures_;
    };
    class GameObject {
      public:
        void set_texture(const Texture& texture)
            { texture_ = std::unique_ptr<Texture>(new Texture(texture)); }
      private:
        std::unique_ptr<Texture> texture_;
    };
    

    ただし、これについての私の理解では、渡された参照から新しいテクスチャがコピー構築され、それが unique_ptr によって所有されるということです。これは、テクスチャを使用するゲーム オブジェクトと同じ数のテクスチャのコピーを持つことになるので、非常に望ましくないと思います。

  2. テクスチャを直接保存するのではなく、共有ポインタをコンテナに保存します。共有ポインタを初期化するには、make_shared を使用します。ゲーム オブジェクトでウィーク ポインターを構築します。

    class TextureManager {
      public:
        const std::shared_ptr<Texture>& texture(const std::string& key) const
            { return textures_.at(key); }
      private:
        std::unordered_map<std::string, std::shared_ptr<Texture>> textures_;
    };
    class GameObject {
      public:
        void set_texture(const std::shared_ptr<Texture>& texture)
            { texture_ = texture; }
      private:
        std::weak_ptr<Texture> texture_;
    };
    

    unique_ptr の場合とは異なり、テクスチャ自体をコピーして構築する必要はありませんが、ゲーム オブジェクトのレンダリングは、weak_ptr を毎回ロックする必要があるためコストがかかります (新しい shared_ptr をコピーして構築するのと同じくらい複雑です)。

要約すると、私の理解は次のとおりです。一意のポインターを使用する場合、テクスチャをコピーして構築する必要があります。あるいは、共有ポインターとウィーク ポインターを使用する場合、ゲーム オブジェクトを描画するたびに共有ポインターを基本的にコピー構築する必要があります。

スマート ポインターは生のポインターよりも本質的に複雑になることを理解しているため、どこかで損失を被る必要がありますが、これらのコストはどちらもおそらく本来あるべきよりも高いようです。

誰かが私を正しい方向に向けることができますか?

長々と読んでしまい申し訳ありません。お時間をいただきありがとうございます。

4

4 に答える 4

11

ユースケースを要約すると: *) オブジェクトはユーザーよりも長く存続することが保証されます *) オブジェクトは一度作成されると変更されません (これはあなたのコードによって暗示されていると思います) *) オブジェクトは名前で参照可能であり、任意の存在が保証されますアプリが要求する名前 (推測です。これが正しくない場合の対処方法については、以下で説明します)。

これはうれしいユースケースです。アプリケーション全体でテクスチャの値セマンティクスを使用できます。これには、優れたパフォーマンスと簡単に推論できるという利点があります。

これを行う 1 つの方法は、TextureManager が Texture const* を返すようにすることです。検討:

using TextureRef = Texture const*;
...
TextureRef TextureManager::texture(const std::string& key) const;

基になる Texture オブジェクトにはアプリケーションの有効期間があり、変更されることはなく、常に存在するため (ポインターが nullptr になることはありません)、TextureRef を単純な値として扱うことができます。それらを渡したり、戻したり、比較したり、それらのコンテナーを作成したりできます。それらは非常に簡単に推論でき、非常に効率的に作業できます。

ここでの厄介な点は、値のセマンティクス (これは良いことです) がありますが、ポインター構文 (値のセマンティクスを持つ型を混乱させる可能性があります) があることです。つまり、Texture クラスのメンバーにアクセスするには、次のようにする必要があります。

TextureRef t{texture_manager.texture("grass")};

// You can treat t as a value. You can pass it, return it, compare it,
// or put it in a container.
// But you use it like a pointer.

double aspect_ratio{t->get_aspect_ratio()};

これに対処する 1 つの方法は、pimpl イディオムのようなものを使用して、テクスチャ実装へのポインターへのラッパーに過ぎないクラスを作成することです。これは、実装クラスの API に転送するテクスチャ ラッパー クラスの API (メンバー関数) を作成することになるため、もう少し作業が必要です。しかし、利点は、値のセマンティクスと値の構文の両方を持つテクスチャ クラスがあることです。

struct Texture
{
  Texture(std::string const& texture_name):
    pimpl_{texture_manager.texture(texture_name)}
  {
    // Either
    assert(pimpl_);
    // or
    if (not pimpl_) {throw /*an appropriate exception */;}
    // or do nothing if TextureManager::texture() throws when name not found.
  }
  ...
  double get_aspect_ratio() const {return pimpl_->get_aspect_ratio();}
  ...
  private:
  TextureImpl const* pimpl_; // invariant: != nullptr
};

...

Texture t{"grass"};

// t has both value semantics and value syntax.
// Treat it just like int (if int had member functions)
// or like std::string (except lighter weight for copying).

double aspect_ratio{t.get_aspect_ratio()};

ゲームのコンテキストでは、存在が保証されていないテクスチャを要求することは決してないと思います。その場合は、その名前が存在すると断言できます。しかし、そうでない場合は、その状況に対処する方法を決定する必要があります。私のお勧めは、ポインターが nullptr にならないことをラッパー クラスの不変条件にすることです。これは、テクスチャが存在しない場合にコンストラクタからスローすることを意味します。つまり、ラッパー クラスのメンバーを呼び出すたびに null ポインターをチェックする必要はなく、テクスチャを作成しようとするときに問題を処理できます。

元の質問への答えとして、スマート ポインターは有効期間管理に価値があり、有効期間がポインターよりも長持ちすることが保証されているオブジェクトへの参照を渡すだけでよい場合は特に役に立ちません。

于 2013-11-13T23:41:18.110 に答える
3

テクスチャが格納されている std::unique_ptrs の std::map を持つことができます。次に、テクスチャへの参照を名前で返す get メソッドを記述できます。そうすれば、各モデルがそのテクスチャの名前を知っている場合 (そうあるべきです)、単純にその名前を get メソッドに渡し、マップから参照を取得できます。

class TextureManager 
{
  public:
    Texture& get_texture(const std::string& key) const
        { return *textures_.at(key); }
  private:
    std::unordered_map<std::string, std::unique_ptr<Texture>> textures_;
};

その後、Texture*、weak_ptr などではなく、ゲーム オブジェクト クラスで Texture を使用できます。

このようにして、テクスチャ マネージャはキャッシュのように機能し、get メソッドを書き直してテクスチャを検索し、見つかった場合はマップから返し、そうでない場合は最初にロードし、マップに移動してから ref を返します。

于 2013-11-12T13:16:00.050 に答える
1

行く前に、うっかり小説を…。

TL;DR 責任の問題を解決するために共有ポインタを使用しますが、循環的な関係には十分注意してください。もし私があなたなら、共有ポインターのテーブルを使用してアセットを保存し、それらの共有ポインターを必要とするすべてのものも共有ポインターを使用する必要があります。これにより、読み取りのためのウィーク ポインターのオーバーヘッドがなくなります (ゲームでのオーバーヘッドは、オブジェクトごとに 1 秒間に 60 回新しいスマート ポインターを作成するようなものです)。これは私のチームと私が採用したアプローチでもあり、非常に効果的でした。また、テクスチャはオブジェクトよりも長持ちすることが保証されているため、オブジェクトが共有ポインターを使用している場合、オブジェクトはテクスチャを削除できないとも言います。

2 セントを投じることができれば、自分のビデオ ゲームでスマート ポインターを使って行ったほぼ同じ試みについてお話したいと思います。良いことも悪いことも。

このゲームのコードは、解決策 2 とほぼ同じアプローチを採用しています: ビットマップへのスマート ポインターで満たされたテーブル。

ただし、いくつかの違いがありました。ビットマップのテーブルを 2 つの部分に分割することにしました。1 つは「緊急」ビットマップ用で、もう 1 つは「簡易」ビットマップ用です。緊急ビットマップは、常にメモリにロードされるビットマップであり、戦闘の途中で使用されます。この場合、今すぐアニメーションが必要で、スタッターが非常に目立つハードディスクに移動したくありませんでした。簡単なテーブルは、hdd 上のビットマップへのファイル パスの文字列のテーブルでした。これらは、ゲームプレイの比較的長いセクションの開始時にロードされる大きなビットマップです。キャラクターの歩行アニメーションや背景画像など。

ここで生ポインタを使用すると、特に所有権などの問題が発生します。アセット テーブルにはBitmap *find_image(string image_name)関数がありました。この関数は、最初に緊急テーブルで一致するエントリを検索しますimage_name。見つかった場合は、素晴らしいです。ビットマップ ポインターを返します。見つからない場合は、facile テーブルを検索します。イメージ名に一致するパスが見つかった場合は、ビットマップを作成し、そのポインターを返します。

これを最もよく使用するクラスは、間違いなく Animation クラスでした。所有権の問題は次のとおりです。アニメーションはいつビットマップを削除する必要がありますか? それが簡単なテーブルから来た場合、問題はありません。そのビットマップはあなたのために特別に作成されました。それを削除するのはあなたの義務です!

ただし、ビットマップが緊急テーブルからのものである場合は、削除できません。削除すると、他のユーザーが使用できなくなり、ET ゲームのようにプログラムがダウンし、売り上げがそれに追随します。

スマート ポインターがない場合、ここでの唯一の解決策は、Animation クラスにビットマップのクローンを作成させることです。これにより、安全な削除が可能になりますが、プログラムの速度が低下します。これらの画像は時間に敏感であるはずではありませんでしたか?

ただし、資産クラスが を返す場合はshared_ptr<Bitmap>、心配する必要はありません。私たちの資産テーブルはご覧のとおり静的だったので、これらのポインターはプログラムの最後まで持続していました。関数を に変更しshared_ptr<Bitmap> find_image (string image_name)たため、ビットマップを再度複製する必要はありませんでした。ビットマップが簡単なテーブルから取得された場合、そのスマート ポインターはその種類の唯一のものであり、アニメーションと共に削除されました。それが緊急のビットマップである場合、テーブルはアニメーションの破棄時にまだ参照を保持しており、データは保持されていました。

それが楽しい部分で、ここが醜い部分です。

共有ポインターと固有ポインターが優れていることがわかりましたが、それらには間違いなく注意点があります。私にとって最大の問題は、データがいつ削除されるかを明示的に制御できないことです。共有ポインターは、アセットのルックアップを保存しましたが、実装時にゲームの残りの部分を殺しました。

メモリ リークが発生し、「どこでもスマート ポインタを使用する必要がある」と考えました。重大な失敗。

私たちのゲームには がGameObjectsあり、 によって制御されていましたEnvironment。各環境には のベクトルがGameObject *あり、各オブジェクトにはその環境へのポインターがありました。

あなたは私がこれでどこに行くのかを見るべきです。

オブジェクトには、環境から自分自身を「排出」するメソッドがありました。これは、新しいエリアに移動したり、テレポートしたり、他のオブジェクトを段階的に通過したりする必要がある場合です。

環境がオブジェクトへの唯一の参照ホルダーである場合、オブジェクトは削除されずに環境を離れることができませんでした。これは、発射物、特にテレポート発射物を作成するときによく発生します。

少なくともオブジェクトが最後に環境を離れた場合は、オブジェクトも環境を削除していました。ほとんどのゲーム ステートの環境も具体的なオブジェクトでした。スタックで DELETE を呼び出していました。ええ、私たちはアマチュアでした、私たちを訴えてください。

私の経験では、delete を呼び出すのが面倒で、オブジェクトを 1 つしか所有しない場合は unique_pointers を使用し、複数のオブジェクトが 1 つのものを指すようにしたいが、誰がそれを削除する必要があるかを判断できない場合は shared_pointers を使用します。 shared_pointers との循環関係には十分注意してください。

于 2015-08-07T00:30:40.150 に答える