Variadic Templates, Low Coupling and Some Thoughts

Every programmer, for sure, was faced with a situation when the application has a set of classes (possibly service) that are used in many parts of the program. And everything seemed to be all right, but as soon as the need arose to change these classes, this could negatively affect the calling code. Yes, as indicated in the title, the article will focus on the very Low Coupling pattern.



The problem is not new and has long been known. There can be several ways to solve it, it all depends on the subject area. I offer the reader a possible solution that I found while doing the applied task. As an idealist, the solution I found was not completely satisfactory. It was also designed to a large extent from the desire to take advantage of the new features of the C ++ 11 standard. Naturally, everything written is subject to discussion, and perhaps someone will offer a more coherent version.

Task statement




There are various devices in the system. For example, temperature sensors, speed sensors, alarms, traffic lights and others. As the reader suggests, all of these devices in the program are represented by heirs from the base class. Of course, each heir has its own specific member functions.
-Everything is as usual. What is the problem?

The problem is that there are several systems. In each such system, devices can vary both in composition and in functionality (expand or, on the contrary, narrow). Say, in one system there is no traffic light, and in the other, everything is the same as in the first, but the alarm has several operation modes. I cited a possible case on the diagram.



It can be seen that the specific implementation SyncBoardfor the first system only redefines the request method for speed, while new methods are still being added for the second system. Thus, the caller needs knowledge of the specific type of each device with which he works. But the caller does not change from system to system. Its basic logic is unchanged, only the ways of interacting with devices change.
-Of course, if the caller needs to use the new functionality of the device, then he simply must know its specific type.

But let's look at it the other way around. There is a certain module / class that uses a type in one system SyncBoard_System1and, respectively, in the second SyncBoard_System2. As I said above, this module / class does not change its logic in these two systems. Only the interaction with the device changes. What are the options?
1) Copy this module and use the new way of interaction. In different systems, use different copies of the modules.
-Thank you, laughed.

2) Make interaction with the device in a separate virtual function. The heirs redefine interaction with the device.
- Better. Only on each device with which the class interacts will you do a virtual function? With new devices, new virtual functions will have to appear in the classes concerned.

3) Encapsulate interaction with the device in a separate class and use it as a strategy.
- It sounds like smart books. But for the new functionality of the device, the strategy must also implement it. That is, the interface of the strategy will change, and then you still need to know the specific implementation (type) of the strategy.

The specifics did not go away, but the number of connections was reduced. Instead of each calling module being tied to a specific device implementation (of which there are many), you can define (a strategy) that will delegate calls to these devices.
-Very similar to Mediator or Facade.

Only outwardly. The common feature of these patterns and my task is that instead of many links, one single link is created with "". For now, let’s dwell on the decision that a class is needed that hides all the specific device implementations and provides an interface to clients for interacting with them.

Search for an implementation option


Option 1 - in the forehead.


The manager stores a list of all devices and provides an appropriate interface.



-And if you have more than one temperature sensor in the system? getTemperature()Which function will return?

Fair remark. It turns out that it is necessary to add a parameter to each such function - the device identifier. Correct.



This option immediately occurred to me, because simplest. And this is perhaps his last plus. Let's imagine what awaits us when adding a new device. In addition to creating the class of the new device itself, we will have to display all its functions in the manager (and delegate them, of course). And if this device is suddenly added immediately to all systems, then also add all these functions to all specific implementations of managers of all systems. "Copy-paste" in its purest form.
-Not beautiful, but not deadly.

Another scenario. The two systems are exactly the same. They differ only in the absence of a single device. In this case, we can no longer use the written manager class, because It contains functions that are not needed. Well, more precisely, we can, but there is an opinion that this is wrong. It remains either to inherit and replace these methods with an empty implementation, or copy the class and remove the excess. Neither one nor the other suited me.

Option 2 - a bunch of case'ov.


But what if we make the manager one single virtual method within which delegation will take place?
- You didn’t forget about different parameters of functions and different return values?

To do this, we will create a base data class through which the calling code will transmit and receive parameters.



