Skip to content

TutorialAdvanced

Dmitry Koplyarov edited this page May 18, 2016 · 15 revisions

Back to tutorial

Table of contents

Template policies

Some wigwag clases are highly customizable due to the use of policy-based design:

You may get detailed information about the policies and their implementations at the reference page.

signal and listenable policies

Using a non-default policy implementation

Often, you may find a more suitable implementation for some policy. A good example is exception handling. The default policy does not catch any exceptions, so an error in one handler may prevent other from being invoked. It's much better to catch exceptions and log them. print_to_stderr implementation fits you perfectly if you use only std::exception subclasses and the stderr logging is OK for you.

signal<void(), exception_handling::print_to_stderr> s;
token_pool tokens;

tokens += s.connect([] { throw std::runtime_error("test exception"); });

s();

Output:

Uncaught std::exception: test exception

Developing your own policy implementation

Sometimes you may want to make your own policy implementation. There is a section in each policy's reference that describes how to implement it.

Here's a custom implementation of exception_handling policy that uses a logger instead of std::cerr:

struct log_exceptions
{
	using tag = wigwag::exception_handling::tag<wigwag::api_version<2, 0>>;

	template < typename Func_, typename... Args_ >
	void handle_exceptions(Func_&& func, Args_&&... args) const
	{
		try
		{ func(std::forward<Args_>(args)...); }
		catch (const std::exception& ex)
		{ Logger::Log(LogLevel::Error) << "Uncaught std::exception: " << ex.what(); }
	}
};

// ...

wigwag::signal<void(), log_exceptions> s;
s(); // Use the signal

Template aliases for your signal configuration

Usually, you don't really need a variety of signals in your project, so it's convenient to use template aliases for several configurations that you use.

namespace my_project
{
	template < typename Signature_>
	using signal = wigwag::signal <
			Signature_, 
			wigwag::exception_handling::print_to_stderr
		>;

	template < typename Signature_>
	using ui_signal = wigwag::signal <
			Signature_, 
			wigwag::exception_handling::print_to_stderr,
			wigwag::threading::none,
			wigwag::state_populating::none,
			wigwag::life_assurance::none,
			wigwag::creation::lazy,
			wigwag::ref_counter::single_threaded
		>;
}

Now you have my_project::signal to use in the multithreaded my_project core and myproject::ui_signal that is perfectly suitable for UI due to minimal overhead:

class FileSystemListener
{
public:
	signal<void(const FilePath&)> OnFileAttributesUpdated;

	// ...
};
class Button
{
public:
	ui_signal<void()> OnClick;

	// ...
};

Signal constructors

Signal constructors have some variadic arguments, which are forwarded to the policies' internals if they have constructors that accept the passed types:

Single argument version: If you pass a single argument to a signal constructor, it is passed to one of these objects.

Two arguments version: If you pass two arguments to a signal constructor, they are passed to one of these pairs. The first argument is passed to the first object in pair, and the second argument is passed to the second.

Three arguments version: If you pass three arguments to a signal constructor, they are passed to these three objects correspondingly.

The default signal specialization has only one policy that may accept constructor arguments: the state_populating. Thus, you have only one option from all the above: handler_processor, which effectively accepts a populator function.

However, you may use the policies that may require some parameters to be initialized (e.g. shared_mutex that needs a shared_ptr to the mutex). In this case you should use one of these signal constructors.

For example, if you want to share a single mutex between several signals, and at the same time you need populators, you would write something like this:

class MediaScanner
{
private:
	MediaScannerState              _state;
	std::vector<MediaPtr>          _media;
	std::shared_ptr<std::mutex>    _mutex;
	// ...

public:
	MediaScanner()
		: _state(MediaScannerState::Idle),
		  _mutex(std::make_shared<std::mutex>()),
		  OnStateChanged(_mutex, [&](const decltype(OnStateChanged)::handler_type& h) { h(_state); }),
		  OnMediaFound(_mutex, [&](const decltype(OnMediaFound)::handler_type& h) { for (auto&& m : _media) h(m); })
	{ }

	wigwag::signal<void(MediaScannerState), wigwag::threading::shared_mutex> OnStateChanged;
	wigwag::signal<void(const MediaPtr&), wigwag::threading::shared_mutex> OnMediaFound;

