Pull to refresh

О бедном Александреску замолвлю я слово

Reading time6 min
Views20K
Дорого времени суток!
Книга АлександрескуПрочитал я недавно статью одну про Template metaprogramming в С++. И был там такой комментарий: «Ровно то же самое с тем же уровнем настраиваемости можно было сделать на интерфейсах, реализациях, на фабриках, на дефайнах, на конфигах и на еще целой куче вещей». И вообще, мораль статьи и обсуждения — эти шаблоны от Александреску в жизни не шибко-то и нужны.
Я вспомнил свою задачу, где мне его (Александреску) идея об ортогональном проектировании здорово помогла. Хочу с вами ею поделиться.


Проблема


Я пишу программы для компьютера и микроконтроллерных устройств. И частенько мне приходится организовывать связь между ними. И, как водится, в программах надо отлавливать ошибки. И если PC-ую программу отладить не очень-то и сложно, то с микроконтроллерами дела обстоят гораздо хуже (в смысле, есть всякие внутрисхемные отладчики, но они не подходят для задач реального времени). Ну и даже не в этом дело — иной раз надо отладить саму связь! Т. е. вроде бы информация должна идти, но она идет как-то неправильно… Это существенно на первых порах разработки программы — когда отлаживается сама связь, протокол и т. п. Искал я какую-нибудь программу для отслеживания обмена по USB, да как-то не мог найти. Что-то на глаза попадалось, но это все было не то (или то, но очень платное).

И вдруг подумалось мне — а чего бы мне не написать самому такую программу? Собственно говоря, и всех делов — на функции чтения/записи, открытия/закрытия вешаем дополнительную функцию записи во внешний файл. А там с помощью #define отключаем эти функции, когда они не нужны.

Параллельно с этим я сделал несколько полезных классов для обмена данными в памяти между процессами, по локальной и глобальной сетям. И тоже надо было этот обмен отлаживать.

И там и там я использовал общий логический протокол — есть в сообщении запрещенный символ, который является признаком начала/конца пакета.

И вот, на глаза мне попадается эта книга Александреску. В голове звякнул звоночек о том, что тут можно что-то замутить эдакое…

Собственно говоря, захотелось сделать единый базовый класс, который реализует этот самый логический протокол. Этому классу надо также задать физическую природу обмена — USB, межпроцессный обмен (дело было в Windows, использовалось отображение в память), обмен по локальной сети (именованные каналы), по глобальной (сокеты). Также захотелось сделать функцию отладки. А еще захотелось…

Постановка задачи


Итак, надо сделать базовый класс, которому задаются следующие свойства (или «политики»):
  • физическая природа — какой способ связи используется. Сейчас у меня реализован обмен по USB от FTDI, отображение в память, именованные каналы, сокеты. Хочется сюда еще добавить обмен по COM-порту;
  • Способ обработки — вначале это были просто функции «есть вход?», «есть на выход», «каково состояние связи?». Потом, когда я писал программу для интенсивного асинхронного обмена, я реализовал многозадачный алгоритм обмена. Итак, у меня сейчас есть 2 разных политики для способа вызова — функциональный и многопоточный;
  • Защита обмена — не надо забывать, что моя программа работает в многозадачной среде и разные потоки могут одновременно обращаться к устройству обмена. А могут и нет. Посему получаем еще одну «плоскость» — однозадачный (в простых случаях), защита критическими секциями. Если надо — сделаю защиту мьютексами, но пока еще не было нужды;
  • Отчет в файл — нужен или нет? Если нужен — в каком виде? Я сделал вариант, который пишет текстовый файл с разделителями. Он удобно экспортируется в Excel и другие подобные программы;
  • Измерение времени — иной раз важно бывает знать временнЫе промежутки между пакетами. ИХ можно измерять по-разному (в зависимости от задачи). Я сделал измерение времени грубое (GetTicksCount), точное (высокоточный таймер в Windows)и сверхточное (таймер RDTSC).


И самое главное — существенные только две первые политики — природа связи и способ обработки событий связи. Все остальное может быть проигнорировано. Поэтому у меня есть «пустая» политика — та, которая не делает ничего.

Да, еще одно — этот класс не претендует на универсальность решения. Речь не идет о динамическом подключении разных методов и классов. Все глобальные настройки должны осуществиться в одном месте программы — и впредь на все время работы не меняться.

Что получилось


В итоге был сделан шаблонный класс InterConnect:
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;
// ...
};