Implementation example
classIDevData
{public:
	IDevData();
	virtual ~IDevData();
	virtualintgetID()= 0;
};
classDevManager_v2 :public IDeviceManager
{
public:
	boolinitialize(){
		// очень полезная инициализация
	}
	// супер методvirtualvoidcallMethod(int dev_id, IDevData& data){
		switch (data.getID())
		{
		case DATA_SYNC_BOARD_GET_STATE:
			// вызываем у устройтсва с идентификатором dev_id метод getState();// результат складываем обратно в IDevDatabreak;
		case DATA_SYNC_BOARD_GET_VELOCITY:
			// ...break;
			// ... etc
		}
	}
};


What have we achieved? We now have only one virtual method. All changes occur only in it, when adding / removing devices from the system to the system, unnecessary functions automatically disappear. Caller classes do not need to know the specific manager implementations at all! They only need to know the method callMethodand ...
-Yes Yes! And a specific type IDevDatafor each call. If you ran earlier from binding to specific implementations of devices, then you came to binding to specific implementations of wrappers IDevData. It's funny.

Some kind of vicious circle. We arrived at the same place where we started. Before invoking a single manager method, the caller will need to know exactly what type to IDevDatacreate. And how does this differ from the situation when the caller knew a specific type of device? Yes, nothing!

Option 3 - C ++ 11


callMethod()I liked the idea of ​​a single function . But the problem with passing and returning the parameters reduced all efforts to nothing. It would be great if we could pass any parameters in any quantity to this single function and get any type of return value from it ...
-Yes everyone already understood what you are talking about templates and C ++ 11. Come on, tell how spaceships plow ... ©

The new standard just provides such opportunities. It became clear that the function callMethodshould be boilerplate and have the following prototype:
template <typename ResType, typename ... Args>
ResType callMethod(int dev_id, Args&& ... args);

The calling code now does not need to know anything except the manager, parameter types and return values ​​(which is already taken for granted)!
- And how will you solve the situation when in one class there are two functions identical in signature? Judging by your train of thought, you want to contact somewhere (where?) By dev_id(which means a particular class) and just pass all the parameters to Args&&…someone (to whom?).

Indeed, this creates a problem. There are two options for solving it. Add another parameter - int method_idwhich I don't like at all, or give a different meaning to the parameter - int dev_id. We will call it, say, command_idand now it will mean - a concrete method of a particular class. That is, a certain identifier of the pair is Class-> method. Thus, the values ​​of these command_idwill be exactly the same as the methods of all classes of devices. For good, of course, this needs to be turned into an enumeration, but we will not focus on this. Now about “where to contact command_id” and “to whom to transfer Args&&”. The parameter itself gives us a hint command_id. A certain collection of methods is supposed to be referenced command_id. In other words, the following scheme is needed:
1) Create a repository for any functions with any signature
2) To callMethodremove the command_idobject from the repository by key and pass all the parameters
3) Profit!
-Thanks, Cap.

Point 1 has already been resolved before me. In particular, on a habr there was also an article about type erasure . I read and modified it to my needs. Thanks rpz and other sources .

For those who are too lazy or have no time to re-read, I will briefly tell you how it works. First of all, we need a base class with one useful virtual function for type checking.

classbase_impl 
{public:
	virtualstd::type_info const& getTypeInfo()const= 0;
};


Next, create an heir - a template class. You can pass any function to it. In order not to create different templates for different functions (a class method, or a simple function), I decided to use the ready-made one std::function. All that is required of this class is to overload the operator operator(), into which the parameters for delegating the call are passed.
Type_impl template
template <typename ResType, typename ... Args>
classtype_impl :public base_impl
{
	typedefstd::function<ResType(Args ...)> _Fn;
	_Fn _func;
public:
	type_impl(std::function<ResType(Args ...)> func) : 
		_func(func) {} 
	std::type_info const& getTypeInfo()const{
		returntypeid(_Fn);
	}
	ResType operator()(Args&& ... args){
		return _func(std::forward<Args>(args)...);
	}
};


