Форматный вывод для МК на Си++

В предыдущей статье я писал о форматном выводе для микроконтроллеров стандартными средствами языка Си: Форматный вывод на Си для микроконтроллеров. Теперь посмотрим что интересного можно сделать на С++.

Астарожна многабукаф!
Чем-же плоха стандартная библиотека Си stdio с функциями типа printf? С одной стороны, printf стандартна, доступна практически в любом Си компиляторе и достаточно функциональна. С другой стороны, библиотека stdio практически монолитна. Это значит, что использовав printf только, скажем, для вывода строк и целых чисел, она потянет за собой весь свой функционал, включая, например, вывод чисел с плавающей точкой и всё остальное, который будет висеть мертвым грузом и занимать драгоценные килобайты памяти программ. В ограниченной по ресурсам среде микроконтроллеров всегда хочется платить только за то, что реально используется в конкретной программе. Поэтому часто имеется несколько версий функций с различным уровнем функциональности. Параметры в printf и подобные функции передаются через стек, даже если они все поместятся в регистрах, что удорожает их вызов и увеличивает нагрузку на стек. Ну а когда тип фактического параметра не совпадает с тем, что указано в форматной строке — это вообще песня.
Еще функциональность printf нельзя расширять, например для вывода своих типов. Допустим у нас есть структура Point:
typedef struct {
int x;
int y;
} Point;

И мы хотим ее выводить в формате (x; y). Для этого нам придётся каждый раз повторять один и тотже фрагмент форматной строки и параметров:
Point p = {10, 20};
…
printf("point = (%d; %d)", p.x, p.y); 

Если надо изменить формат вывода, то придётся искать все места, где выводится этот тип и исправлять форматную строку. Можно, конечно клеить форматную строку из отдельных кусков с помощью препроцессора, но это не удобно и выглядит ужасно.

В С++ форматный вывод обычно осуществляется с помощью стандартной библиотеки iostream. Для вывода имеется класс ostream реализующий поток вывода. Обычно определены три глобальных объекта этого класса cout, clog и cerr. Для форматного вывода в ostream переопределены операторы << (сдвиг влево) для большинства встроенных типов, которые теперь называются оператор поместить в поток:
std:cout << "myVar = ";
std::cout << 12345;
std::cout << std::endl;

Оператор << поддерживает сцепление вывода и этот пример можно записать короче:
std::cout << "myVar = " << 12345 << std::endl;

Потоки вывода являют собой совсем другую концепцию форматного вывода нежле функции семейства printf. Если в printf данные — параметры функции, и их представление — форматная строка раздельны, то в потоках они объединены в одной конструкции. Оба подхода имеют свои плюсы и минусы. Потоки типо-безопасны, так как точные типы выводимых объектов известны во время компиляции и компилятор всегда вызовет нужный operator<<. По этой-же причине потоки должны быть быстрее, теоретически, поскольку не тратится время запихивание параметров в стек и на разбор форматной строки.
На практике со стандартной библиотекой С++ и в частности с библиотекой потоков нас ждут две проблемы:
1 — её просто нет в большинстве С++ компиляторов для МК.
2 — там, где она всё-таки есть, использовать её практически невозможно, потому, что:
— Используются исключения. Исключения, конечно, штука хорошая, но дорогая, например, в arm-gcc в связке с библиотекой NewLib, инфраструктура исключений занимает около 17 Кб флэша и около килобайта оперативки. Поэтому использовать их не всегда уместно.
— используются виртуальное наследование и виртуальные функции, которые так-же очень нужны и полезны, но опять-же не всегда.
— во многих реализациях (IAR и NewLib не исключения) для преобразования различных типов в строки используется функция sprintf (facepalm.jpg). Конечно, такая реализация не будет работать быстрее, чем printf
— локали, стрим-буффреры и коллбэки на разные события в потоках тоже не добавляют оптимизма в нашем случае.
Ну что же, будем писать свою реализацию ostream с блекджеком и … э-э-э с шаблонами и жестоким попранием стандартов, то есть адаптированную под МК. Вернее это будет ни разу не ostream, а что-то на него очень похожее.
Наша цель — платить только за то, что реально используется и ни капли больше. Поэтому будем избегать использования даже такой мелочи как виртуальные функции, пока они реально не понадобились.

Итак, вооружившись официальной документацией приступим за дело.
Iostream library
Рис. 1. Диаграмма классов библиотеки iostream.

Рассмотрим структуру библиотеки iostream. Нас в первую очередь будет интересовать класс ostreamименно в нем функциональность форматного вывода. Классы ios_base и ios содержат общую базовую функциональность форматного ввода и вывода. Это — различные флаги форматирования и функции манипулирования ими, атрибуты форматирования, локали, регистрация функций обратного вызова и еще какая-то хрень. Поскольку у нас будет минималистичная реализация, поэтому очень вольно обойдемся со стандартом и реализуем только то, что непосредственно относится к форматированию, а локали и всякая хрень идут лесом.
Библиотека Iostream поддерживает как обычные символы и строки (char), так и «широкие» (wchar_t), поэтому большинство классов библиотеки являются шаблонами, принимающими тип символа как параметр. Класс ios не является шаблонным и содержит только ту функциональность, которая не зависит от типа символа. В класс ios_base уже добавлены вещи зависящие от типа символа, например символ заполнения поля.

