Самый правильный безопасный printf

в 11:25, , рубрики: c++, c++11, constexpr, static_assert, user-defined literals, variadic templates, метки: , , , ,

Под катом Вас ждет увлекательная история о том, как я сильно расстроился, познакомившись поближе с пользовательскими литералами (с нового стандарта), но при этом в последствии все же реализовал вышеупомянутую функцию, а также разобрался с constexpr, а позже еще и реабилитировал те самые литералы.

История

Еще в далекий 2009 год в интернете появились мифы о грядущих пользовательских литералах, которые разрешат делать абсолютно все, а именно парсить строку во время компиляции. (Кстати, спасибо ikalnitsky за апетит — рекомендую посмотреть перед прочтением.) Имеется ввиду тот их вариант, что с шаблоном. Но не тут то было. Реализация с шаблоном разрешена только для цифровых литералов. А это значит, что во время компиляции таким образом можно парсить только цифры.

Завязка. Первые шаги на пути решения

Тут я и расстроился. Но погуглив, узнал что можно и без шаблонов парсить строку во время компиляции.

Итак, часть решения задачи безопасного printf.

template< class > struct FormatSupportedType;

#define SUPPORTED_TYPE(C, T) 
template<> struct FormatSupportedType< T > { 
	constexpr static bool supports(char c) { return c == C; } }

SUPPORTED_TYPE('c', char);
SUPPORTED_TYPE('d', int);

template< std::size_t N >
constexpr bool checkFormatHelper(const char (&format)[N], std::size_t current)
{
	return
		current >= N ?
			true
		: format[current] != '%' ?
			checkFormatHelper( format, current + 1 )
		: format[current + 1] == '%' ?
			checkFormatHelper( format, current + 2 )
		:
			false;
}

template< std::size_t N, class T, class... Ts >
constexpr bool checkFormatHelper(const char (&format)[N], std::size_t current, const T& arg, const Ts & ... args)
{
	return
		current >= N ?
			false
		: format[current] != '%' ?
			checkFormatHelper( format, current + 1, arg, args... )
		: (format[current] == '%' && format[current + 1] == '%') ?
			checkFormatHelper( format, current + 2, arg, args... )
		: FormatSupportedType< T >::supports(format[current + 1]) &&
			checkFormatHelper( format, current + 2, args... );
}

template< std::size_t N, class... Ts >
constexpr bool checkFormat(const char (&format)[N], const Ts & ... args)
{
	return checkFormatHelper( format, 0, args... );
}

int main()
{
	static_assert( checkFormat("%c %dn", 'v', 1), "Format is incorrect" );
}

Логика работы, думаю, предельно ясна: перебираем возможные варианты и в зависимости от соотношений типов и символов возвращаем результат. Для проверки существования поддержки и соответствия символу используется дополнительный класс. Как результат ми можем во время компиляции проверить корректность формата (если он конечно известен на этапе компиляции). Далее используя классический printf напечатаем результат.

template< std::size_t N, class... ARGS >
int safe_printf(const char (&format)[N], ARGS... args)
{
	static_assert( checkFormat(format, args... ), "Format is incorrect" );
	return printf( format, args... );
}

Но мой gcc-4.7 не хочет это кушать! Я вдруг решил расстроиться еще раз, но пришло озарение. Для продвижения дальше нам необходимо понять constexpr. Ниже, я думаю, наиболее интересная часть статьи.

Кульминация. Понимание constexpr

Что было раньше? Раньше были этап компиляции и этап выполнения, нужно также заметить (хотя и все это знают), что типизация происходит на этапе компиляции.
Что есть сейчас? Сейчас есть constexpr, который разрешает выполнять функции на этапе компиляции — какой-то каламбур выходит. Нам нужно ввести уточняющие определения: будем рассматривать не просто компиляцию и выполнение, а компиляцию и выполнение конкретных частей программы (в нашем случае функций, потому как еще можно и объекты во время компиляции использовать). Например «компиляция функции f», «время выполнения функции f», «компиляция всего проекта», «время выполнения проекта».
То есть теперь этап компиляции всего проекта разбился на компиляции и выполнения различных единиц проекта. Рассмотрим пример

template< int N >
constexpr int f(int n)
{
	return N + n;
}

int main()
{
	constexpr int i0 = 1;
	constexpr int i1 = f<i0>(i0);
	constexpr int i2 = f<i1>(i1);
	static_assert(i2 == 4, "");
}