At the moment, there is already a scheme that allows you to add any function into a container through a pointer to a class base_impl. But how to get to the call through this pointer operator()? Type conversion required. For this we have a method getTypeInfo(). To hide this ins and outs, as well as the need to manually write a template every time you add a function to the container type_impl, create the last class with one little trick - a template constructor.
Funcwrapper
classFuncWrapper
{std::unique_ptr<base_impl> holder;
public:
	template <typename ResType, typename ... Params>
	FuncWrapper(std::function<ResType(Params ...)> func) : 
		holder(new type_impl<ResType, Params ...>(func)) {}
	~FuncWrapper() {}
	template <typename ResType, typename ... Args>
	ResType call(Args&& ... args){
		typedefstd::function<ResType(Args ...)> _Fn;
		if (holder->getTypeInfo() != typeid(_Fn))
			throwstd::exception("Bad type cast");
		type_impl<ResType, Args ...>* f = 
			static_cast<type_impl<ResType, Args ...>*>(holder);	
		return (*f)(std::forward<Args>(args)...);		}
	}
};


We add a template method to it call()and in it we delegate the call to the saved one type_impl.
Usage example
classFuncWrapper
{private:
	// базовый класс для обертки над callable object.// необходим только для того, чтобы иметь указатель на любой типclassbase_impl 
	{public:
		virtualstd::type_info const& getTypeInfo()const= 0;
	};
	// в этом классе хранится секрет фокуса. // объявляем шаблонный класс - наследник от base_impl. // теперь чем бы мы не параметризировали type_impl, его ссылку// всегда можно присвоить указателю base_impl// параметризуется шаблон возвращаемым значением функции - ResType// и аргументами функции - Args...template <typename ResType, typename ... Args>
	classtype_impl :public base_impl
	{
		typedefstd::function<ResType(Args ...)> _Fn;
		_Fn _func; // тут то и хранится переданный нам колбек!public:
		// я не стал изголяться, и сразу решил в конструктор отправлять std::function// зачем мне реализовывать функционал std::function самому?
		type_impl(std::function<ResType(Args ...)> func) : 
			_func(func) {}
		// та самая функция предка, которая выполняет проверку типов.// с ее помощью, мы гарантируем корректное приведение типов.// ну или exception.std::type_info const& getTypeInfo()const{
			returntypeid(_Fn);
		}
		// ну а тут, очевидно, идет делегирование вызова сохраненному колбеку.ResType operator()(Args&& ... args){
			return _func(std::forward<Args>(args)...);
		}
	};
	std::unique_ptr<base_impl> holder;
public:
	// чудо конструктор, в который мы передаем свжеиспеченную std::function// сами знаете, что в нее можно уложить что угодноtemplate <typename ResType, typename ... Params>
	FuncWrapper(std::function<ResType(Params ...)> func) : 
		holder(new type_impl<ResType, Params ...>(func)) {}
	~FuncWrapper() {}
	// шаблонный метод, через который получаем доступ к сохраненной функцииtemplate <typename ResType, typename ... Args>
	ResType call(Args&& ... args){
		typedefstd::function<ResType(Args ...)> _Fn;
		if (holder->getTypeInfo() != typeid(_Fn))
			throwstd::exception("Bad type cast");
		// если типы совпали, то делегируем вызов
		type_impl<ResType, Args ...>* f = 
			static_cast<type_impl<ResType, Args ...>*>(holder.get());	
		return (*f)(std::forward<Args>(args)...);
	}
};
// предположим, что класс test, какое-то устройствоclasstest
{public:
	test() {}
	intfn1(int a){
		cout << "test::fn1!!! " << a << endl;
		return ++a;
	}
	intfn2(int a, int b){
		cout << "test::fn2!!! " << a << endl;
		return a + 2;
	}
	intfn3(int a, int b){
		cout << "test::fn3!!! " << a << endl;
		return a + 3;
	}
};
classIDeviceManager
{protected:
	std::map<int, FuncWrapper*> m_funcs;
public:
	virtual ~IDeviceManager() {};
	virtualvoidinitialize()= 0;
	template <typename ResType, typename ... Args>
	ResType callMethod(int command_id, Args&& ... args){
		// очень не аккуратный код – только для демонстрации идеи.return m_funcs[command_id]->call<ResType>(std::forward<Args>(args)...);
	}
};
constint FN1_ID = 0;
constint FN2_ID = 1;
constint FN3_ID = 2;
classDevManager_v3 :public IDeviceManager
{
	std::unique_ptr<test> m_test_ptr;
public:
	voidinitialize(){
		// очень полезная инициализация
		m_test_ptr.reset(new test);
		std::function<int(int)> _func1 = std::bind(&test::fn1, m_test_ptr.get(), std::placeholders::_1);
		std::function<int(int, int)> _func2 = std::bind(&test::fn2, m_test_ptr.get(), std::placeholders::_1, std::placeholders::_2);
		std::function<int(int, int)> _func3 = std::bind(&test::fn3, m_test_ptr.get(), std::placeholders::_1, std::placeholders::_2);
		// складываем в коллекцию все методы
		m_funcs[FN1_ID] = new FuncWrapper(_func1);
		m_funcs[FN2_ID] = new FuncWrapper(_func2);
		m_funcs[FN3_ID] = new FuncWrapper(_func3);
	}
	~DevManager_v3()
	{
		// тут подчищаем коллекцию
	}
};
int _tmain(int argc, _TCHAR* argv[])
{
	DevManager_v3 dev_manager;
	dev_manager.initialize();
	// Вуая-ля! К любому методу можно обратится через его идентификатор.
	dev_manager.callMethod<int>(FN1_ID, 1);
	dev_manager.callMethod<int>(FN2_ID, 2, 3);
	dev_manager.callMethod<int>(FN3_ID, 4, 5);
	getchar();
}


