グローバルオブジェクトとその生息地

グローバルオブジェクトは、使いやすさから広く普及しています。 これらは、設定、ゲームエンティティ、および一般的にコードのどこにでも必要なデータを保存します。 必要なすべての引数を関数に渡すと、パラメーターリストが非常に大きくなる可能性があります。 利便性に加えて、初期化と破棄の順序、追加の依存関係、単体テストの記述の難しさなどの欠点もあります。 多くのプログラマーは、初心者だけがグローバル変数を使用していると思い込んでおり、これが学生ラボのレベルです。 ただし、CryEngine、UDK、OGREなどの大規模プロジェクトでは、グローバルオブジェクトも使用されます。 唯一の違いは、このツールの所有権のレベルです。







それでは、このグローバルオブジェクトはどのような種類の獣なのでしょうか? まとめましょう。



グローバルオブジェクトを作成するには多くの方法があります。 最も簡単な方法は、ヘッダーファイルでextern変数を宣言し、cppでインスタンス化することです。



// header file extern Foo g_foo; // cpp file Foo g_foo;
      
      





より抽象的なアプローチは、 シングルトンパターンです。



 void PrepareFoo(...) { FooManager::getInstance().Initialize (); }
      
      





この決定が非常に注意を払っているのは何が良いでしょうか? プログラム内の任意の場所でオブジェクトを使用できます。 それは非常に便利であり、そうする誘惑は非常に大きい。 問題は、システムの一部を交換する必要があるときに始まります。残りを中断したり、コードをテストしたりする必要はありません。 後者の場合、対象のメソッドが使用するほぼすべてのグローバル変数を初期化する必要があります。 さらに、上記の問題により、オブジェクトの動作をテストに望ましいものに置き換えることが非常に難しくなります。 また、作成と削除の順序を制御することはできません。これは、未定義の動作やプログラムのクラッシュを引き起こす可能性があります。 たとえば、まだ作成されていない、または既に削除されているグローバルオブジェクトにアクセスする場合。



一般に、グローバル変数の代わりにローカル変数を使用することをお勧めします。 たとえば、オブジェクトを描画する必要があり、グローバルレンダラーが存在する場合、グローバルRender::Instance



()を使用するよりも、 void Draw(Renderer& render_instance)



に直接渡す方が適切です。 シングルトンを使用してはならない理由と理由の詳細については、 投稿を参照してください



ただし、グローバルオブジェクトなしで行うことは困難です。 設定またはプロトタイプにアクセスする必要がある場合、必要なすべてのコンテナー、ファクトリー、およびその他のパラメーターを各オブジェクトに添付しません。 このケースを検討します。



開始するには、問題のステートメント:



  1. オブジェクトは、プログラムのどの部分からもアクセス可能でなければなりません。
  2. メンテナンスを容易にするため、リサイクル可能なグローバルオブジェクトはすべて中央に保管する必要があります。
  3. コンテキストに応じてグローバルオブジェクトを追加または置換する機能は、実際の実行またはテストです。


実装を成功させるためには、示されているすべての条件が満たされていることが重要です。



興味深いソリューションがCryEngineの腸内で SSystemGlobalEnvironment



SSystemGlobalEnvironment



参照)。 グローバルオブジェクトは1つの構造にラップされ、プログラム内の適切な場所で適切なタイミングで初期化される抽象エンティティへのポインターです。 追加のオーバーヘッド、不要なアドオン、コンパイル時の型制御はありません-美!



CryEngineはかなり古くて古いプロジェクトであり、すべてのインターフェイスが落ち着いており、新しいインターフェイスは現在存在するものと同じようにねじ込まれています。 したがって、追加のラッパーや、グローバルオブジェクトを操作する方法を考え出す必要はありません。 別のオプションがあります-若くて急速に開発されているプロジェクトで、厳密なインターフェースがなく、機能が絶えず変化しているため、インターフェースを頻繁に修正することを奨励します。 古いプロジェクトがリファクタリングするのに役立ち、それでもグローバルアクセスが必要な新しいプロジェクトでは、使用の欠点を最小限に抑えるソリューションが必要です。 答えを見つけるには、1つ上のレベルに進み、別の角度から問題を調べてみてくださいGlobalObjectBase



