Implementando sinais em C ++ 11

O padrão do observador é muito usado em videogames quando vários subsistemas de jogo precisam ser notificados de um evento. O padrão original envolve a definição de uma interface que cada observador implementará, mas uma maneira muito mais simples de fazer isso é usando sinais . Neste artigo, discutimos a evolução desse padrão nos jogos móveis do SocialPoint .

Um exemplo típico de um evento de jogo observável seria uma mudança na pontuação do jogo. Implementar isso em C ++ é fácil.

class IGameScoreObserver
{
public:
virtual IGameScoreObserver(){};
virtual void notifyGameScoreChanged(unsigned score) = 0;
};

class GameScore
{
private:
std
::vector<IGameScoreObserver*> _observers;
unsigned _value;
public:
GameScore(unsigned value=0) : _value(value)
{
}

void setValue(unsigned value)
{
_value
= value;
for(auto observer : _observers)
{
observer
->notifyGameScoreChanged(value);
}
}

void addObserver(IGameScoreObserver& observer)
{
_observers
.push_back(&observer);
}

void removeObserver(IGameScoreObserver& observer)
{
_observers
.erase(std::remove(_observers.begin(), _observers.end(), &observer), _observers.end());
}
};

Essa implementação é simples e funcionaria, mas tem dois problemas principais:

  • há algum código padrão que deve ser escrito para cada observador (poderia ser melhorado escrevendo uma classe de assunto abstrato usando modelos)
  • pode levar a herança múltipla em classes que implementam o IGameScoreObserverque pode levar ao temido diamante da morte

Usar sinais resolve esses dois problemas, ocultando o contêiner do observador dentro de um objeto de função . Em nosso caso, começamos a usar boost :: signal, pois em nossos primeiros jogos para celular ainda não havia suporte para C ++ 11 e já estávamos usando algumas das bibliotecas boost .

Nossa primeira implementação usando parecia algo assim:boost::signal

#include <boost/signal.hpp>

class Signals
{
private:
Signals(){};
public:
static boost::signal<void(unsigned value)> gameScoreChanged;
};

class GameScore
{
public:

GameScore(unsigned value=0) : _value(value)
{
}

void setValue(unsigned value)
{
_value
= value;
Signals::gameScoreChanged(value);
}
};

Como você pode ver, muito menos código clichê e nenhuma interface necessária para o observador de pontuação do jogo. A conexão e o observador são feitos usando .boost::function

#include <boost/function.hpp>
ScoreView view;
boost
::signal<void(unsigned value)>::connection connGameScoreChanged = Signals::gameScoreChanged.connect(boost::function(&ScoreView::updateScore, view, _1));

O retorna um objeto de conexão que pode ser usado para remover o observador. Há também um que se desconectará automaticamente quando o objeto for destruído. Desconectar o sinal é importante para evitar o travamento que ocorrerá ao chamar o em um objeto que já está destruído.boost::signalboost::scoped_connectionboost::function

Conseguimos fazer isso funcionar e parecia ok, mas introduzimos novos problemas devido à maneira como estávamos usando:

  • boost::signalpode retornar um valor não vazio, esse valor é gerado a partir dos valores de retorno das funções conectadas usando um argumento de modelo de sinal adicional denominado combinador . Essa funcionalidade é confusa e não faz sentido no contexto do padrão do observador original.
  • boost::signalos objetos são todos públicos e agrupados em um cabeçalho gigante. Isso cria dependências artificiais e também aumenta o tempo de compilação.
  • a implementação do padrão de observador usando é mostrado para o exteriorboost::signal

Em nossos jogos mais novos, tentamos resolver esses problemas e também tentar mudar para o C ++ 11 removendo a dependência do boost. A primeira coisa que fizemos foi implementar nossa própria classe de sinal.

template<class... F>
class SignalConnection;

template<class... F>
class ScopedSignalConnection;

template<class... F>
class SignalConnectionItem
{
public:
typedef std::function<void(F...)> Callback;
private:
Callback _callback;
bool _connected;

public:
SignalConnectionItem(const Callback& cb, bool connected=true) :
_callback
(cb), _connected(connected)
{
}

void operator()(F... args)
{
if(_connected && _callback)
{
_callback
(args...);
}
}

bool connected() const
{
return _connected;
}

void disconnect()
{
_connected
= false;
}
};