Wow! We now have one single virtual method initialize()that creates all the necessary devices for a given system and puts their methods in a collection. The caller does not even need to know the specific type of manager. The template method callMethod()will do everything for us. For each specific system, the desired instance is created IDevManagerusing, say, the <factory> . The caller needs only a pointer to the ancestor IDevManager.
- It seems you have reached your goal.

Yes, but new shortcomings appear and they, perhaps, have more significant negative consequences, in comparison with the first options. The code is not safe!
First, look carefully at callMethod(). If we pass a key that is not in the collection, we will get an exception. Of course, you must first check whether this key is in the collection. But what to do when it turns out that the key does not exist (a non-existent method was requested)? Throw an exception? And most importantly, at the compilation stage we cannot catch this. This can happen when a device is absent in some system, or part of its methods.
Secondly, the code editor will not tell you what parameters are expected at the inputcallMethod()- will not display the name / type / number of parameters. If we pass the wrong type of parameter, or the wrong number of parameters, an exception will be expected again, but already in the call()class method test_impl. And again, we cannot catch this at the compilation stage. This can easily happen due to the carelessness of the programmer.

With regard to the task, this did not suit me, for the following reasons:
- At the compilation stage, the exact number of classes (respectively methods) that need access are always known.
- These classes change only when designing different systems.
Therefore, I had to start from scratch.
- "Shaw, again ?!" ©


Option 4 - the final?


I came to him, seeing a very simple design:
template <class ... T>
classDissembler :public T ...
{
};

What's going on here? Just multiple inheritance. But that seems to be what I need. If I inherit from all my device classes, I automatically get all their methods in this class. The disadvantages of the third option disappear.
- Holy simplicity. This will work for you until you have to inherit from two identical classes. Or in two classes there will be identical functions (diamond-shaped inheritance).

So you need to somehow make the methods unique, even if they have the same signature. And I knew where the clue is. Thank you Andrey !
It's simple here
template <typename T, T t>
structValueToType 
{};
template<typename C, classT>
classClassifiedWrapper
{};
template<typename C, C c, classT>
classClassifiedWrapper<ValueToType<C, c>, T> :private T
{
public:
	ClassifiedWrapper(T&& fn) : T(std::forward<T>(fn)) {};
	ClassifiedWrapper() = delete;
	virtual ~ClassifiedWrapper() {};
	template <typename ... Args>
	std::result_of<T(Args...)>::type 
	operator()(ValueToType<C, c>&&, Args&& ... args){
		return T::operator()(std::forward<Args>(args) ...);
	};
};