namespace IO{
// Тип размера потока, нужен для задания ширины поля вывода.
typedef unsigned streamsize_t;
	class ios_base
	{
	// Конструктор копирования и оперетор присваивания закрытые,
	// чтоб объекты этого класса нельзя было копировать.
		ios_base(const ios_base&);
		ios_base& operator=(const ios_base&);
	public:
		ios_base()
		:_flags(right)
		{}
	// флаги форматирования
		enum fmtflags
		{
			boolalpha   = 1 << 0,
			dec         = 1 << 1,
			fixed       = 1 << 2,
			hex         = 1 << 3,
			internal    = 1 << 4,
			left        = 1 << 5,
			oct         = 1 << 6,
			right       = 1 << 7,
			scientific  = 1 << 8,
			showbase    = 1 << 9,
			showpoint   = 1 << 10,
			showpos     = 1 << 11,
			skipws      = 1 << 12,
			unitbuf     = 1 << 13,
			uppercase   = 1 << 14,
			adjustfield = left | right | internal,
			basefield   = dec | oct | hex,
			floatfield  = scientific | fixed
		};
	// Состояние потока.
		enum iostate
		{
			goodbit = 0,
			badbit  = 1 << 0,
			eofbit  = 1 << 1,
			failbit = 1 << 2
		};
	// Направление поиска для файловых потоков.
		enum seekdir
		{
			beg = 0,
			cur = 1,
			end = 2
		};
	// Режим открытия файлов
		enum openmode
		{
			app    = 1 << 0,
			ate    = 1 << 1,
			binary = 1 << 2,
			in     = 1 << 3,
			out    = 1 << 4,
			trunc  = 1 << 5
		};
	// Манипулирование флагами форматирования
       		inline fmtflags flags ( ) const;
		inline fmtflags flags ( fmtflags fmtfl );
		inline fmtflags setf ( fmtflags fmtfl );
		inline fmtflags setf ( fmtflags fmtfl, fmtflags mask );
		inline fmtflags unsetf ( fmtflags mask );
	// Ширина поля.
        		inline streamsize_t width ( ) const;
		inline streamsize_t width ( streamsize_t width );
	// Точность.
		inline streamsize_t precision ( ) const;
		inline streamsize_t precision ( streamsize_t prec );
	private:
            fmtflags _flags;
            streamsize_t _width;
	streamsize_t _prec;
	};

DECLARE_ENUM_OPERATIONS(ios_base::fmtflags)
DECLARE_ENUM_OPERATIONS(ios_base::iostate)

    	template<class CharT>
	class basic_ios :public ios_base
	{
	public:
		basic_ios()
			:_fillch(' ')
		{}
		inline bool good () const;
		inline bool fail () const;
		inline bool bad () const;
		inline bool eof ( ) const;
		inline iostate rdstate ( ) const;
		inline void setstate ( iostate state );
		inline void clear ( iostate state = goodbit );
		inline CharT fill ( ) const;
		inline CharT fill ( CharT fillch );
	protected:
		iostate _state;
		CharT _fillch;
	};
// Конкретизации шаблона basic_ios
	typedef basic_ios<char> ios; // для обычных символов
	typedef basic_ios<wchar_t> wios; // для широких
}

Для того, чтобы удобнее работать с флагами используем DECLARE_ENUM_OPERATIONS.
Класс ostream наследуется от ios путём виртуального наследования, для того чтобы избежать дублирования элементов ios в классе iostream, который дважды наследует ios через ostream и через istream. Виртуальное наследование не то чтобы очень дорогая штука, но не бесплатная — обращение ко всем элементам базового класса происходит через еще один дополнительный указатель. Далее, класс ostream использует внутри себя объекты класса streambuf, которые во-первых буферизируют вывод, а во-вторых реализуют его по средствам виртуальных функций, который тоже не очень дороги, но всё-же. Это противоречит заявленному выше принципу — «платить только за то, что реально используется». Так зачем платить за виртуальные функции и виртуальное наследование, если поток вывода всего один, а ввода (istream и iostream тоже) нет и в помине. Раз уж реализация всё равно сильно не стандартная, а обеспечивает только внешний вид iostream, то не будем стеснятся в средствах — streambuf не используем, виртуальное наследование тоже.

template<class OutputPolicy,
            class CharT = char,
            class IOS = basic_ios<CharT>
            >
	class basic_ostream :public OutputPolicy, public IOS
{
...
};

Здесь OutputPolicy — класс политики вывода, который должен содержать определение функции-члены «put» — вывод одного символа. Как именно эта функция определена зависит от наших требований. Она может быть, например, статической чисто виртуальной или обычной функцией-членом, об этом позднее. Эта штука нужна чтоб избавится от streambuf и не использовать виртуальные функции без надобности.
CharT — тип символа — char или wchar_t, если хотим использовать широкие символы.
IOS — класс basic_ios (по умолчанию) или класс унаследованный от него. Передача basic_ios в качестве параметра шаблона как раз и дает возможность спрямить иерархию наследования.

Рис. 2. Спрямленная иерархия классов.

У нас получается своего рода конструктор из которого легко сделать такой ostream, какой нужен. Например, если у нас всего один поток вывода, то его можно реализовать так:
struct WriteToUsart
{
	void put(char c)
	{
		Usart::Putch(c);
	}
};