template<class... F>
class Signal
{
public:
typedef std::function<void(F...)> Callback;
typedef SignalConnection<F...> Connection;
typedef ScopedSignalConnection<F...> ScopedConnection;

private:
typedef SignalConnectionItem<F...> ConnectionItem;
typedef std::list<std::shared_ptr<ConnectionItem>> ConnectionList;

ConnectionList _list;
unsigned _recurseCount;

void clearDisconnected()
{
_list
.erase(std::remove_if(_list.begin(), _list.end(), [](std::shared_ptr<ConnectionItem>& item){
return !item->connected();
}), _list.end());
}

public:

Signal() :
_recurseCount
(0)
{
}

~Signal()
{
for(auto& item : _list)
{
item
->disconnect();
}
}

void operator()(F... args)
{
std
::list<std::shared_ptr<ConnectionItem>> list;
for(auto& item : _list)
{
if(item->connected())
{
list
.push_back(item);
}
}
_recurseCount
++;
for(auto& item : list)
{
(*item)(args...);
}
_recurseCount
--;
if(_recurseCount == 0)
{
clearDisconnected
();
}
};

Connection connect(const Callback& callback)
{
auto item = std::make_shared<ConnectionItem>(callback, true);
_list
.push_back(item);
return Connection(*this, item);
}

bool disconnect(const Connection& connection)
{
bool found = false;
for(auto& item : _list)
{
if(connection.hasItem(*item) && item->connected())
{
found
= true;
item
->disconnect();
}
}
if(found)
{
clearDisconnected
();
}
return found;
}

void disconnectAll()
{
for(auto& item : _list)
{
item
->disconnect();
}
clearDisconnected
();
}

friend class Connecion;
};

template<class... F>
class SignalConnection
{
private:
typedef SignalConnectionItem<F...> Item;

Signal<F...>* _signal;
std
::shared_ptr<Item> _item;

public:
SignalConnection()
: _signal(nullptr)
{
}

SignalConnection(Signal<F...>& signal, const std::shared_ptr<Item>& item)
: _signal(&signal), _item(item)
{
}

void operator=(const SignalConnection& other)
{
_signal
= other._signal;
_item
= other._item;
}

virtual ~SignalConnection()
{
}

bool hasItem(const Item& item) const
{
return _item.get() == &item;
}

bool connected() const
{
return _item->connected;
}

bool disconnect()
{
if(_signal && _item && _item->connected())
{
return _signal->disconnect(*this);
}
return false;
}
};

template<class... F>
class ScopedSignalConnection : public SignalConnection<F...>
{
public:

ScopedSignalConnection()
{
}

ScopedSignalConnection(Signal<F...>* signal, void* callback)
: SignalConnection<F...>(signal, callback)
{
}

ScopedSignalConnection(const SignalConnection<F...>& other)
: SignalConnection<F...>(other)
{
}

~ScopedSignalConnection()
{
disconnect
();
}

ScopedSignalConnection & operator=(const SignalConnection<F...>& connection)
{
disconnect
();
SignalConnection<F...>::operator=(connection);
return *this;
}
};

Esta implementação do C ++ 11 funciona exatamente da mesma maneira e usa um para compartilhar um . Este ponteiro compartilhado é usado pela conexão e o sinal para marcar a conexão como desconectada, mas não é público. O template de sinal não permite que você retorne valores diferentes de void, o que era um problema , e também implementamos uma classe para um idioma RAII mais simples .boost::signalstd::shared_ptrSignalConnectionItemboost::signalScopedConnection

Ao usar o novo sinal, mudamos a implementação para ocultá-lo inteiramente dos observadores.

class GameScore
{
private:
typedef Signal<unsigned> ChangedSignal;
ChangedSignal _changedEvent;

public:
typedef ChangedSignal::Callback ChangedCallback;
typedef ChangedSignal::Connection ChangedConnection;
typedef ChangedSignal::ScopedConnection ChangedScopedConnection;

GameScore(unsigned value=0) : _value(value)
{
}

void setValue(unsigned value)
{
_value
= value;
_changedEvent
(value);
}

ChangedConnection addObserver(const ChangedCallback& callback)
{
return _changedEvent.connect(callback);
}
};

Desta forma, a implementação do padrão do observador usando o sinal não é mostrada para o exterior. Também adicionamos cada sinal como uma propriedade ao objeto que irá gerar o evento, removendo o cabeçalho gigante da lista de sinais.

Agora, ouvir este evento é muito mais limpo.

class ScoreView
{
private:
GameScore::ScopedConnection _scoreChangedConnection;

public:
GameView(GameScore& score) :
_scoreChangedConnection
(score.addObserver(std::bind(&ScoreView::updateScore, this, std::placeholders::_1)))
{
}

~GameView()
{
// no need to disconnect when using ScopedConnection
}

void updateScore(unsigned score)
{
// update the score view
}
};

Todo o código-fonte desta página Copyright (c) 2016 Miguel Ibero
Licenciado ao abrigo da Licença MIT
https://opensource.org/licenses/MIT