The class ValueToTypeserves one purpose - to set the type depending on the value of the template parameter. A class ClassifiedWrapperis another wrapper over a “callable object”. Its purpose is to inherit from an object for which the bracket operator is defined operator(), and to delegate the call, but with an additional parameter that introduces “uniqueness”. I.e:
Example
classtest
{public:
	test() {}
	intfn1(int a){
		cout << "test::fn1!!! " << a << endl;
		return ++a;
	}
	intfn2(int a, int b){
		cout << "test::fn2!!! " << a << endl;
		return a + 2;
	}
	intfn3(int a, int b){
		cout << "test::fn3!!! " << a << endl;
		return a + 3;
	}
};
int _tmain(int argc, _TCHAR* argv[])
{
test t;
std::function<int(int, int)> _func2 = std::bind(&test::fn2, &t, std::placeholders::_1, std::placeholders::_2);
	ClassifiedWrapper<ValueToType<int, 0>, decltype(_func2)> cw1(std::ref(_func2));
	ClassifiedWrapper<ValueToType<int, 1>, decltype(_func2)> cw2(std::ref(_func2));
	cw1(ValueToType<int, 0>(), 2, 1); // ОК
	cw2(ValueToType<int, 1>(), 3, 4); // ОК
 	cw1(ValueToType<int, 0>(), 1); // не скомпилируется
 	cw2(ValueToType<short, 1>(), 3, 4); // не скомпилируется
}


The function is the same, but we made two of its wrappers different using an additional parameter.
- And what to do with this good?

As a result, we have a class that allows you to wrap any function and make it unique. Remember the original problem, where did this option begin? Now you can apply the same trick with multiple inheritance, but inherit from CalssifiedWrapper.
First, declare a blank:
template <class ... T>
classDissembler
{};


Next, we will make a partial specialization, which will simultaneously start the recursion of the expansion of the parameter package
template <typename C, C c, classFunc, class ... T>
classDissembler<ValueToType<C, c>, Func, T ...> :protected ClassifiedWrapper<ValueToType<C, c>, Func>, protected Dissembler<T ...>
// тут стоит поянсить, что Dissembler<T ...> раскрывается снова в пару ValueToType<C, c> и Func и остаток T ..., который может быть и пустым. И так далее, вплоть до замыкания (ниже)
{
protected:
	using ClassifiedWrapper<ValueToType<C, c>, Func>::operator();
	using Dissembler<T ...>::operator();
public:
	Dissembler(ValueToType<C, c>&& vt, Func&& func, T&& ... t) :
		ClassifiedWrapper<ValueToType<C, c>, Func>(std::forward<Func>(func)),
		Dissembler<T ...>(std::forward<T>(t) ...) {};
// нельзя создать с конструктором по умолчанию
	Dissembler() = delete;
	virtual ~Dissembler() {};
	// основной метод, который и делает всю грязную работу.template <typename Cmd, Cmd cmd, typename ... Args>
	std::result_of<Func(Args...)>::type
	call(Args&& ... args){
// здесь мы опять просто делегируем вызов, но уже ClassifiedWrapper’у.// точнее не делегируем, это же теперь наш собственный унаследованный методreturnthis->operator()(ValueToType<Cmd, cmd>(), std::forward<Args>(args)...);
	};
};


Well, it remains only to make the recursion stop
template <typename C, C c, classFunc>
classDissembler<ValueToType<C, c>, Func> :protected ClassifiedWrapper<ValueToType<C, c>, Func>
{
public:
	Dissembler(ValueToType<C, c>&&, Func&& func) :
		ClassifiedWrapper<ValueToType<C, c>, Func>(std::forward<Func>(func)) {};
	Dissembler() = delete;
	virtual ~Dissembler() {};
};


- You will break your eyes. Can you explain on fingers and understandably?

