Global objects and their habitats

    Global objects are widespread because of their ease of use. They store settings, game entities, and generally any data that may be needed anywhere in the code. Passing all the necessary arguments to the function can inflate the parameter list to a very large size. In addition to convenience, there are also disadvantages: the order of initialization and destruction, additional dependencies, the difficulty of writing unit tests. Many programmers prejudice that only beginners use global variables and this is the level of student labs. However, in large projects like CryEngine, UDK, OGRE, global objects are also used. The only difference is the level of ownership of this tool. So what kind of beast is this global object,



    how to tame it and use amenities, minimizing the disadvantages? Let's get it together.

    There are many ways to create a global object. The easiest is to declare an extern variable in the header file and instantiate it in cpp:

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

    A more abstract approach is the singleton pattern .

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

    What good is this decision that it pays so much attention? It allows you to use the object anywhere in the program. It is very convenient, and the temptation to do so is very great. Problems begin when you need to replace part of the system without disrupting the rest, or test the code. In the latter case, we will have to initialize almost all the global variables that the method of interest to us uses. Moreover, the above difficulties make it very difficult to replace the behavior of the object with the desired for tests. There is also no control over the order of creation and deletion, which can lead to undefined behavior or crashes of the program. For example, when accessing a global object that has not yet been created or has already been deleted.

    In general, it is preferable to use local variables instead of global ones. For example, if you need to draw an object and there is a global Renderer, then it is better to pass it directly to the method void Draw(Renderer& render_instance), rather than using global Render::Instance(). More examples and justifications why you should not use singleton can be found in the post .

    However, it is difficult to do without global objects. If you need access to settings or prototypes, then you will not attach all the necessary containers, factories and other parameters to each object. We will consider this case.

    To start, the statement of the problem:

    1. The object must be accessible from any part of the program.
    2. All recyclable global objects should be stored centrally - for ease of maintenance.
    3. The ability to add and / or replace global objects depending on the context is a real run or test.

    In order to consider the implementation successful, it is important that all the conditions indicated are met.

    An interesting solution was spied in the bowels of CryEngine (see SSystemGlobalEnvironment). Global objects are wrapped in one structure and are pointers to abstract entities that are initialized at the right time in the right place in the program. No extra overhead, no extra add-ons, type control at compile time is a beauty!

    CryEngine is a fairly old and years-old project, where all interfaces are settled down, and the new one is screwed on like what exists at the moment. Therefore, there is no need to come up with additional wrappers or ways to work with global objects. There is another option - a young and rapidly developing project, where there are no strict interfaces, where the functionality is constantly changing, which encourages to make corrections to the interfaces quite often. I would like to have a solution that will help in the old projects to refactor, and in new ones, where global access is nevertheless necessary, to minimize the disadvantages of use. To find the answer, you can try to go up one level and look at the problem from a different angle - create a repository of global objects inherited fromGlobalObjectBase. Using the shell will add operations at runtime, so be sure to pay attention to performance after the changes.

    First you need to create a base class whose descendants can be placed in the storage object.

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

    Now the repository itself. To access from any part of the program, an object of this class must be made global using one of the standard methods that you like best.

    Storage class
    class GlobalObjectsStorage
    {
    private:
    	using ObjPtr = std::unique_ptr;
    	std::vector m_dynamic_globals;
    private:
    	GlobalObjectBase* GetGlobalObjectImpl(size_t i_type_code) const
    	{ … }
    	void AddGlobalObjectImpl(std::unique_ptr ip_object)
    	{ … }
    	void RemoveGlobalObjectImpl(size_t i_type_code)
    	{ … }
    public:
    	GlobalObjectsStorage() {}	
    	template 
    	void AddGlobalObject()
    	{
    		AddGlobalObjectImpl(std::make_unique());
    	}
    	template 
    	ObjectType* GetGlobalObject() const
    	{
    		return static_cast(GetGlobalObjectImpl(typeid(ObjectType).hash_code());
    	}
    	template 
    	void RemoveGlobalObject()
    	{
    		RemoveGlobalObjectImpl(typeid(ObjectType).hash_code());
    	}
    };

    To work with this type of objects, their type is enough, so the interface GlobalObjectsStorageis made up of template methods that transmit the necessary implementation data.

    So, the first test drive - it works!

    class FooManager : public GlobalObjectBase
    {
    public:
    	void Initialize() {}
    };
    static GlobalObjectsStorage g_storage; // имитируем глобальность хранилища
    void Test()
    {
    	// делаем объект "глобальным"
    	g_storage.AddGlobalObject();
    	// используем
    	g_storage.GetGlobalObject()->Initialize();
    	// и удаляем
    	g_storage.RemoveGlobalObject();
    }

    But that’s not all - you can’t replace objects for different contexts. We fix it by adding the parent class for the repository, transferring the template methods there, and making the implementation methods virtual.

    Base storage class
    template 
    class ObjectStorageBase
    {
    private:
    	virtual BaseObject* GetGlobalObjectImpl(size_t i_type_code) const = 0;
    	virtual void AddGlobalObjectImpl(std::unique_ptr ip_object) = 0;
    	virtual void RemoveGlobalObjectImpl(size_t i_type_code) = 0;
    public:
    	virtual ~ObjectStorageBase() {}
    	template 
    	void AddGlobalObject()
    	{
    		AddGlobalObjectImpl(std::make_unique());
    	}
    	template 
    	ObjectType* GetGlobalObject() const
    	{
    		return static_cast(GetGlobalObjectImpl(typeid(ObjectType).hash_code()));
    	}
    	template 
    	void RemoveGlobalObject()
    	{
    		RemoveGlobalObjectImpl(typeid(ObjectType).hash_code());
    	}
    	virtual std::vector GetStoredObjects() = 0;
    };
    class GameGlobalObject : public GlobalObjectBase
    {
    	public:
    		virtual ~GameGlobalObject() {}
    		virtual void Update(float dt) {}
    		virtual void Init() {}
    		virtual void Release() {}
    };
    class DefaultObjectsStorage : public ObjectStorageBase
    {
    private:
    	using ObjPtr = std::unique_ptr;
    	std::vector m_dynamic_globals;
    private:
    	virtual GameGlobalObject* GetGlobalObjectImpl(size_t i_type_code) const override
    	{    …	}
    	virtual void AddGlobalObjectImpl(std::unique_ptr ip_object) override
    	{    …	}
    			virtual void RemoveGlobalObjectImpl(size_t i_type_code) override
    	{    …	}
    public:
    	DefaultObjectsStorage() {}
    	virtual std::vector GetStoredObjects() override { return m_cache_objects; }
    };
    static std::unique_ptr> gp_storage(new DefaultObjectsStorage());
    void Test()
    {
    	// делаем объект "глобальным"
    	gp_storage->AddGlobalObject();
    	// используем
    	gp_storage->GetGlobalObject()->Initialize();
    	// и удаляем
    	gp_storage->RemoveGlobalObject();
    }

    Often, global objects need to be manipulated differently during creation or deletion. In our projects, this is reading data from a disk (for example, a settings file for a subsystem), updating player data that occurs when the application loads and after a certain time interval during the game, and updating the in-game cycle. Other programs may have additional or completely different actions. Therefore, the final base type will be determined by the user of the class and will avoid multiple calls to the same methods.

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

    Is everything good in the end?


    It is clear that the performance of such a wrapper will be worse than using a global object directly. Ten different types were created for the test. At first they were used as a global object without our changes, then through DefaultObjectsStorage. Result for 1,000,000 calls.


    The current code is almost 18 times slower than a regular global object! The profiler tells you what takes the most time typeid(*obj).hash_code(). Since the extraction of data on types at runtime spends a lot of processor time, you need to bypass it. The easiest way to do this is to store the type hash in the base class of global objects ( 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(); }
    };

    It’s also worth changing the method ObjectStorageBase::AddGlobalObject и DefaultObjectsStorage:: GetGlobalObjectImpl. Additionally, we statically store the type data in the template function of the parent class ObjectStorageBase::GetGlobalObject.

    Storage Optimization
    template 
    class ObjectStorageBase
    {
    	…
    public:
    	template 
    	void AddGlobalObject()
    	{
    		auto p_object = std::make_unique();
    		p_object->RecalcHashCode();
    		AddGlobalObjectImpl(std::move(p_object));
    	}
    	template 
    	ObjectType* GetGlobalObject() const
    	{
    		static size_t type_hash = typeid(ObjectType).hash_code());
    		return static_cast(GetGlobalObjectImpl(type_hash);
    	}
    	…	
    };
    class DefaultObjectsStorage : public ObjectStorageBase
    {
    	…
    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();
    	}
    	…
    };

    The above changes can significantly reduce the search time for the desired object, and the difference will not be 18 times, but 1.25 - this is quite acceptable in most cases.


    In addition, in order not to change the entire storage for tests, you can override the method GlobalObjectBase::RecalcHashCodeand selectively replace only the necessary objects. For replacement in the main class, it is necessary to make the methods necessary for the test and the test successor class virtual.

    Replacement Example
    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();
    g_getter.GetGlobalObject()->SetX();

    The pioneer for implementing this approach was Fishdom , where several objects were used through this wrapper. This allowed us to remove dependencies, cover part of the code with tests and make it easier to monotonously work on calling methods (Init, Release, Update) in the right places.

    According to the link you can find the final shell code and the tests described.

    Also popular now: