История
Еще в далекий 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 %d\n", '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, ""); }
Сразу скажу, что оно компилится
Что же получается? Функция обозначенная как 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 %d\n", 'v', 1); }
Победа!
Развязка — что случилось на самом деле или впихнуть невпихиваемое
Как водится, сначала показываем счастливый конец, а потом как все получилось. Ниже правильная реализация безопасного printf.
template< char... > struct TemplateLiteral { }; template< char... FORMAT, class... ARGS > int safe_printf_2(TemplateLiteral<FORMAT...>, ARGS... args) { constexpr char format[] = {FORMAT... , '\0'}; static_assert( checkFormat(format, args... ), "Format is incorrect"); return printf( format, args... ); } int main() { safe_printf_2(шаблонизировать_строку("%c %d\n"), '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..., '\0' >::Result Result; }; template< class T, std::size_t N > constexpr inline std::size_t sizeof_literal( const T (&)[N] ) { return N; } // функция и макрос для взятия N-ого элемента литерала template< std::size_t M > constexpr inline char getNthCharSpec( std::size_t N, const char (&literal)[M] ) { return N < M ? literal[N] : '\0'; } #define GET_Nth_CHAR_FOR_PP(I, N, LIT) ,getNthCharSpec(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 %d\n"), '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...>())) const& { return std::get<N>(*this); } template< std::size_t N > auto operator[](Number2Type<N>) -> decltype(std::get<N>(std::tuple<Ts...>())) & { return std::get<N>(*this); } }; 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..., '\0'})> { 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>. Вот так вот, надеюсь всем будет удобно.
Было бы еще, конечно, неплохо внести это в стандартную библиотеку…
UPDATE: лучше вместо макроса функцию использовать
UPDATE: Перенес топик в «Ненормальное программирование», думаю тут ему будет уютней.