から継承したグローバルオブジェクトのリポジトリを作成します。 シェルを使用すると、実行時に操作が追加されるため、変更後のパフォーマンスに注意してください。



最初に、ストレージオブジェクトに子孫を配置できる基本クラスを作成する必要があります。



  class GlobalObjectBase { public: virtual ~GlobalObjectBase() {} };
      
      





今リポジトリ自体。 プログラムの任意の部分からアクセスするには、このクラスのオブジェクトを、最適な標準メソッドのいずれかを使用してグローバルにする必要があります。



ストレージクラス
 class GlobalObjectsStorage { private: using ObjPtr = std::unique_ptr<GlobalObjectBase>; std::vector<ObjPtr> m_dynamic_globals; private: GlobalObjectBase* GetGlobalObjectImpl(size_t i_type_code) const { … } void AddGlobalObjectImpl(std::unique_ptr<GlobalObjectBase> ip_object) { … } void RemoveGlobalObjectImpl(size_t i_type_code) { … } public: GlobalObjectsStorage() {} template <typename ObjectType> void AddGlobalObject() { AddGlobalObjectImpl(std::make_unique<ObjectType>()); } template <typename ObjectType> ObjectType* GetGlobalObject() const { return static_cast<ObjectType*>(GetGlobalObjectImpl(typeid(ObjectType).hash_code()); } template <typename ObjectType> void RemoveGlobalObject() { RemoveGlobalObjectImpl(typeid(ObjectType).hash_code()); } };
      
      





このタイプのオブジェクトを操作するには、そのタイプで十分なので、 GlobalObjectsStorage



インターフェースは、必要な実装データを送信するテンプレートメソッドで構成されます。



だから、最初のテストドライブ-それは動作します!



 class FooManager : public GlobalObjectBase { public: void Initialize() {} }; static GlobalObjectsStorage g_storage; //    void Test() { //   "" g_storage.AddGlobalObject<FooManager>(); //  g_storage.GetGlobalObject<FooManager>()->Initialize(); //   g_storage.RemoveGlobalObject<FooManager>(); }
      
      





しかし、それだけではありません。異なるコンテキストのオブジェクトを置き換えることはできません。 リポジトリの親クラスを追加し、そこにテンプレートメソッドを転送し、実装メソッドを仮想化することで修正します。



基本ストレージクラス
 template <typename BaseObject> class ObjectStorageBase { private: virtual BaseObject* GetGlobalObjectImpl(size_t i_type_code) const = 0; virtual void AddGlobalObjectImpl(std::unique_ptr<BaseObject> ip_object) = 0; virtual void RemoveGlobalObjectImpl(size_t i_type_code) = 0; public: virtual ~ObjectStorageBase() {} template <typename ObjectType> void AddGlobalObject() { AddGlobalObjectImpl(std::make_unique<ObjectType>()); } template <typename ObjectType> ObjectType* GetGlobalObject() const { return static_cast<ObjectType*>(GetGlobalObjectImpl(typeid(ObjectType).hash_code())); } template <typename ObjectType> void RemoveGlobalObject() { RemoveGlobalObjectImpl(typeid(ObjectType).hash_code()); } virtual std::vector<BaseObject*> GetStoredObjects() = 0; }; class GameGlobalObject : public GlobalObjectBase { public: virtual ~GameGlobalObject() {} virtual void Update(float dt) {} virtual void Init() {} virtual void Release() {} }; class DefaultObjectsStorage : public ObjectStorageBase<GameGlobalObject> { private: using ObjPtr = std::unique_ptr<GameGlobalObject>; std::vector<ObjPtr> m_dynamic_globals; private: virtual GameGlobalObject* GetGlobalObjectImpl(size_t i_type_code) const override { … } virtual void AddGlobalObjectImpl(std::unique_ptr<GameGlobalObject> ip_object) override { … } virtual void RemoveGlobalObjectImpl(size_t i_type_code) override { … } public: DefaultObjectsStorage() {} virtual std::vector<GameGlobalObject*> GetStoredObjects() override { return m_cache_objects; } }; static std::unique_ptr<ObjectStorageBase<GameGlobalObject>> gp_storage(new DefaultObjectsStorage()); void Test() { //   "" gp_storage->AddGlobalObject<ResourceManager>(); //  gp_storage->GetGlobalObject<ResourceManager>()->Initialize(); //   gp_storage->RemoveGlobalObject<ResourceManager>(); }
      
      





多くの場合、グローバル操作は、作成または削除中に異なる方法で操作する必要があります。 私たちのプロジェクトでは、これはディスク(たとえば、サブシステムの設定ファイル)からデータを読み取り、アプリケーションの読み込み時およびゲーム中の特定の時間間隔後に発生するプレーヤーのデータを更新し、ゲーム内サイクルを更新します。 他のプログラムには、追加のアクションまたは完全に異なるアクションがある場合があります。 したがって、最終的な基本型はクラスのユーザーによって決定され、同じメソッドへの複数の呼び出しが回避されます。



 for (auto p_object : g_storage->GetStoredObjects()) p_object->Init();
      
      





最後にすべてが良いですか?



このようなラッパーのパフォーマンスは、グローバルオブジェクトを直接使用するよりも悪いことは明らかです。 テスト用に10種類のタイプが作成されました。 最初は、変更なしでグローバルオブジェクトとして使用され、その後DefaultObjectsStorage



1,000,000コールの結果。









現在のコードは、通常のグローバルオブジェクトよりもほぼ18倍遅いです! プロファイラーは、 typeid(*obj).hash_code()



が最も時間がかかることをtypeid(*obj).hash_code()



ます。 実行時の型に関するデータの抽出には多くのプロセッサ時間がかかるため、回避する必要があります。 これを行う最も簡単な方法は、グローバルオブジェクトの基本クラス( GlobalObjectBase



)にタイプハッシュを格納することです。



 class GlobalObjectBase { protected: size_t m_hash_code; public: ... size_t GetTypeHashCode() const { return m_hash_code; } virtual void RecalcHashCode() { m_hash_code = typeid(*this).hash_code(); } };
      
      





ObjectStorageBase::AddGlobalObject DefaultObjectsStorage:: GetGlobalObjectImpl



も変更する価値があります。 さらに、親クラスObjectStorageBase::GetGlobalObject



テンプレート関数に型データを静的に保存します。



ストレージ最適化
 template <typename BaseObject> class ObjectStorageBase {public: template <typename ObjectType> void AddGlobalObject() { auto p_object = std::make_unique<ObjectType>(); p_object->RecalcHashCode(); AddGlobalObjectImpl(std::move(p_object)); } template <typename ObjectType> ObjectType* GetGlobalObject() const { static size_t type_hash = typeid(ObjectType).hash_code()); return static_cast<ObjectType*>(GetGlobalObjectImpl(type_hash); } … }; class DefaultObjectsStorage : public ObjectStorageBase<GameGlobalObject> { … private: virtual GlobalObjectBase* GetGlobalObjectImpl(size_t i_type_code) const override { auto it = std::find_if(m_dynamic_globals.begin(), m_dynamic_globals.end(), [i_type_code](const ObjPtr& obj) { return obj->GetTypeHashCode() == i_type_code; }); if (it == m_dynamic_globals.end()) { //      ,  -    return nullptr; } return it->get(); } … };
      
      





上記の変更により、目的のオブジェクトの検索時間が大幅に短縮され、差は18倍になりませんが、1.25-ほとんどの場合、これで十分です。









さらに、テスト用にストレージ全体を変更しないために、 GlobalObjectBase::RecalcHashCode



をオーバーライドして、必要なオブジェクトのみを選択的に置き換えることができます。 メインクラスで置き換えるには、テストとテスト後継クラスに必要なメソッドを仮想化する必要があります。



交換例
 struct Foo : public GlobalObjectBase { int x = 0; virtual void SetX() { x = rand()%1; } }; struct FooTest : public Foo { virtual void SetX() override { x = 5; } virtual void RecalcHashCode() { m_hash_code = typeid(First).hash_code(); } }; g_getter.AddGlobalObject<FooTest>(); g_getter.GetGlobalObject<Foo>()->SetX();
      
      





このアプローチを導入した先駆者はFishdomで 、このラッパーを通じていくつかのオブジェクトが使用されていました。 これにより、依存関係を削除し、コードの一部をテストで覆い、適切な場所でメソッド(Init、Release、Update)の呼び出しを単調に作業しやすくしました。



ここで 、最終的なシェルコードと説明されているテストを見つけることができます。



All Articles