Сразу скажу, что оно компилится но ничего полезного не делает. Рассмотрим ближе процесс компиляции функции main(). Сначала переменной i0 присваивается значение, далее эта переменная используется для вычисления значения переменной i1, но для того чтобы его вычислить нам нужно выполнить функцию f<i0>(i0), но для этого нужно ее скомпилировать, а для компиляции ей нужно значение i0. Аналогично с f<i1>(i1). То есть мы имеем следующее: Процесс компиляции функции main() содержит в себе последовательною компиляцию функции f<1>(int), затем ее выполнение, затем компиляцию функции f<2>(int), и, соответственно, ее выполнение.
Что же получается? Функция обозначенная как constexpr ведет себя как самая обычная функция. Посмотрим на функцию f: N известно на этапе ее компиляции, а n — на этапе ее выполнения.

Развязка. Реализация безопасного printf

Вот почему это не хотелось компилироваться!

template< class... ARGS >
int safe_printf(const char* format, ARGS... args)
{
	static_assert( checkFormat(format, args... ), "Format is incorrect");
	return printf( format, args... );
}

static_assert разрешается на этапе компиляции функции safe_printf, а format будет известен только во время ее выполнения (даже если для чего-то другого в этот момент буде этап компиляции).
И как же это обойти? А ни как, или вставить символы формата в параметры шаблона, чтоб они были видны на этапе компиляции (а как мы помним применение пользовательских литералов не дает возможности это сделать) или вспомнить о том, что когда все супер крутые, могучие и непобедимые средства С++ (и даже С++11) становятся беспомощными, на сцену выходят макросы!

#define safe_printf(FORMAT, ...) 
	static_assert(checkFormat( FORMAT, __VA_ARGS__ ), "Format is incorrect"); 
	printf(FORMAT, __VA_ARGS__)

int main()
{
	safe_printf("%c %dn", 'v', 1);
}

Победа!

Развязка — что случилось на самом деле или впихнуть невпихиваемое

Как ведется сначала показываем счастливый конец, а потом как все получилось. Ниже правильная реализация безопасного printf.

template< char... > struct TemplateLiteral { };

template< char... FORMAT, class... ARGS >
int safe_printf_2(TemplateLiteral<FORMAT...>, ARGS... args)
{
	constexpr char format[] = {FORMAT... , ''};
	static_assert( checkFormat(format, args... ), "Format is incorrect");
	return printf( format, args... );
}

int main()
{
	safe_printf_2(шаблонизировать_строку("%c %dn"), 'v', 2);
}

Те есть функции передаеться переменная, ТИП которой нам интересен (а НЕ значение), и аргументы, которые нужно вывести. Осталось реализовать механизм для превращения литерала в шаблон. В идеале было бы круто если в контексте в котором существует литерал был бы еще pack индексов для этого литерала (что-то типа enumerate), чтоб его потом роспаковать, то есть

template< std::size_t... INDXs >
//...
	TemplateLiteral<"some literal"[INDXs]...>
//...

Но длина литерала и длина pack'а должны совпадать, а поскольку pack можно ввести только снаружы, то и литерал должен быть передан снаружы, а если он передается снаружы (но еще НЕТ механизма засунуть его в шаблон как параметр), то он передается как простой аргумент функции, и поэтому не известен на этапе компиляции функции в которой он должен завернуться в шаблон, поскольку шаблоны — это типы, а типы — это компиляция — короче, так нельзя.
Но вспомним снова о макросах. Можно попросить boost::preprocessor сгенерировать список номеров. Конечно же их количество будет статическое, а изменить его можно будет только на этапе препроцессинга. Еще нужно предусмотреть что взятие элемента по индексу у литерала на этапе компиляции контролируется, по-этому нужно предусмотреть какой-то защитный механизм, и, также, нужно будет почистить конец строки. А еще нужно как то проверять все ли строка захватилась, т.е. не ввел ли программист слишком длинный литерал. Ниже код.

template< char... > struct TemplateLiteral { };