typedef IO::basic_ostream<WriteToUsart> ostream;
ostream cout;
…
// "стандартный" поток вывода
cout << "Hello, world!";

Здесь нет ни виртуальных функций, ни виртуального наследования, функция вывода символа вызывается непосредственно (даже не по указателю) и легко может быть заинлаенена компилятором. Далее если нужно несколько потоков, то целесообразно воспользоваться виртуальными функциями:
struct VirtualWrite
{
	virtual void put(char c)=0;
};

typedef IO::basic_ostream<VirtualWrite> ostream;
…
class LcdStream :public ostream
{
public:
virtual void put(char c)
{
	Lcd::Putch(c);
}
};

class UsartStream :public ostream
{
public:
virtual void put(char c)
{
	Usart::Putch(c);
}
};
// "стандартный" поток вывода
LcdStream cout;
// "стандартный" поток ошибок
UsartStream cerr;
…
cout << "Hello, world!";
cerr << "Shit happened.";

Получилось два потока, разделяющие общую реализацию и отличающиеся только функцией put. Теперь можно передавать эти потоки как параметры функций через указатель или ссылку на базовый класс ostream:
void PrintSomething(ostream &out)
{
out << "Foo = " << foo << endl;
}
…
PrintSomething(cout);
PrintSomething(cerr);

Если в дальнейшем нам потребуется поток ввода-вывода (iostream), то его можно будет реализовать так:

typedef IO::basic_istream<MyGet> istream;
typedef IO::basic_ostream<MyPut, char, istream> iostream;
...
iostream my_stream;
…
// пишем что-то
my_stream << foo;
// читаем что-то
my_stream >> bar;

Класс basic_istream пока не реализован — это дело ближайшего времени.
При таком определении получается иерархия классов как на рис. 2. Так-же можно при желании реализовать стандартную ромбовидную иерархию:

class VirtualIos :virtual public ios
{};
...
typedef IO::basic_istream<MyGet, char, VirtualIos> istream;
typedef IO::basic_ostream<MyPut, char, VirtualIos > ostream;
…
class iostream :public istream, public ostream
{
...
};

Класс VirtualIos -пустой и нужен только для того чтоб виртуально наследоваться от ios.
Теперь рассмотрим детали реализации. Для начала — самое простое — вывод строки.
// оператор для помещения строки в поток
Self& operator<< (const CharT* value)
{
// вывод строки
	puts(value);
// все операторы << возвращают ссылку на поток
	return *this;
}

// функция фывода строки
template<class CharPtr>
void puts(CharPtr str)
{
// вычисляем длину строки
	CharPtr strEnd = str;
	while(*strEnd) ++strEnd;
	streamsize_t outputSize = strEnd - str;
// заполнение поля слева при выравнивании вправо
FieldFill(outputSize, IOS::right);
// непосредственно вывод строки
	while(CharT c = *str)
	{
		put(c);
		++str;
	}
// заполнение поля справа при выравнивании влево
	FieldFill(outputSize, IOS::left);
}

// заполненяет поле символом заполнения при соответствующем установленном флаге
void FieldFill(streamsize_t lastOutputLength, typename IOS::fmtflags mask)
{
	if(IOS::flags() & mask)
	{
// получаем текущую ширину поля и сбрасываем её в ноль
streamsize_t width = IOS::width(0);
// если есть, что заполнять
		if(width < lastOutputLength)
			return;
// сколько заполнять
		streamsize_t fillcount = width - lastOutputLength;
// чем заполнять
		CharT c = IOS::fill(' ');
// заполняем
		for(streamsize_t i=0; i<fillcount; i++)
			put(c);
	}
}

Выглядит довольно просто. Но зачем функцию puts сделана шаблонной с дополнительным параметром в виде типа указателя на символ? Просто нам нужно иметь ввиду, что строки могут быть в разных адресных пространствах, как в AVR. Это позволяет использовать эту технику для вывода строк из памяти программ. Self — это псевдоним для basic_ostream для краткости.
Что еще нужно для форматного вывода — так это преобразование целых чисел в строку. Не сложная задача на первый взгляд. Вроде бы для этого есть специальные функции в библиотеке Си, типа itoa, ltoa и т.д. Но они необязательны для реализации и во многих компиляторах их нет. По этому придётся написать эту функциональность самостоятельно.
template<class T, class CharT>
CharT * IntToString(T value, CharT *bufferEnd, unsigned radix)
{
	CharT *ptr = bufferEnd;
	do
	{
		T q = value / radix;
		T rem = value - q*radix;
		value = q;
		*--ptr = (rem < 10 ? '0' : 'a' - 10) + rem;
	}
	while (value != 0);
	return ptr;
}

Здесь value — преобразуемое значение,
bufferEnd — указатель на конец буфера результата,
radix — основание преобразования — от 2 до 36.
Возвращает функция указатель на начало результата в буфере. Вызывающий код ответственен за предоставление буфера подходящего размера. Отличие от itoa у этой функции в том, что она принимает указатель не на начало буфера, а не его конец, это позволяет избежать дополнительного переворачивания строки в памяти, так как символы результата в этом алгоритме вычисляются от старшего разряда к младшему. Также эта функция игнорирует знак числа, о нем мы позаботимся в другом месте.
О ужас! В этой функции используется деление, которое не реализовано аппаратно на многих контроллерах и типа жутко медленно. Разве нельзя было реализовать это как-то по-другому, например вычитанием степени основания (степеней 10, например, 1000, 100, 10). Я сравнивал эти два варианта и пришел к выводу, что метод с «вычитанием» дает заметное преимущество по скорости и размеру кода только для преобразования одно-байтовых переменных и то только для восьми-битных платформ. В остальных случаях получается более компактный код и, что удивительно иногда более быстрый.
Теперь рассмотрим саму функцию форматированного вывода целых чисел:
Self& operator<< (int value)
{
	PutInteger(value);
	return *this;
}

