📜 ⬆️ ⬇️

Variadic Templates, Low Coupling and a bit of thinking

Each programmer must have come across a situation where the application has a set of classes (possibly service ones) that are used in many parts of the program. And everything seems to be nothing, but as soon as it became necessary to change these classes, it could negatively influence the calling code. Yes, as indicated in the title, the article will discuss the very pattern of “Low Coupling”.



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

Task statement



')
There are various devices in the system. For example, temperature sensors, speed sensors, alarms, traffic lights and others. As the reader assumes, all these devices in the program are represented by the 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, the devices may differ both in composition and in functionality (expand or vice versa, narrow). For example, in one system there is no traffic light, and in the other, everything is the same as in the first one, but the alarm has several operation modes. I gave a possible case in the diagram.



It can be seen that the specific implementation of SyncBoard for the first system only overrides the speed request method, while new methods are 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 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 from the other side. There is a certain module / class, which in one system uses the type SyncBoard_System1 , and in the second, respectively, SyncBoard_System2 . As I said above, this module / class does not change its logic in these two systems. Changing only the interaction with the device. 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) To make interaction with the device in a separate virtual function. In successors to redefine interaction with the device.
- Already better. Only you on each device with which the class interacts will be done by a virtual function? With new devices, new virtual functions will have to appear in interested classes.

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

Concrete is not gone, but the number of links is reduced. Instead of each caller being tied to a specific device implementation (of which there are many), you can define (strategy) that will delegate the calls to these devices.
- Very similar to Mediator or Facade.

Only externally. The common feature of these patterns and my task is that instead of a multitude of links, one single link is created with "". For now, we’ll dwell on the decision that a class is needed that hides in itself all concrete device implementations and provides an interface to clients for interacting with them.

Search for implementation options


Option 1 - in the forehead.


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



-And if you have more than one temperature sensor in your system? Function getTemperature() what will return?

Fair remark. It turns out that it is necessary to add a parameter to each such function - a device identifier. Let's fix it.



This variant immediately came to my mind, since simplest. And this is probably 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 need to display all its functions in the manager (and delegate them, of course). And if this device is suddenly added to all systems at once, then it will also add all these functions to all specific implementations of the managers of all systems. "Copy-paste" in its purest form.
-Not beautiful, but not deadly.

Another scenario. The two systems are exactly the same. Differ only in the absence of a single device. In this case, we can no longer use the written manager class, since it contains functions that are not needed. Well, we can be more exact, but there is an opinion that this is wrong. It remains to either inherit and replace these methods with an empty implementation, or copy the class and delete the unnecessary. Neither one nor the other did not suit me.

Option 2 - a bunch of cases.


But what if we make the manager a single virtual method within which delegation will take place?
- You have not forgotten about the different parameters of the functions and different return values?

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



Implementation example
 class IDevData { public: IDevData(); virtual ~IDevData(); virtual int getID() = 0; }; class DevManager_v2 : public IDeviceManager { public: bool initialize() { //    } //   virtual void callMethod(int dev_id, IDevData& data) { switch (data.getID()) { case DATA_SYNC_BOARD_GET_STATE: //      dev_id  getState(); //     IDevData break; 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 disappear automatically. The calling classes do not need to know the specific implementations of the manager at all! They only need to know the callMethod method and ...
-Yes Yes! And the specific type of IDevData for each call. If you ran earlier from binding to specific implementations of devices, then you came to linking to specific implementations of IDevData wrappers. Funny.

Some kind of vicious circle. We came to the same with which we started. Before calling a single method on a manager, the caller will need to know exactly what type of IDevData create. And how does this differ from the situation where the caller knew a particular type of device? Yes, nothing!

Option 3 - C ++ 11


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

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

The caller now needs to know nothing, except for the manager, the types of parameters and the returned values ​​(which is already taken for granted)!
- And how will you solve the situation when in the same class there are two functions identical in signature? Judging by your thinking, you want to go somewhere (where?) To dev_id (which means a particular class) and just pass all the parameters of Args&&… someone (to whom?).

Indeed, this creates a problem. There are two options to solve it. Add another parameter - int method_id , which I don’t like at all, or give a different meaning to the parameter - int dev_id . Let's call it, say, command_id , and now it will mean a specific method of a particular class. That is, a certain pair identifier Class-> method. Thus, the values ​​of these command_id will be exactly the same as the methods of all classes of devices. For good, of course, this should be turned into an enumeration, but we will not dwell on this. Now, about "where to go with command_id " and "who to send to Args&& ". The command_id parameter gives us a hint. A certain collection of methods is supposed, which is accessed by command_id . In other words, the following scheme is needed:
1) Create a repository for any functions with any signature.
2) In the callMethod remove the required object from the storage using the command_id key and transfer all parameters
3) Profit!
-Thanks, Cap.