// эта структура убирает лишние нули в конце;
// также ей передается длинна исходного литерала, чтоб потом сверить
template< std::size_t LEN, char CHAR, char... CHARS >
struct TemplateLiteralTrim
{
private:
	// структура для рекурсивного удаления нулей
	// первый параметр - флаг, отличный ли следующий символ от нуля 
	template< bool, class, char... > struct Helper;
	template< char... C1, char... C2 >
	struct Helper< false, TemplateLiteral<C1...>, C2... >
	{
		// сверяем длину, 
		static_assert(sizeof...(C1) == LEN, "Literal is too large");
		typedef TemplateLiteral<C1...> Result;
	};
	template< char... C1, char c1, char c2, char... C2 >
	struct Helper< true, TemplateLiteral<C1...>, c1, c2, C2... >
	{
		typedef typename Helper< (bool)c2, TemplateLiteral<C1..., c1>, c2, C2...>::Result Result;
	};
public:
	typedef typename Helper<(bool)CHAR, TemplateLiteral<>, CHAR, CHARS..., '' >::Result Result;
};

template< class T, std::size_t N >
constexpr inline std::size_t sizeof_literal( const T (&)[N] )
{ return N; }

// макросы для взятия N-ого элемента литерала
#define GET_Nth_CHAR_SPEC(N, LIT) (N < sizeof_literal(LIT) ? LIT[N] : '')
#define GET_Nth_CHAR_FOR_PP(I, N, LIT) ,GET_Nth_CHAR_SPEC(N, LIT)

// передаем количество символов и литерал
// количество символов на этапе препроцессинга неизвестно,
// по-этому нужно передавать заведомо достаточно большое число,
// иначе не скомпилируется
#define TEMPLATE_LITERAL_BASE(MAX, LIT) 
	(typename TemplateLiteralTrim< sizeof_literal(LIT) - 1 
	BOOST_PP_REPEAT(MAX, GET_Nth_CHAR_FOR_PP, LIT) >::Result())

// MAX_SYM можно задефайнить перед инклудом хедера с этим кодом
#define TEMPLATE_LITERAL(LITERAL) TEMPLATE_LITERAL_BASE(MAX_SYM, LITERAL)

int main()
{
	// вуаля
	safe_printf_2(TEMPLATE_LITERAL("%c %dn"), 'v', 2);
}

Кстати, очень для меня было интересно посмотреть в boost::preprocessor — я не представлял себе что такое им можно делать (как, например, арифметические операции). Так что макросы действительно страшная сила.

Невошедшие кадры. Реабилитация пользовательских литералов

Пришло время показать за что я все же начал их (литералы) уважать. Когда-то очень давно, около двух лет назад, я узнал о кортежах. Очень уж удобными они мне показались, НО кортежи эти были из Питона, Немерла и Хаскеля. А когда я узнал о кортежах из С++, меня очень расстроил std::get<N>(tuple) — фу как громоздко, подумал я, и с тех пор хотел разработать механизм для получения элемента, но через оператор квадратных скобок. И вот тут вот на помощь пришли пользовательские литералы.

template< std::size_t > struct Number2Type { };

template< class... Ts >
class tupless: public std::tuple<Ts...>
{
public:
	template< class... ARGS >
	tupless(ARGS... args): std::tuple<Ts...>(args...) { }
	template< std::size_t N >
	auto operator[](Number2Type<N>) const ->
		decltype(std::get<N>(std::tuple<Ts...>())) consttemplate< std::size_t N >
	auto operator[](Number2Type<N>) ->
		decltype(std::get<N>(std::tuple<Ts...>())) };

template< std::size_t N >
constexpr std::size_t chars_to_int(const char (&array)[N],
	std::size_t current = 0, std::size_t acc = 0)
{
	return (current >= N || array[current] == 0) ?
			acc
		  : chars_to_int(array, current + 1, 10 * acc + array[current] - '0');
};

template<char... Cs> constexpr auto operator "" _t() ->
	Number2Type<chars_to_int((const char[1 + sizeof...(Cs)]){Cs..., ''})>
{
	return {}; // прошу обратить на это внимание
};

int main()
{
	tupless<char, int, float> t('x', 10, 12.45);
	safe_printf_2(TEMPLATE_LITERAL("%c %d %f"), t[0_t], t[1_t], t[2_t]);
}

Что тут интерестного? Ну во первых для того что бы два раза не писать тип, который возвращает литерал (а именно тип нам интересен), был использован пустой список инициализации, а компилятор попробует его привести к объекту нужного типа и сам вставит туда конструктор.
Данный пользовательский литерал очень интересен тем, что его тип напрямую зависит от значения, т.е. например тип литерала 2_t будет Number2Type<2>. Вот так вот, надеюсь всем будет удобно.
Было бы еще, конечно, неплохо внести это в стандартную библиотеку…

Автор: dima_mendeleev

Поделиться

* - обязательные к заполнению поля