Self& operator<< (long value)
{
	PutInteger(value);
	return *this;
}

Self& operator<< (unsigned long value)
{
	PutInteger(value);
	return *this;
}

Self& operator<< (unsigned value)
{
	PutInteger(value);
	return *this;
}

template<class T>
void PutInteger(T value)
{
// размер буфера перобразования sizeof(T)*3 + 1
	const int bufferSize = Impl::ConvertBufferSize<T>::value;
// буфер перобразования
	CharT buffer[bufferSize];
// размер буфера префикса
	const int maxPrefixSize = 3;
// буфер перфикса - знак (+/-) или основание (0 или 0x) числа
	CharT prefix[maxPrefixSize];
	CharT *prefixPtr = prefix + maxPrefixSize;
// если число десятичное - обрабатываем знак
	if((IOS::flags() & (IOS::hex | IOS::oct)) == 0)
	{
		if(Util::IsSigned<T>::value)
		{
			if(value < 0)
			{
				value = -value;
				*--prefixPtr = '-';
			} else if(IOS::flags() & IOS::showpos)
				*--prefixPtr = '+';
		}
		else
		{
			if(IOS::flags() & IOS::showpos)
				*--prefixPtr = '+';
		}
	}
	else // обрабатываем базу
	if(IOS::flags() & IOS::showbase)
	{
		if(IOS::flags() & IOS::hex)
			*--prefixPtr = 'x';
		*--prefixPtr = '0';
	}
// приводим число к беззнаковому виду
	typedef typename Util::Unsigned<T>::Result UT;
	UT uvalue = static_cast<UT>(value);
// преобразуем число в строку
	CharT * str = Impl::IntToString(uvalue, buffer + bufferSize, Base());
// заполнение слева при выравнивании вправо
	int outputSize = buffer + bufferSize - str + prefix + maxPrefixSize - prefixPtr;
	FieldFill(outputSize, IOS::right);
// вывод префикса
	write(prefixPtr, prefix + maxPrefixSize);
// внутреннее заполнение
	FieldFill(outputSize, IOS::internal);
// вывод результата
	write(str, buffer + bufferSize);
// заполнение справа для выравнивания влево
	FieldFill(outputSize, IOS::left);
}

Зачем приводить преобразуемое значение к без-знаковому типу? Очень просто — чтоб избежать знакового деления, оно нам не нужно и без-знаковое деление несколько быстрее.
value = -value;

Что будет если value будет равно INT_MIN, ведь это положительное число с таким модулем не представимо в int? Действительно минус INT_MIN всё равно будет INT_MIN. Но в данном случае это безразлично потому, что дальше мы его всё равно приводим к без-знаковому типу, в котором получается правильное значение.
Поддержку чисел с плавающей точкой я пока не реализовал, это довольно сложно сделать чтоб было быстро, компактно и в тоже время переносимо.
Теперь возьмёмся за манипуляторы потока, чтоб можно было использовать такие конструкции:
cout << "Foo" << setw(10) <<  hex << 0x1234 << endl;

Здесь setw, endl и hex — функции-манипуляторы потока. setw — параметризованная, endl и hex — не параметризованная. Эти функции не являются членами класса basic_ostream и могут быть определены где угодно.
template<class OutputPolicy, class CharT, class IOS>
basic_ostream<OutputPolicy, CharT, IOS>& 
hex(basic_ostream<OutputPolicy, CharT, IOS>& os)
{
	os.setf(hex, basefield);
	return os;
}

Как видно, эта функция принимает объект класса basic_ostream по ссылке, устанавливает нужный флаг и возвращает этот-же объект по ссылке. Это нужно для поддержки сцепленных вызовов. Из-за шаблонных аргументов запись получается несколько многословной, but it's a small price to pay. Осталось только перегрузить оператор помещения в поток для функций-манипуляторов, в котором эти функции вызываются. Его удобнее определить как член класса basic_ostream:
Self& operator<<(Self& (*__pf)(Self&))
{
	return __pf(*this);
}

Похожим образом реализуются и параметризованные манипуляторы, только они должны возвращать нечто, например, структуру, для чего определен оператор помещения в поток, который уже делает какую-то работу.
Это практически вся реализация «минимального» и не стандартного ostream. За кадром остались такие мелочи как вывод bool, отдельных символов и еще немного. И еще нет ни слова про обработку ошибок. В классе basic_ios есть поле _state, которое содержит флаги состояния потока, но оно пока нигде не используется. Дело в том, что в этой реализации поток ничего не знает про способ вывода и про возможные ошибки. По стандарту этим должен заниматься объект класса streambuf, но у нас его нет, зато есть политика вывода OutputPolicy — она обработкой ошибок и должна заниматься. Как — это уже вопрос конкретного применения, их, например, можно вообще не обрабатывать как в примерах выше. В случае если у нас один поток обрабатывать ошибки можно так:
struct WriteToUsart
{
	void put(char c);
};

typedef IO::basic_ostream<WriteToUsart> ostream;
ostream cout;

void WriteToUsart::put(char c)
{
	Usart::Putch(c);
	if(shit_is_happened())
		cout.setstate(IO::ios::badbit);
}
…
cout << "Hello, world!";
// если произошла ошибка
if(cout.bad())
{
	// сделать какую-нибудь гадость
	SystemReset();
}

Строки в памяти программ
В МК семейства AVR, как известно, строковые литералы хочется хранить во флеш памяти, чтоб не расходовать на них драгоценное ОЗУ, но работать с ними не очень удобно из-за того, что они находятся в отдельном адресном пространстве. Попробуем сделать вывод строк из флеш по методу упомянутому выше. Для этого уже есть почти всё: «умный» указатель на память программ и функция puts, способная с ним работать. Осталось реализовать оператор вывода для удобства:
ostream& operator<<(ostream &s, ProgmemPtr<char> str)
{
	s.puts(str);
	return s;
}

И всё, можно пользоваться:
FLASH char flash_str[] = "This string is form flash! Hello world!!";
…
cout << FLASH_PTR(flash_str) << endl;

Полевые испытания.
Тестовый полигон: целевой МК Atmega16, прошивка запускается на симуляторе simulavr. Это быстро, просто и удобно, не нужно никуда заливать прошивку, достаточно скомпилировать проект и запустить скрипт.
Среды разработки:
AVRStudio 4 + WinAVR-2010-01-20;
IAR for AVR kickstart edition 5.50
«Hello, world» — полный код:
// специальные порты для отлодочного вывода
#define special_output_port (*((volatile char *)0x24))
#define special_input_port  (*((volatile char *)0x25))
#define special_abort_port  (*((volatile char *)0x22))
#define special_exitcode_port  (*((volatile char *)0x020))

#include <avr/io.h>
#include <tiny_ostream.h>
#include <flashptr.h>

struct MyWriter
{
	void put(char c)
	{
		special_output_port = c;
	}
};

typedef IO::basic_ostream<MyWriter> ostream;
ostream cout;

int main()
{
	cout << "hello, world!\n";
	special_exitcode_port = 0;
	return 0;
}

В WinAVR при оптимизации по размеру (Os) получилось 338 байт кода, 114 из которых — стартап и вектора прерываний. Функция main выполняется за 128 тактов для вывода 14-ти символов.
У IAR при оптимизации по размеру 482 байта кода, из которых 196 байт полезный код, а остальное — стартап и вектора прерываний. Функция main выполняется за 328 такта.
Не используем вывод чисел, bool-ов, строк из флеша и т.д. и соответствующий код не попадает в бинарик — платим, только за то, что заказывали.

Примечание:
Приведенные результаты относятся именно к WinAVR (avr-gcc 4.3), в новом AVR Tollchain (avr-gcc 4.5.1) всё несколько хуже: у него есть особенность, что при включенной оптимизации по размеру кода (-Os) он не инлайнит функции в C++ исходниках вообще, никакие (в Си всё нормально). Никакие опции касательно встраивания функций в этом режиме не работают. Не знаю, баг это, или фича, но результат много хуже. Если всё-же использовать AVR Tollchain, то лучше использовать опции -O3 плюс -finline-limit=30. Результат всё-равно хуже, но уже не настолько.

Это еще не всё!
У потоков в стиле C++ есть один значимый недостаток, который им часто вменяется сторонниками форматного вывода в стиле printf. Это — формата вывода с выводимыми данными. Допустим нужно вывести сложным образом форматированное сообщение, включающее несколько переменных:
printf("A = 0x%08lx and B =  0x%08lx", A, B);

Если A = 1, а B = 2, то этот код выведет:
A = 0x00000001 and B = 0x00000002

Чтоб получить такой-же результат с помощью С++ потоков нужно:
cout << "A = " << hex << internal << setw(8) << setfill('0') << A
<< "and B = " << hex << internal << setw(8) << setfill('0') << B;

Во-первых, получается многословно. Во-вторых форматирование размазано между данных. В варианте с printf форматную строку можно вынести в любое удобное место, где ее относительно легко изменить, локализовать и т.д.
Хочется совместить мощь, безопасность и скорость потоков с удобствами форматной строки. Какие есть готовые решения для поставленной задачи? Есть очень мощные библиотеки решающие эту задачу, это BoostFormat, FastFormat, SafeFormat. Они действительно хороши и удобны для «больших» машин, но они довольно требовательны к памяти: внутренняя буферизация, нумерованные аргументы и т.д. В общем они слишком тяжелы для МК. Придется снова изобретать велосипед.
В качестве форматной строки я решил выбрать синтаксис похожий на printf, но с выкинутым типом аргумента — он больше не нужен. Остаётся знак процента %, флаги, ширина поля и точность (пока нет поддержки плавающей точки она не используется). Только возникает вопрос в том, что символ типа в форматной строке одновременно служит ограничителем форматного спецификатора его просто так не выкинешь. В одном из возможных форматтеров BoostFormat тоже используется printf синтаксис, там эту проблему решили ограничив форматный спецификатор символами '|'. Получилось что-то типа такого:
Format("A = %|x08| and B = %|x08|") % A % B;
// а можно и так
Format("Foo = %") % Foo;

Для «скармливания» форматтеру параметров используется оператор % для единообразного вида с форматной строкой. Возьмём за основу этот синтаксис, не забывая про то, что форматтер должен уметь кушать строки из flash памяти и, что хорошо уметь выбирать уровень функциональности: разбираем флаги, ширину, точность, или не разбираем.
Общий алгоритм работы форматтера такой: посимвольно читаем и выводим форматную строку пока не встретится форматный спецификатор. Когда находим спецификатор, разбираем его, устанавливаем соответствующие флаги потока и останавливаем разбор форматной строки. Оператор % выводит свой аргумент в поток с помощью оператора << и продолжает разбор форматной строки до следующего спецификатора. После вывода каждого параметра флаги форматирования потока сбрасываются в значение по умолчанию.
namespace IO
{
// Уровень функциональности
	enum FormatMode
	{
		FmMinimal,
		FmNormal,
		FmFull
	};

	template<class Stream, FormatMode Mode = FmNormal, class FormatStrPtrType = char *>
	class FormatParser
	{
		static const bool ScanFloatPrecision = Mode == FmFull;
		static const bool ScanFieldWidth = (Mode == FmNormal || Mode == FmFull);
		static const bool ScanFlags = (Mode == FmNormal || Mode == FmFull);
// указатель на форматную строку
		FormatStrPtrType _formatSrting;
		typedef FormatParser Self;
	public:
// ссылка на поток вывода
		Stream & out;
	private:
// очистить флаги форматироваия потока
		void inline ClearFmt()
		{
			out.setf(Stream::right | Stream::dec, Stream::unitbuf | Stream::showpos |
											Stream::boolalpha | Stream::adjustfield |
											Stream::basefield | Stream::floatfield |
											Stream::skipws | Stream::showbase |
											Stream::showpoint | Stream::uppercase);
		}
// Функция разбора форматной строки
		inline void ProcessFormat();

	public:
// Конструктор
		FormatParser(Stream &stream, FormatStrPtrType format)
			:out(stream),
			_formatSrting(format)
		{
			if(_formatSrting)
				ProcessFormat();
		}
// поддержка для манипуляторов потока
		Self&
		operator% (Stream& (*__pf)(Stream&))
		{
			__pf(out);
			return *this;
		}
// поддержка для манипулятора setw
		Self&
		operator% (SetwT f)
		{
			out.width(f.width);
			return *this;
		}
// вывод значения в поток
		template<class T>
		Self& operator % (T value)
		{
			if(_formatSrting)
			{
				out << value;
				ProcessFormat();
			}
			return *this;
		}
	};
}

Благодаря тому, что форматтер использует для вывода оператор << потока, мы автоматом получаем вывод для всех типов, для которых есть соответствующий опрератор поместить в поток. Теперь разберем код функции ProcessFormat:
template<class Stream, FormatMode Mode, class FormatStrPtrType>
void FormatParser<Stream, Mode, FormatStrPtrType>::ProcessFormat()
{
// очищаем флаги потока
	ClearFmt();
// Оптимизация: кешируем указатель на форматую строку в локальной переменной
	FormatStrPtrType ptr = _formatSrting;
	while(true)
	{
// нашли спецификатор
		if(*ptr == '%')
		{
			ptr++;
// точно спецификатор, а не двойной %
			if(*ptr != '%')
			{
// разбираем флаги если они есть
				if(*ptr == '|' && ScanFlags)
				{
					bool isFlag=true;
					do{
						ptr++;
						typename Stream::fmtflags flags, mask;

						if(*ptr == '+')
						{
							mask = flags = Stream::showpos;
						}
						else if(*ptr == '#')
						{
							mask = flags = Stream::showbase | Stream::boolalpha;
						}
						else if(*ptr == 'x')
						{
							flags = Stream::hex;
							mask = Stream::basefield;
						}
						else if(*ptr == 'o')
						{
							flags = Stream::oct;
							mask = Stream::basefield;
						}
						else if(*ptr == '0')
						{
							out.fill('0');
							flags = Stream::internal;
							mask = Stream::adjustfield;
						}
						else if(*ptr == '-')
						{
							out.fill(' ');
							flags = Stream::left;
							mask = Stream::adjustfield;
						}
						else
						{
							isFlag = false;
						}

						if(isFlag)
						{
							out.setf(flags, mask);
						}
					}while(isFlag);
// сканируем ширину поля, если нужно
					if(ScanFieldWidth)
					{
						out.width(Impl::StringToIntDec<streamsize_t>(ptr));
					}
// сканируем точность, если нужно
					if(ScanFloatPrecision && *ptr == '.')
					{
						ptr++;
						out.precision(Impl::StringToIntDec<streamsize_t>(ptr));
					}
// конец спецификатора
					if(*ptr == '|')
						ptr++;
				}
			}
// спецификатор разобран, выходим
			_formatSrting = ptr;
			return;
		}
// форматная строка кончилась
		if(*ptr == '\0')
		{
			_formatSrting = 0;
			return;
		}
// выводим символ из форматной строки
		out.put(*ptr);
		ptr++;
	}
}

Поскольку тип указателя форматной строки задаётся в дополнительном шаблонном параметре, мы «из коробки» получили поддержку форматных строк из flash памяти. В принципе форматтером уже можно пользоваться:
FormatParser(cout, "A = %") % A;

Но не удобно задавать уровень функциональности потому, что придётся указывать и полный тип потока, а он длинный. Поэтому сделаем еще дополнительную обёртку для удобства:
// дополнительная структура, которая хранит уровень функциональности форматтера
// в шаблонном параметре Mode, тип указателя форматной строки - FormatStr
// и сам указатель форматной строки 
template< FormatMode Mode, class FormatStr>
struct FormatT
{
	FormatStr FormatSrting;

	FormatT(FormatStr formatSrting)
		:FormatSrting(formatSrting)
	{}
};

// Вспомогательная функция инициализирующая структуру FormatT.
template<FormatMode Mode, class FormatStr>
FormatT<Mode, FormatStr> Format(FormatStr formatStr)
{
	return FormatT<Mode, FormatStr>(formatStr);
}
// специализация вспомогательной функции для режима по умолчанию
template<class FormatStr>
FormatT<FmNormal, FormatStr> Format(FormatStr formatStr)
{
	return FormatT<FmNormal, FormatStr>(formatStr);
}

// Оператор % для потока и структуры FormatT, возвращает инициализированный форматтер
template<class Stream, FormatMode Mode, class FormatStr>
FormatParser<Stream, Mode, FormatStr> operator% (Stream &stream, FormatT<Mode, FormatStr> format)
{
	return FormatParser<Stream, Mode, FormatStr>(stream, format.FormatSrting);
}

Теперь форматтеров можно пользоваться так:
// уровень функциональности по умолчанию
cout % Format("A = %|+10|\n") % A;
// явно заданный уровень функциональности
cout % Format<FmMinimal>("A = %") % A;

Полный код примера, находящегося в архиве:
#define special_output_port (*((volatile char *)0x24))
#define special_input_port  (*((volatile char *)0x25))
#define special_abort_port  (*((volatile char *)0x22))
#define special_exitcode_port  (*((volatile char *)0x020))

#include <avr/io.h>
#include <tiny_ostream.h>
#include <flashptr.h>
#include <format_parser.h>

struct MyWriter
{
	void put(char c)
	{
		special_output_port = c;
	}
};

typedef IO::basic_ostream<MyWriter> ostream;
ostream cout;

FLASH char flash_str[] = "String form flash!!";
FLASH char format[] = "dec  =%|+10|\nhex  =%|#x10|\noct  =%|#o10|\nbool =%|#10|\n%\n";

inline ostream& operator<<(ostream &s, ProgmemPtr<char> str)
{
	s.puts(str);
	return s;
}

__attribute((OS_main))
int main()
{
	cout % IO::Format(FLASH_PTR(format)) % 12345 % 0x1234 % 012345 % true % FLASH_PTR(flash_str);
	special_exitcode_port = 0;
	return 0;
}

Резултаты для WinAVR (avr-gcc 4.3): размер кода 1400 байт, общее время выполнения теста — 6500 тактов (1624 бфйта / 6900 такта для avr-gcc 4.5.1).
Для IAR 5.5 for AVR — 1656 байт, 8081 такт.
Как видно, почти полноценный поток вывода, да ещё и с парсером форматной строки можно запихнуть меньше чем в 2 килобайта кода. Функция printf в такой объём влазит только в сильно урезанном виде, притом работает медленнее.
В итоге получили быстрый, компактный, типо-безопасный и расширяемый инструмент для организации форматного вывода.

  • +3
  • 10 ноября 2011, 23:53
  • neiver
  • 1
Файлы в топике: CppFormat.zip

Комментарии (22)

RSS свернуть / развернуть
Очень клёво! Респект!
И как показывает ваша практика IAR генерит бОльший бинарник чем gcc! Занятненько… =)

З.Ы. Сам пользуюсь готовым xprintf'ом от небезызвестного Elm Chan'а. Причем еще старой версией, оптимизированной под AVR и написанной на асме.
+1
IAR показывает более плохой результат из-за моего стиля кодирования. Компилятор IAR заточен на оптимизацию общих подвыражений и межпроцедурных вызовов. В моем коде практически не общих подвыражений, зато много inline функций, которые IAR оптимизирует хуже чем gcc. Если в коде будет много общих подвыражений, например, сгенирированных из макро функций, то результат будет обратный.
0
Спасибо за статью.
Статья заставляет задуматься о мудрости современников, а теги — о мудрости древних ;D
0
Не буду хвалить (каждый поэт сам знает себе цену).
Можно по подробней про: "… общих подвыражений и межпроцедурных вызовов".
Ну и применительно к компиляторам.
0
Насколько я понимаю, common subexpression — вариант оптимизации по размеру, когда компилятор находит идентичные куски кода и выносит их в отдельную процедурку. Инлайн наоборот, так сказать.
0
Отличная статья, +1. А почему Вы иногда используете struct для декларирования классов? Просто чтобы не писать лишний раз «public:» или есть особенности оптимизации?
0
Именно, чтоб не писать public: — других отличий нет.
0
Похоже все сделали вид, что все поняли. Разбередим омут уснувшей мысли :).
У меня такой вопрос. Имеется несколько переменных, для каждой из них предполагается свой форматный вывод в массив char[]. Т.е. для каждой переменной нужно создать свой поток, и написать свою реализацию —
struct MyWriter
{
        void put(char c)
        {
                special_output_port = c;
        }
};

?
И какой должна быть реализация для вывода в массив char[]?
0
Зачем на каждую переменную свой поток? Поток создается для устройства вывода — USART, USB endpoint, и тому подобное. Если нужен форматный вывод в текстовый буфер, в простейшем виде можно сделать так:

struct StringWriter
{
	StringWriter()
	{
		_position = 0;
	}
	inline void clear()
	{
		_position = 0;
	}
	inline void put(char c)
	{
		if(_position >= BufferSize)
			return;
		_buffer[_position++] = c;
	}
	inline char* str()
	{
		_buffer[_position] = '\0';
		return _buffer;
	}
protected:
	enum{BufferSize = 30};
	char _buffer[BufferSize];
	unsigned _position;
};

typedef IO::basic_ostream<StringWriter> ostringstream;
А пользоваться им так:
ostringstream str;
str << "Hello, world! " << 12345 << IO::endl;
DoSomethingWithString(str.str());
0
Т.е. для каждой переменной создаем экземпляр ostringstream? Может тогда передавать указатель на буфер и размер, в функцию StringWriter() и использовать один экземпляр?
P.S. Что означает
unsigned _position;
unsigned int?
0
И так можно использовать один экземпляр, только буфер он содержит у себя внутри, так удобнее. Буфер можнло почистить и использовать повторно.
Просто unsigned эквивалентно unsigned int, но на 4 символа короче :)
0
Отформатированные данные с текстовых буферов, динамически выводятся на графический дисплей в цикле, поэтому одним обойтись не получится. Ссылку и размер?
0
Если форматирование и вывод происходят в одном потоке, то можно и одним буфером обойтись. Если-же вывод на дисплей — в другом потоке или в прерывании, то — минимум два буфера.
0
Фоматирование и вывод разнесенные во времени прцессы. Т.е. любая из переменных может изменится в любой момент->переписываем соответствующий буфер.А драйвер вывода ничего об этом не знает (да ему и пофиг), он просто циклически выводит буферы.
0
Тогда, да — лучше передавать в StringWriter указатель на нужный буфер.
struct StringWriter
{
        StringWriter()
        {
                _position = 0;
        }
        inline void clear()
        {
                _position = 0;
        }
        inline void put(char c)
        {
                if(_position >= _bufferSize)
                        return;
                _buffer[_position++] = c;
        }
        inline char* str()
        {
                _buffer[_position] = '\0';
                return _buffer;
        }
        void SetBuffer(char buffer, unsigned size)
        {
            _buffer = buffer;
            _bufferSize = size;
        }
protected:
        unsigned _bufferSize;
        char *_buffer;
        unsigned _position;
};
0
Попробовал «на зуб» ваш форматный вывод. Создал такой файлик —
#include "tiny_ostream.h"
#include "flashptr.h"
#include "tiny_iomanip.h"
#include "format_parser.h"

struct StringWriter
{
  inline void put(char c)
  {
    if(Position >= BufferSize)
       return;
    Buffer[Position++] = c;
  }
  
  inline char* Str()
  {
    Buffer[Position] = '\0';
    return Buffer;
  }
  
  void SetBuffer(char *BufferIn, uint8_t BufferSizeIn)
  {
    Buffer     = BufferIn;
    BufferSize = BufferSizeIn;
    Position   = 0;
  }
  
  protected:
        uint8_t BufferSize;
        char    *Buffer;
        uint8_t Position;
};

typedef IO::basic_ostream<StringWriter> TStr;

TStr Str;

//----------------------------------
void Test(void)
{
  Str << "Opana";
}

Компилятор выдает —
Error[Pe020]: identifier «PtrT» is undefined D:\Eng\LibEmbed\FormatOut\flashptr.h 125
Error[Pe020]: identifier «PtrT» is undefined D:\Eng\LibEmbed\FormatOut\flashptr.h 125
Error[Pe114]: function «IO::basic_ostream<OutputPolicy, CharT, IOS>::FieldFill [with D:\Eng\LibEmbed\FormatOut\tiny_ostream.h 22
OutputPolicy=StringWriter, CharT=char, IOS=IO::basic_ios]» was referenced but not defined

Компилятор — IAR ARM 6.0
0
#include «flashptr.h» можно сразу вукинуть — эта штука сугубо для avr-gcc.
0
Выкинул. Осталось это —
Error[Pe114]: function «IO::basic_ostream<OutputPolicy, CharT, IOS>::FieldFill [with D:\Eng\LibEmbed\FormatOut\tiny_ostream.h 22
OutputPolicy=StringWriter, CharT=char, IOS=IO::basic_ios]» was referenced but not defined
0
А файл «impl\tiny_ostream.h» и его содержимое, который включается из «tiny_ostream.h» на месте?
0
Я все хидеры свалил в одну кучу, и грешным делом поправил выши файлики, вместо #include «impl\tiny_ostream.h» написал #include «tiny_ostream.h» (директория прописана в проекте).
0
Ну так реализации функций FieldFill, PutInteger и ииже с ними из «impl\tiny_ostream.h» включены в проект или нет?
«tiny_ostream.h» и «impl\tiny_ostream.h» — это два разных файлика, первый включает второй, в перовм — интерфейсная часть, во второй убрана реализация, чтоб глаза не мозолить.
0
Поправил. Вроде Done. Попробую в работе. Спасибо, удачи!
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.