About poor Alexandrescu, I will say a word

    Expensive time of day!
    Book AlexandrescuI recently read one article about Template metaprogramming in C ++. And there was such a comment there : "Exactly the same thing with the same level of customization could be done on interfaces, implementations, factories, on defines, on configs, and on a whole bunch of things." In general, the moral of the article and discussion - these templates from Alexandrescu are not very necessary in life.
    I remembered my task, where the idea of ​​orthogonal design helped me greatly with his (Alexandrescu). I want to share it with you.


    Problem


    I write programs for computers and microcontroller devices. And often I have to organize a connection between them. And, as usual, in programs it is necessary to catch errors. And if it is not very difficult to debug a PC program, then things are much worse with microcontrollers (in the sense, there are all sorts of in-circuit debuggers, but they are not suitable for real-time tasks). Well, that’s not even the point - sometimes you need to debug the connection itself! That is, it seems that the information should go, but it somehow goes wrong ... This is significant at the beginning of the development of the program - when the connection itself, the protocol, etc. are debugged. I was looking for some program to track the exchange via USB, yes somehow could not find. Something caught my eye, but it was all not that (or that, but very paid).

    And suddenly I thought - why shouldn’t I write such a program myself? As a matter of fact, and all business - on the functions of reading / writing, opening / closing we hang up an additional function of writing to an external file. And there, using #define, we disable these functions when they are not needed.

    In parallel with this, I made several useful classes for exchanging data in memory between processes, on local and global networks. And also it was necessary to debug this exchange.

    Both there and there I used a common logical protocol - there is a forbidden character in the message, which is a sign of the beginning / end of the packet.

    And now, this book of Alexandrescu catches my eye. A bell rang in my head that something could stir up something here ...

    Actually, I wanted to make a single base class that implements this same logical protocol. This class also needs to specify the physical nature of the exchange - USB, interprocess exchange (it was in Windows, memory mapping was used), local area network exchange (named pipes), global exchange (sockets). I also wanted to make a debugging function. I also wanted to ...

    Formulation of the problem


    So, we need to make a base class that is set with the following properties (or “policies”):
    • physical nature - what communication method is used. Now I have implemented USB sharing from FTDI, mapping to memory, named pipes, sockets. I would like to add here an exchange on the COM port;
    • The processing method - at the beginning it was just the function “is there an input?”, “Is there an output”, “what is the state of communication?”. Then, when I wrote a program for intensive asynchronous exchange, I implemented a multitask exchange algorithm. So, I now have 2 different policies for the calling method - functional and multithreaded;
    • Exchange protection - do not forget that my program works in a multitasking environment and different threads can simultaneously access the exchange device. Or maybe not. Therefore, we get another “plane” - single-task (in simple cases), protection by critical sections. If necessary, I will make protection with mutexes, but so far there has been no need;
    • Report to file - is it necessary or not? If necessary - in what form? I made an option that writes a delimited text file. It is conveniently exported to Excel and other similar programs;
    • Time measurement - sometimes it is important to know the time intervals between packets. They can be measured in different ways (depending on the task). I took a rough time measurement (GetTicksCount), accurate (high-precision timer on Windows) and ultra-precise ( RDTSC timer ).


    And most importantly, only the first two policies are significant - the nature of communications and the way communications events are handled. Everything else can be ignored. Therefore, I have an “empty” policy - one that does nothing.

    Yes, one more thing - this class does not claim to be a universal solution. We are not talking about the dynamic connection of different methods and classes. All global settings should be implemented in one place of the program - and continue to not change for the entire time of work.

    What happened


    As a result, the InterConnect template class was made:
    template 
    <
    	typename Phys,				// physical layer of connection - shared memory, USB, RS-232, LAN...
    	typename Call,					// call mode - function style, thread with events
    	typename Prot = NoStrategy,		// protection layer - protection based on critical sections, multiprocess protection etc.
    	typename Log = NoStrategy,		// logging mode - no logging (default), to file, to external process
    	typename Timer = NoStrategy		// timer mode - no timer (default), based on GetTickCount, high frequency timer, RDTSC
    >
    class InterConnect {
    protected:
    	Phys	_phys;
    	Call	_call;
    	Prot	_prot;
    	Log		_log;
    	Timer	_timer;
    // ...
    };
    


    In the body of the class, I always check whether this policy is used or not. Here's how, for example, the byte recording function is implemented:
    UPD - there were exceptions, removed

    template 
    bool InterConnectStorage::send_byte (const unsigned char Val)
    {
    	byte val;
    	bool res = true;
    	Prot::ProtectByte pr (_prot);
    	// ...
    	res &= _call.send (_phys, Val);
    	// ...
    	if (Log::to_use ())
    	{
    		if (Timer::to_use ())
    		{
    			_timer.stamp ();
    			_log.send (Val, _timer.dprev (), _timer.dstart (), res);
    		}
    		else
    			_log.send (Val, 0, 0, res);
    		return res;
    	}
    }
    


    Everything seems to be obvious here. From the “protection” policy, we call the byte protection function (in other places, the entire packet is protected). We call the call function from the “exchange organization” policy (we pass the “physical nature” object and, in fact, the byte to it). Then we call from the “report recording” policy a check - is there this policy? If so, write information to the report. If the “time counting” policy is used, we write according to the time (we put the “event” mark with the stamp function, write to the report), no - just to the report.

    Everywhere there is a check - is this policy used or not? The static function bool to_use (void) answers this question. Due to the fact that it is static and very simple, the compiler can optimize the code - either not create it at all (if the policy is not used) or immediately call it without checking.

    As a result, quite cumbersome C ++ code turns into quite simple and optimal in assembler.

    Using


    As a result, I got a very convenient tool that is configured with very little code.

    Here is an example of a small USB sharing program:

    typedef InterConnectStrat::FTDIAccess Phys;			// физический уровень
    typedef InterConnectStrat::ThreadCall  Call;	// интерфейс вызовов
    typedef InterConnectStrat::CritSectProtectPack Prot;	// межпотоковая защита
    typedef InterConnectStrat::NoStrategy Timer;		// нет учета времени
    #if defined (MakeLogFile)
    	typedef InterConnectStrat::TabulatedASCIILogFile Log; // лог в файл
    	File _log;		// файл отчета
    #else
    	typedef InterConnectStrat::NoStrategy Log;		// нет файла отчета
    #endif
    InterConnectStorage  _con;	// устройство для связи
    


    With MakeLogFile constant, I completely change the logic of the program. Recompilation, of course, takes time, but the resulting code is very effective - both in terms of speed and resources, as well as functionality.

    In this code, the intervals between the packages are not important to me, but at the beginning of the development of this program it was necessary. I changed only typedef / * ... * / Timer - and there everything was set.

    Along the way, I worked out the reaction of the program to messages. To do this, I changed the nature of the exchange from USB to interprocess. I made a second console simple application that sent the packages I needed. And so I tuned the reaction of the main program to the right one. Then, again, I changed typedef Phys and set the communication parameters in one place of the program (5 lines maximum) - and everything began to work with USB. Very simple and convenient!

    I also overloaded the main operations - the beginning / end of the packet (this sets, if necessary, the protection mode in a multitasking environment), sending bytes, etc. As a result, the packet is written like this:

    
    	// . версия программы
    	_con++;
    	_con <<= SetProgramDate;
    	_con <<= _HEX_date.get_Unix_time ();
    	--_con;
    	// . переход на область программы
    	_con++;
    	_con <<= GotoProgram;
    	--_con;
    


    The code speaks for itself.

    Appreciate the beauty of the solution! In the whole program, except for the initialization point, I am indifferent to a whole bunch of things - what kind of exchange protocol, whether the program is multi-tasking or not, whether there is an entry in the report or not. Listing is very simple!

    But at the same time, unlike all kinds of class factories, etc., the resulting code is optimal - there are no unnecessary checks and transitions.

    Comparison of other methods


    And now for fun, consider other ways to solve the same problem using C ++ 9x. Tell me in the comments what I missed:

    • Inheritance - unlike templates, guarantees the existence of the necessary functions (which is lacking in templates). But everything else - a dynamic connection, various virtual functions - is not needed here. Moreover, virtual functions cannot be optimized in embedded ones. There is an undoubted bonus for the templates;
    • Factories - I have not yet encountered them. As I understand it, this is a further development of the previous method. In terms of optimization and performance inferior to templates;
    • typedef - probably, all this could be implemented without templates. But the .h-file of templates can be compiled even without the classes used in it, such a number will not work with typedef. And somehow it will be very difficult, on typedef :-);
    • #define - ahem ... I think no comments ;-)


    In general, the tool turned out to be very convenient and of high quality. It allows you to develop your functionality in completely different planes.

    PS Alexandrescu - well done, whatever some would say!

    Also popular now: