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

    Под катом Вас ждет увлекательная история о том, как я сильно расстроился, познакомившись поближе с пользовательскими литералами (с нового стандарта), но при этом в последствии все же реализовал вышеупомянутую функцию, а также разобрался с 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 %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, "");
    }
    

    Сразу скажу, что оно компилится но ничего полезного не делает. Рассмотрим ближе процесс компиляции функции 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 %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: Перенес топик в «Ненормальное программирование», думаю тут ему будет уютней.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 36

      +4
      Ок, аргумент в пользу Haskell принят.
      • UFO just landed and posted this here
          0
          Я к тому, что type matching на этапе компиляции в Хаскелле сильно естественнее выглядит.
          • UFO just landed and posted this here
      • UFO just landed and posted this here
          0
          Я бы с удовольствием, а куда нужно обратиться? Может кто-то уже знает как?
          • UFO just landed and posted this here
          +2
          А почему нельзя сделать подобный код:

          template< class T >
          constexpr inline auto GET_Nth_CHAR_SPEC(N, const T (&LIT)[] ) -> decltype(T)
          { return N < sizeof_literal(LIT) ? LIT[N] : '\0'; }
          

          Я правда не уверен, что все правильно написал, но смысл в том, чтобы вместо макроса шаблонную функцию сделать.
            0
            Конечно можно, это даже лучше будет, поскольку литерал не будет копироваться еще раз.
              0
              Ладно, а почему вы так тогда не сделали? Вариант — для облегчения читабельности кода не прокатит, этому коду уже ничем не поможешь :)
              Просто интересно, в контексте бурных холиваров о вреде макросов.
                0
                Что-то на волне с макросами забыл о существовании функций))) Может по-этому они зло
                  0
                  Странно, я как посмотрел на этот фрагмент кода:

                  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] : '\0')
                  

                  Так у меня возник некий дискомфорт, в связи с неединообразностью стиля очень похожих вещей.
                  Но вообще бывает, я сам помню generic контейнеры на пуре си на макросах писал, знаю, как оно увлекает :)
            –7
            Отвратительное решение проблемы. Всё делается гораздо проще, берётся исходники printf из CRT и уже прямо в ней (на чистом C) добавляются небольшие проверки валидности аргументов, на всё уйдёт пару часов с тестированием (моё время). Причём можно сразу решить, что делать, например, если кл-во аргументов не соответствует кол-ву спецификаторов: ошибку возвращать, игнорировать, пропускать неподходящие/лишние и т.п.
              +8
              я примерно понял о чем речь, но по-моему вы имеете ввиду рантайм
                –1
                А вы уверены, что лицензия вам позволяет «берётся исходники printf из CRT»?
                Неговоря уже о том, что в статье речь идет о времени компиляции.
                  0
                  Да, лицензия GPL позволяет это делать.

                  Да, в статье идёт речь немного о другом, но вариант с правкой библиотечной printf гораздо эффективнее и универсальнее. Представь, что форматирующая строка не может быть заранее задана, до компиляции программы, а может меняться значительно позже релиза, например, это часть пользовательской локализации интерфейса.
                    0
                    Сначала про эффективнее. Разработка? Исполнение-то явно замедлится.

                    Потом про «форматирующая строка не может быть заранее задана».
                    Если кратко, то так делать нельзя.
                    Если не сильно кратко, то во-первых пример плохой, т.к. при локализации часто требуется переставлять аргументы местами, чего с printf сделать не получится. Тогда уж boost::format.
                    Во-вторых, это же классическая атака на программы на Си. Как только у вас появляется printf(format, args), вы можете сделать дамп памяти, подсунув в format (пользовательскую локализацию, ок) длинную строку с одними только "%s".
                      0
                      Не очень понял комментарий, это вопрос или нет, мне или нет? :-\

                      Но, в общем случае, если уж решили модить printf, то естественно "%s" делается безопасным. Собственно, если касается дело локализации, то изменяемый снаружи только «format», а «args» скомпилирован жёстко.
                        0
                        Про эффективнее — вопрос, остальное — возражение.
                        О том и речь, что скомпилированные args — это не гибко, а задавать формат опасно в принципе при использовании printf. Что касается, «сделаем printf безопасным и потом будем локализовывать именно так», то это странно, т.к. есть более правильные способы.
                        Ну и модят printf на этапе компиляции, иначе он будет медленно работать.
                +3
                Компилятор gcc умеет статически проверять соответствие форматной строки и аргументов в printf.
                  +1
                  Не только gcc, есть еще много утилит для статического анализа кода, но это совсем не то, это другой уровень абстракции: код проверяют снаружи, этот же подход разрешает делать это изнутри.
                    0
                    Не знаком с этим новым значением слова «абстракция» :)
                    Подход как подход кстати — свои достоинства есть что у статического анализа что у подобных проверок во время компиляции.
                    Но честно говоря к специализированным инструментам у меня доверия больше.
                      +3
                      Больше, чем к коду, написанному по стандарту языка, и который в общем-то, вы можете изучить сами и даже модифицировать?
                        –6
                        Подозреваю, что изучить или даже модифицировать мне будет проще как раз исходники gcc или clang.
                  +1
                  Как же вы живёте-то? Вам за что зарплату платят?
                    +12
                    Я никогда не в восторге от кода, который выглядит сложным более чем для 50% программистов.
                    И реализация, и использование — усложнено.
                    Больше сложности — больше места для ошибки.

                    Но как разминку для ума — одобряю :-)
                      –5
                      Для меня этот код не сложен, поэтому ошибок там нет=)
                      С использованием согласен: вариант с шаблонизацией литерала в случае неправильного использования из-за препроцессинга дает далеко-не-сразу-понятные ошибки.
                      Ну а все остальное, а особенно кортежи, по моему проще пареной репы.
                        +6
                        Вы не с той стороны смотрите на проблему. Написать код — это 10% работы.
                        Остальные 90% — поддерживать его на протяжении следующих 20 лет, когда вам лично уже будет не до него.
                          0
                          Ну вы знаете, в большей части boost'а чёрт ногу сломит, однако же его можно и нужно использовать.
                            0
                            Верно. Но у буста есть саппорт какой-никакой, и он обеспечивает поддержку этого безумства в долгосрочной перспективе :-)
                              0
                              Поддержка безумства О_о
                              5+
                    • UFO just landed and posted this here
                        0
                        С потоками согласен, я вообще ненавижу С++ не использую printf.
                        А действительно дополнительное заворачивание в TEMPLATE_LITERAL выглядит громоздко?
                      • UFO just landed and posted this here
                          +1
                          Даже в текущем виде constexpr — головная боль для создателей компилятора. По словам разработчиков Clang это самая сложная для реализации часть C++11, и просили ни в коем случае не расширять эту функциональность.
                            0
                            Ну как же нельзя, я же уже писал об этом. Создаем pack длинны кортежа, а далее подставляем в функцию взятия элемента:
                            template<int... INDXs>
                            T apply(FuncType f, tuple t)
                            {
                                return f(get<INDXs>(t)...);
                            }
                            

                            Примерно так.

                          Only users with full accounts can post comments. Log in, please.