	// ...
};

Here you invoke the signal constructor with two arguments, and the signal forwards them to the (lock_primitive, handler_processor) pair because lock_primitive has a constructor that accepts the std::shared_ptr<std::mutex> type, and handler_processor has a constructor that accepts the std::function<void(const HandlerType_&)> type.

Signal connectors

Since a signal in your particular class may use some random policies (e.g. share a mutex between several signals) and also have a non-default constructor, it's usually a bad idea to expose specific signal instantiations in abstract classes aka interfaces.

Interface should only have something that allows connecting a handler, not the implementation details. This entity is called signal_connector.

You should define a signal_connector getter in your interface, move the signal member to private in the subclass, and return signal::connector() from the getter implementation.

enum class VideoDecoderState { NotInitialized, DecodingOk, DecodingError };

struct IVideoDecoder
{
	virtual ~IVideoDecoder() { }
	virtual wigwag::signal_connector<void(VideoDecoderState)> OnStateChanged() const = 0;

	// ...
};

class FfmpegVideoDecoder : public IVideoDecoder
{
private:
	std::shared_ptr<std::mutex>    _mutex;
	// ...

	wigwag::signal<void(VideoDecoderState),
		wigwag::exception_handling::print_to_stderr,
		wigwag::threading::shared_mutex>    _onStateChanged;

public:
	virtual wigwag::signal_connector<void(VideoDecoderState)> OnStateChanged() const
	{ return _onStateChanged.connector(); }

	// ...
};

The only thing that changes in the client code is that you have to invoke a getter to obtain the signal_connector object, so you should write the additional parentheses.

IVideoDecoderPtr vdec = //...
token t = vdec->OnStateChanged().connect(/*...*/);

Passing resource ownership and the withdrawers

Sometimes you may want to grant some limited resource ownership via signals or listenables.

class Resource;
using ResourcePtr = std::shared_ptr<Resource>;


class ResourceRequestSession
{
public:
	wigwag::signal<void(const ResourcePtr&)> OnResourceGranted;

	// ...
};


class ResourceUser
{
private:
	ResourceRequestSession _resourceRequest;
	ResourcePtr            _resource;
	wigwag::token          _token;

public:
	ResourceUser()
	{
		// Atomically connecting and obtaining the resource if it has already been granted
		_token = _resourceRequest.OnResourceGranted.connect(
			std::bind(&ResourceUser::ResourceHandler, this, std::placeholders::_1));
	}

	~ResourceUser()
	{
		_token.reset();
		ResourceHandler(nullptr);
	}

private:
	void ResourceHandler(const ResourcePtr& resource)
	{
		_resource = resource;
		if (_resource)
			std::cout << "Resource granted" << std::endl;
		else
			std::cout << "Resource withdrawed" << std::endl;
	} 
};

Here you have a race condition similar to the one we solved with the populators.

~ResourceUser()
{
	_token.reset();
	// If the ResourceRequestSession tries to withdraw the resource between these two lines, it doesn't wait until we actually stop using it
	ResourceHandler(nullptr);
}
~ResourceUser()
{
	ResourceHandler(nullptr);
	// If a resource ownership is granted between these two lines, we will not even release it
	_token.reset();
}

The solution is also very similar to the populators: the user-defined function that is invoked when a handler is being disconected: the withdrawer.

using populator_and_withdrawer = wigwag::state_populating::populator_and_withdrawer; 

template < typename Signature_ >
using resource_signal = wigwag::signal<Signature_, populator_and_withdrawer>;

class ResourceRequestSession
{
private:
	ResourcePtr _resource;

public:
	ResourceRequestSession()
		: OnResourceGranted(populator_and_withdrawer(
			[](const std::function<void(const ResourcePtr&)>& h) { h(_resource); },
			[](const std::function<void(const ResourcePtr&)>& h) { h(nullptr); }
		))
	{ }

	resource_signal<void(const ResourcePtr&)> OnResourceGranted;

	// ...
};

By invoking the token::reset method you disconnect from a signal and release the resource ownership atomically.

~ResourceUser()
{
	_token.reset(); // No more race condition
}

Clone this wiki locally