В теле класса я везде проверяю — используется ли данная политика или нет. Вот как, например, реализована функция записи байта:
UPD — были исключения, убрал

template <typename Phys, typename Call, typename Prot, typename Log, typename Timer>
bool InterConnectStorage<Phys,Call,Prot,Log,Timer>::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;
	}
}


Тут вроде бы все очевидно. Из политики «защита» вызываем функцию защиты байта (в других местах защищается весь пакет). Вызываем из политики «организация обмена» функцию call (ей передаем объект «физическая природа» и, собственно, байт). Дальше вызываем из политики «запись отчета» проверку — есть ли эта политика? Если да — пишем в отчет информацию. Если используется политика «отсчет времени» — пишем с учетом времени (ставим отметку «событие» функцией stamp, пишем в отчет), нет — просто в отчет.

Везде стоит проверка — используется ли данная политика или нет? На этот вопрос отвечает статическая функция bool to_use (void). Ввиду того, что она статическая и очень простая, компилятор может оптимизировать код — или его вообще не создавать (если политика не используется) или сразу вызывать без проверки.

В итоге довольно-таки громоздкий код на С++ превращается в достаточно простой и оптимальный на ассемблере.

Использование


В результате я получил очень удобный инструмент, который настраивается совсем небольшим кодом.

Вот пример небольшой программы обмена по USB:

typedef InterConnectStrat::FTDIAccess Phys;			// физический уровень
typedef InterConnectStrat::ThreadCall <Phys> 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 <Phys, Call, Prot, Log, Timer> _con;	// устройство для связи


Константой MakeLogFile я полностью меняю логику работы программы. Перекомпиляция, конечно, занимает время, но результирующий код получается весьма эффективным — как по быстродействию и по ресурсам, так и по функциональности.

В данном коде мне не важны промежутки между посылками, но вначале разработки этой программы это было нужно. Я менял лишь typedef /*… */ Timer — и там все и задавал.

Еще по ходу дела я отрабатывал реакцию программы на сообщения. Для этого я поменял природу обмена с USB на межпроцессное. Я сделал второе консольное простенькое приложение, которое посылало нужные мне пакеты. И так я настроил реакцию основной программы на нужную. Потом я, опять же, поменял typedef Phys и задание параметров связи в одном месте программы (5 строк максимум) — и все стало работать с USB. Очень просто и удобно!

Также я сделал перегрузку основных операций — начало/конец пакета (это задает, если надо, режим защиты в многозадачной среде), посылку байтов и т. п. В итоге запись пакета выглядит так:


	// . версия программы
	_con++;
	_con <<= SetProgramDate;
	_con <<= _HEX_date.get_Unix_time ();
	--_con;

	// . переход на область программы
	_con++;
	_con <<= GotoProgram;
	--_con;



Код говорит сам за себя.

Оцените красоту решения! Во всей программе, кроме точки инициализации, мне равнодушна целая куча вещей — какой протокол обмена, многозадачная ли программа или нет, есть ли запись в отчет или нет. Листинг очень простой!

Но при этом, в отличие от всевозможных фабрик классов и т. п., получающийся код оптимальный — нет лишних проверок и переходов.

Сравнение других методов


А теперь для интереса рассмотрим другие способы решения этой же задачи средствами С++9х. Подсказывайте в комментариях что я упустил:

  • Наследование — в отличие от шаблонов гарантирует существование нужных функций (чего не хватает шаблонам). Но все остальное — динамическое подключение, разные виртуальные функции — тут не нужны. Более того, виртуальные функции не могут быть оптимизированы во встраиваемые. Тут несомненный бонус у шаблонов;
  • Фабрики — с ними я еще не сталкивался. Насколько я понимаю, это дальнейшее развитие предыдущего способа. В плане оптимизации и быстродействия уступает шаблонам;
  • typedef — наверное, все это можно было реализовать и без шаблонов. Но .h-файл шаблонов может быть скомпилирован и без используемых в нем классов, с typedef такой номер не пройдет. Да и как-то шибко сложно оно будет, на typedef :-) ;
  • #define — кхм… Думаю, без комментариев ;-)


В общем, инструмент получился очень удобным и качественным. Он позволяет развивать свою функциональность в абсолютно разных плоскостях.

P. S. Александреску — молодец, что бы не говорили некоторые!
Tags:
Hubs:
+45
Comments28

Articles

Change theme settings