The basic idea is simple - multiple inheritance. But as soon as we meet the previously mentioned problems (two identical classes in the inheritance chain or diamond-shaped inheritance), then everything stops working. To do this, we create a class ( ClassifiedWrapper) that can, as it were, “assign a unique label” (in fact, it does not attribute anything, I put it so beautifully) of any function. At the same time, it ClassifiedWrapperis itself , of course, unique (again, it is clear that with different template parameters). Next, we simply create a “static list” of such unique functions wrapped in ClassifiedWrapper, and inherit from all of them. Fuh, I’ll probably not be able to explain it easier. In general, the focus I applied with the Variadic Template is described in many places. In particular Habré .
-And why is there no method in recursion closure call?

Because it makes no sense to fence the garden around one single function. That is, if someone wants to use Dissemblernot for many functions, but for one - then this idea does not make sense. Here is how it is supposed to use all this economy:
How to
int _tmain(int argc, _TCHAR* argv[])
{
	test t;
	std::function<int(int, int)> _func2 = std::bind(&test::fn2, &t, std::placeholders::_1, 
		std::placeholders::_2);
	std::function<int(int, int)> _func3 = std::bind(&test::fn3, &t, std::placeholders::_1,
		std::placeholders::_2);
	Dissembler<
		ValueToType<int, 0>, decltype(_func2),
		ValueToType<char, 'a'>, decltype(_func3)
	> dis(
		ValueToType<int, 0>(), std::move(_func2), 
		ValueToType<char, 'a'>(), std::move(_func3)
		);
	dis.call<int, 0>(0, 1);
	dis.call<char, 'a'>(0, 1);
	getchar();
}


I intentionally pointed out two different types of "identifier" of functions for demonstration - ValueToType<int, 0>and ValueToType<char, ‘a’>. In the real problem, instead of the "incomprehensible" int and method number, it is much more visual to use enumerations with intelligible element names. Everything works quite clearly - we indicate the calltype of identifier and its value with the parameters of the template function , and pass the parameters.
Compared with the previous version, it was possible to achieve that an error in the number of parameters or in the key value leads to compile-time errors. So, for the final goal, when the number of classes is known in advance and does not change during program execution, the problem is solved.
Of course, for an inexperienced eye, as well as a person who does not like templates, seeing a generated compilation error will be a shock (in the case of incorrectly specified parameters). But this ensures that the error will attract attention and it will not happen at run-time, and this, I think, is worth the work done.
-And how will yours look in the end IDevManager?

Almost the same as in option 3, but with small differences.
Example
typedef	Dissembler<
		ValueToType<int, 0>, std::function<int(int, int)>,
		ValueToType<int, 1>, std::function<int(int, int)>
		> SuperWrapper;
classIDeviceManager
{protected:
	std::unique_ptr<SuperWrapper> m_wrapperPtr;
public:
	virtual ~IDeviceManager() {};
	virtualvoidinitialize()= 0;
	template <int command_id, typename ResType, typename ... Args>
	ResType callMethod(Args&& ... args){
		return m_wrapperPtr->call<int, command_id>(std::forward<Args>(args)...);
	}
};
classDevManager_v4 :public IDeviceManager
{
	std::unique_ptr<test> m_test_ptr;
public:
	voidinitialize(){
		// очень полезная инициализация
		m_test_ptr.reset(new test);
		std::function<int(int, int)> _func2 = std::bind(&test::fn2, m_test_ptr.get(), std::placeholders::_1, std::placeholders::_2);
		std::function<int(int, int)> _func3 = std::bind(&test::fn3, m_test_ptr.get(), std::placeholders::_1, std::placeholders::_2);
		m_wrapperPtr.reset(new SuperWrapper(
			ValueToType<int, 0>(), std::move(_func2),
			ValueToType<int, 1>(), std::move(_func3)
			));
	}
};
int _tmain(int argc, _TCHAR* argv[])
{
	DevManager_v4 v4;
	v4.initialize();
	v4.callMethod<1, int>(0, 1);
	v4.callMethod<0, int>(10, 31);
	getchar();
}


Definitions SuperWrapper(for each system its own) will have to be put in a separate header file. And to separate each definition with #ifdef'so that the correct one is connected in the right project SuperWrapper. It is for this reason that I put a question mark when writing option 4. Final?

Also popular now: