воскресенье, 13 ноября 2016 г.

Долгий путь борьбы с глобальными переменными

  Основано на моем опыте написания своих движков, приложений. В сторонних проектах видел все те же способы и ничего оригинального припомнить не могу.


1. Самый простой вариант.
  В очень маленьких программах глобальные переменные никому не мешают, пусть они будут там и только там.


2. Синглтон.
  Как решение проблемы именно глобальных переменных синглтон почти идеален, почти это для тех случаев, когда инстанций класса все же нужно больше чем одна. Но синглтон точно не подходит для классов, для которых создается инстанция в каждом потоке, например: класс потока, таск менеджер, рендер и тд.


3. Синглтон с thread_local.
  Для хранения переменных уникальных для каждого потока они объявляются с ключевым словом thread_local. В далеком 2011 у меня движок исползовал именно такой способ и все отлично работало пока не понадобилось перенести на мобильные платформы. Такой подход не мешает создавать несколько инстанций движка, главное чтоб они были в разных потоках.


4. God object.
Это когда класс движка (EngineCore), приложения (MainApplication) или игры (GameCore) хранит все подсистемы в себе и обращение к ним также организовано через него. Сам же god-object может быть, например, в синглтоне. Для быстро написаных приложений, либо для небольших (по объему кода) приложений это не такой уж плохой вариант.


5. Класс-хранилище.
  Просто некая структура, где хранятся указатели на все классы систем движка. Такой класс-хранилище добавляется в класс потока или в класс приложения (самый главный класс). Остальные классы наследуются от базового класса в котором хранится ссылка на класс-хранилище. Такой подход я использовал в движке под мобильные платформы, так как тогда там не поддерживались thread_local. Так же ничего не мешает создавать несколько инстанций движка даже в одном потоке.
struct SubSystems
{
    EngineCore * engine;
    GraphicsEngine * graphics;
    AudioEngine * audio;
};

class BaseObject
{
    SubSystems *subSystems;
    
    BaseObject (SubSystems *subSys) : subSystems(subSys) {}
};


6. Расширяемый класс-хранилище.
  Когда точно известно какие системы движка нужны для глобального доступа это хорошо, но так бывает не всегда, появляются плагины, потом хочется использовать тот же класс-хранилище и в приложении, а не создавать второй. Поэтому появляется необходимость в расширении этого класса, но что делать, когда точно неизвестно сколько и какие это будут классы?

  Сначала я пришел к такому варианту.
// engine code
class BaseApplication
{
    ...
};

struct SubSystems
{
    BaseApplication * app;
    
    template <typename T>
    T* Get ();
};

// application code
class Application : BaseApplication
{
    SubSystems mainSubSystems;
    WorldManager worldManager;
    
    WorldManager * GetWorldManager ()   { return &worldManager; }
};

template <>
WorldManager * SubSystems::Get<WorldManager> ()
{
    return ((Application *) app)->GetWorldManager();
}

  А недавно переделал уже на более универсальный вариант.
// engine code
typedef TypeListFrom<EngineCore,
                      GraphicsEngine,
                      AudioEngine>  SubSystemsTypeList;

struct SubSystems
{
    struct Item
    {
        void *  ptr = null;
        TypeId  id  = 0;
    };

    StaticArray<Item, 64>     items;
    
    template <typename T>
    T* Get ();
};

class BaseObject
{
    SubSystems *subSystems;
    
    BaseObject (SubSystems *subSys) : subSystems(subSys) {}
};

template <>
EngineCore * SubSystems::Get<EngineCore> ()
{
    return reinterpret_cast<EngineCore *>(
        items[ SubSystemsTypeList::IndexOf<EngineCore> ].ptr );
}


// application code
typedef SubSystemsTypeList::Append<
            TypeListFrom<Application> >   MySubSystemsTypeList;

template <>
Application * SubSystems::Get<Application> ()
{
    return reinterpret_cast<Application *>(
        items[ MySubSystemsTypeList::IndexOf<Application> ].ptr );
}
            
EngineCore *  engine = subSystems->Get<EngineCore>(); 
Application * app    = subSystems->Get<Application>();

  Суть в том что создается список типов, его можно расширить своими типами уже в коде приложения. Зарезервированное количество элементов (в примере - 64) легко настроить под конкретный проект. Главное всегда наследовать списки типов в определенном порядке, чтобы не совпали индексы типов, но в крайнем случае проверка идентификатора типа при присвоении значения просигнализирует об этом.

Комментариев нет:

Отправить комментарий