web-dev-qa-db-ja.com

C ++ 11スマートポインタセマンティクス

私はここ数年ポインターを扱ってきましたが、ごく最近、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をコピー構築するのと同じくらい複雑)、ゲームオブジェクトのレンダリングにはコストがかかります。

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

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

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

長い間お読みいただき、誠にありがとうございました。

37
SamuelMS

C++ 11でも、生のポインターは、オブジェクトへの非所有参照として完全に有効です。あなたの場合、あなたは「テクスチャがそれらのポインタよりも長持ちすることが保証されていると仮定しましょう」と言っています。つまり、ゲームオブジェクトのテクスチャへの生のポインタを使用しても完全に安全です。テクスチャマネージャ内で、テクスチャを自動的に(メモリ内の一定の位置を保証するコンテナに)、またはunique_ptrsのコンテナに保存します。

if outlive-the-pointer保証が有効だった場合notマネージャーのshared_ptrにテクスチャを保存し、ゲームオブジェクトでshared_ptrsまたはweak_ptrsを使用することは理にかなっています。テクスチャに関するゲームオブジェクトの所有権セマンティクスによって異なります。それを逆にすることもできます-オブジェクトにshared_ptrsを格納し、マネージャーにweak_ptrsを格納します。そうすれば、マネージャーはキャッシュとして機能します。テクスチャが要求され、そのweak_ptrがまだ有効である場合、マネージャーはそのコピーを提供します。それ以外の場合は、テクスチャをロードし、shared_ptrを提供し、weak_ptrを保持します。

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

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

これを行う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ポインターをチェックする必要はなく、Textureを作成しようとするときに問題を処理します。

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

11
Jon Kalb

テクスチャが保存されている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を返します。

3
const_ref

行く前に、うっかり小説だったので….

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

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

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

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

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

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

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

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

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

それは幸せな部分です、ここに醜い部分があります。

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

ほら、メモリリークがあり、「どこでもスマートポインタを使うべきだ!」と思いました。重大な失敗。

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

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

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

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

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

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

1
DeepDeadpool