Point 1 has already been resolved before me. In particular, on Habré there was also an article about type erasure . I read and slightly modified to fit 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.

 class base_impl { public: virtual std::type_info const& getTypeInfo() const = 0; }; 


Further, we create the successor - a sample class. Any function can be passed to it. In order not to create different templates for different functions (a class method, or a simple function), I decided to use the already prepared std::function . All that is required of this class is to overload the operator() , into which the parameters for the delegation of the call are passed.
Template type_impl
 template <typename ResType, typename ... Args> class type_impl : public base_impl { typedef std::function<ResType(Args ...)> _Fn; _Fn _func; public: type_impl(std::function<ResType(Args ...)> func) : _func(func) {} std::type_info const& getTypeInfo() const { return typeid(_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 to the container through a pointer to the base_impl class. But how to get to the operator() call through this pointer? A type conversion is required. For this we have a method getTypeInfo() . To hide this underling, as well as the need to manually write each time, when adding a function to a container, the type_impl template, create the last class with one little trick - the template designer.
FuncWrapper
 class FuncWrapper { 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) { typedef std::function<ResType(Args ...)> _Fn; if (holder->getTypeInfo() != typeid(_Fn)) throw std::exception("Bad type cast"); type_impl<ResType, Args ...>* f = static_cast<type_impl<ResType, Args ...>*>(holder); return (*f)(std::forward<Args>(args)...); } } }; 


We add to it the sample call() method and in it we delegate the call to the saved type_impl .
Usage example
 class FuncWrapper { private: //      callable object. //    ,       class base_impl { public: virtual std::type_info const& getTypeInfo() const = 0; }; //      . //    -   base_impl. //       type_impl,   //     base_impl //      - ResType //    - Args... template <typename ResType, typename ... Args> class type_impl : public base_impl { typedef std::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 { return typeid(_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) { typedef std::function<ResType(Args ...)> _Fn; if (holder->getTypeInfo() != typeid(_Fn)) throw std::exception("Bad type cast"); //   ,    type_impl<ResType, Args ...>* f = static_cast<type_impl<ResType, Args ...>*>(holder.get()); return (*f)(std::forward<Args>(args)...); } }; // ,   test, -  class test { public: test() {} int fn1(int a) { cout << "test::fn1!!! " << a << endl; return ++a; } int fn2(int a, int b) { cout << "test::fn2!!! " << a << endl; return a + 2; } int fn3(int a, int b) { cout << "test::fn3!!! " << a << endl; return a + 3; } }; class IDeviceManager { protected: std::map<int, FuncWrapper*> m_funcs; public: virtual ~IDeviceManager() {}; virtual void initialize() = 0; template <typename ResType, typename ... Args> ResType callMethod(int command_id, Args&& ... args) { //     –    . return m_funcs[command_id]->call<ResType>(std::forward<Args>(args)...); } }; const int FN1_ID = 0; const int FN2_ID = 1; const int FN3_ID = 2; class DevManager_v3 : public IDeviceManager { std::unique_ptr<test> m_test_ptr; public: void initialize() { //    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() , which creates all the necessary devices for this system and places their methods in the collection. The caller does not even need to know the specific type of manager. The callMethod() method callMethod() does everything for us. For each specific system, the desired IDevManager instance is IDevManager using, say, <factory> . The caller needs only an ancestor IDevManager .
- It seems you have achieved your goal.

Yes, but there are new flaws 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 get an exception. Of course, you must first check whether the key is in the collection. But what to do when it turned out that the key does not exist (a method that does not exist is requested)? Generate an exception? And most importantly, at the compilation stage, we will not be able to catch this. This can happen when a device is missing in some system, or part of its methods.
Secondly, the code editor will not tell you what parameters are expected at the input of callMethod() - it will not highlight the name / type / number of parameters. If we pass the wrong type of parameter, or the wrong number of parameters, an exception is waiting for us again, but in the call() method of the test_impl class. And again, we can't catch it at compile time. This can easily happen because of a programmer’s inattention.

With regard to the task, it did not suit me, for the following reasons:
- At the compilation stage, the exact number of classes (respectively methods) to which access is always known.
- These classes change only when designing different systems.
So I had to start from scratch.
- “Sho, again ?!” ©


Option 4 - the final?


I came to him after seeing a very unpretentious construction:
 template <class ... T> class Dissembler : public T ... { }; 

What's going on here? Only multiple inheritance. But it seems that this is what I need. If I inherit from all my device classes, I automatically have all their methods in this class. The disadvantages of the third option disappear.
- Holy simplicity. It 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 was. Thank you, Andrew !
It's simple
 template <typename T, T t> struct ValueToType {}; template<typename C, class T> class ClassifiedWrapper {}; template<typename C, C c, class T> class ClassifiedWrapper<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 ValueToType class serves one purpose - to set the type depending on the value of the template parameter. The ClassifiedWrapper class is another wrapper over a “callable object”. Its goal is to inherit from an object that has the operator operator() defined, and delegate the call, but with an additional parameter that introduces “uniqueness”. I.e:
Example
 class test { public: test() {} int fn1(int a) { cout << "test::fn1!!! " << a << endl; return ++a; } int fn2(int a, int b) { cout << "test::fn2!!! " << a << endl; return a + 2; } int fn3(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 used is the same, but we made two of its wrappers different with 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, how did this option start? Now you can apply the same focus with multiple inheritance, but inherit from CalssifiedWrapper .
We first declare the stock:
 template <class ... T> class Dissembler {}; 


Next, we will make a partial specialization, which simultaneously starts the recursion of the expansion of the parameter package.
 template <typename C, C c, class Func, class ... T> class Dissembler<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'. //   ,        return this->operator()(ValueToType<Cmd, cmd>(), std::forward<Args>(args)...); }; }; 


Well, it remains only to make the recursion stop
 template <typename C, C c, class Func> class Dissembler<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'll break your eyes. And on the fingers and clear understand you can?

The idea at the core is simple - multiple inheritance. But as soon as we encounter the previously mentioned problems (two identical classes in the chain of inheritance or rhomboid inheritance), then everything stops working. To do this, we start a class ( ClassifiedWrapper ), which can, as it were, “attribute a unique label” (in fact, it does not attribute anything, I put it so beautifully) to any function.At the same time, he himself ClassifiedWrapperis naturally 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, easier, probably can not explain. In general, the focus I applied with the Variadic Template, many of which are described. In particular on Habré .
-And why is there no method in the closure of recursion call?

Because it makes no sense to make a garden around one single function. That is, if someone wants to use Dissemblernot for a variety of functions, but for one, then this idea does not make sense. Here's how to use all this household:
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 deliberately pointed out two different types of "identifier" of functions for the demonstration - ValueToType<int, 0>and ValueToType<char, 'a'>. In the real problem, instead of the “incomprehensible” int and method numbers, it is much clearer to use enumerations with intelligible names of elements. Everything works quite clearly - we specify the calltype of the identifier and its value by the parameters of the template function , and we pass the parameters.
In comparison with the previous version, it was possible to ensure that an error in the number of parameters or in the key value leads to compile-time errors. So, for the ultimate goal, when the number of classes is known in advance and does not change during program execution, the problem is solved.
Of course, the inexperienced eye, as well as the person who dislikes the templates, will be shocked to see the generated compilation error (in 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 like 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; class IDeviceManager { protected: std::unique_ptr<SuperWrapper> m_wrapperPtr; public: virtual ~IDeviceManager() {}; virtual void initialize() = 0; template <int command_id, typename ResType, typename ... Args> ResType callMethod(Args&& ... args) { return m_wrapperPtr->call<int, command_id>(std::forward<Args>(args)...); } }; class DevManager_v4 : public IDeviceManager { std::unique_ptr<test> m_test_ptr; public: void initialize() { //    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), you have to put in a separate header file. And to separate each definition #ifdef"s, so that the" correct "is connected in the necessary project SuperWrapper. It is for this reason that I put a question mark when writing option 4. Final?

Source: https://habr.com/ru/post/231021/


All Articles