Умный print для C +80


AliExpress RU&CIS

Пример использования:

#include "print.h"

int main() {
  print("number:", 25,
    "fractional number:", 1.2345,
    "expression:", (2.0 + 5) / 3
  );
}
number: 25 fractional number: 1.2345 expression: 2.33333

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

Дженерик print, может выводить все основные типы языка Си, целые, со знаком и без, с плавающей точкой и указатели:

char *s = "abc";
void *p = main;
long l = 1234567890123456789;
unsigned char byte = 222;
char ch = 'A';
print("string:", s, "pointer:", p, "long:", l);
print(byte, ch)
string: "abc" pointer: 0x402330 long: 1234567890123456789
222<0xDE> 'A'65

Разные типы отображаются разным цветом, палитру можно настроить, либо вообще отключить цвет.

Можно даже печатать массивы:

int x[] = { 1, 2, 3 };
char *args[] = { "gcc", "hello.c", "-o", "hello" };
print(x, args);
[1 2 3] ["gcc" "hello.c" "-o" "hello"]

Как это работает? На самом деле print это макрос, точнее говоря variadic macro, который генерирует вызов настоящей функции. Первый параметр, который макрос конструирует для этой функции, это количество аргументов, введённых пользователем. Для этого используется известный трюк:

void __print_func(int count, ...);
#define count_arg(q,w,e,r,t,y,...) y
#define print(a...) __print_func(count_arg(a,5,4,3,2,1,0), a);

Элегантностью такое решение не блещет, спасибо ограничениям препроцессора, как видите, максимальное количество аргументов в этом примере 6, (в моей библиотеке сейчас 26 ).

Второй параметр spread operator ... , это сам список всех аргументов. В функции __print_func()используется обычный stdarg.h для обхода этого списка:

void prn(int count, ...) {
  va_list v;
  va_start(v, types);
  for (int i = 0; i < count; i++) {
    ...
    printf("%'li", va_arg(v, unsigned long));
    ...
  }
  va_end(v);
}

Теперь, сложный вопрос: как узнать типы? Ведь va_argне волшебник, мы ему должны указать тип для каждого аргумента. В примере выше -- это unsigned long, но, что на самом деле пользователь передаст, мы ещё не знаем.

Большинство компиляторов Си понимает такую вещь:

int x;
int y = __builtin_types_compatible_p(typeof(x), int);

Это конструкция времени компиляции, принимает типы, а возвращает булевое значение, в данном примере y будет равен 1 или true потому что int == int.

Ещё, есть такой вызов, как __builtin_choose_expr(a, b, с). Это аналог a ? b : c времени компиляции, с помощью этих расширений компилятора можно написать, что-то наподобие свитча, который возвращает тип переменной в виде числа, 3 для int, 2 для double и т.д.:

#define __get_type(x)   __builtin_choose_expr(__builtin_types_compatible_p(typeof(x), double), 1,   __builtin_choose_expr(__builtin_types_compatible_p(typeof(x), char), 2,   __builtin_choose_expr(__builtin_types_compatible_p(typeof(x), int), 3,   __builtin_choose_expr(__builtin_types_compatible_p(typeof(x), void*), 4,   ....... и так далее

Далее, применяя стандартные трюки с variadic macro, то есть, пишем __get_type(), много или, точнее, count раз через запятую, создаём массив char types[], и подставляем его вторым параметром в вызов функции печати, её заголовок станет таким:

void __print_func (int count, char types[], ...) {

Теперь мы можем смело брать аргументы с помощью va_arg, подглядывая их типы из массива:

for (int i = 0; i < count; i++) {
  if (types[i] == 'd') {
    double d = va_arg(v, double);
    printf("%'G", d);
  }
  else if (types[i] == 'i') {
    int d = va_arg(v, int);
    printf("%'i", d);
  }
  ...
}

На самом деле, чтобы печатать массивы, надо ещё передавать sizeof(), что выглядит, примерно, так:

(short[])(sizeof(a), sizeof(b), sizeof(c),.........)

Для экономии тип и размер упаковываются в unsigned short: __get_type(x) + sizeof(x) << 5.

Вся работа препроцессора и builtins компилируется очень эффективно, вот такой вызов:

print(42, 42);

Компилируется gcc -O1в такой код:

xor     eax, eax
mov     ecx, 42
mov     edx, 42
lea     rsi, [rsp+12]
mov     edi, 2
mov     DWORD PTR [rsp+12], 0x00840084
call    __print_func

Описанные выше расширения поддерживают компиляторы GCC 5.1+, Clang3.4.+1, Intel C 17.0.0+, и TinyC. На MSVC их нет, возможно, есть похожие, но мне не удалось найти соответствующей информации.

Вот как рисуется цвет:

void __print_color(int a) {
  if (!__print_enable_color) return;
  if (a == -1) printf("\x1b(B\x1b[m");
  else printf("\x1b[38;5;%im", a);
}

Поменяв значение глобальной переменной __print_enable_color на 0можно отключить цветной вывод. А функция __print_setup_colors() позволяет задать палитру:

void __print_setup_colors(int normal, int number, int string, int hex, int fractional) {

Надо будет ещё добавить автоматическое отключение цвета если stdout не консоль, а файл или pipe.

Есть fprint(fd...) для работы с stderr и любыми дескрипторами.

Возможно, у вас вопрос, почему не _Generic, а __builtin_types_compatible_p? Дело в том, что _Generic не отличает массивы от указателей, например int* для него то же самое, что и int[]поэтому с _Generic выводить массивы бы не получилось.

> Ссылка на github

Теги:




Комментарии (28):

  1. kloppspb
    /#22736016

    и больше не будет предупреждений компилятора, о том, что символ формата после "%" неверного типа.

    Чего ж в этом хорошего?

    Ну и потом, printf() хороша как раз своим f, то бишь возможностью задавать форматирование вывода (да, недостатки есть, но сейчас не о них). Здесь же такая возможность отсутствует полностью. Зато присутствует принудительная вставка пробелов до и после значений.

    Не очень понятна область применения. Хотя… Как компонент какого-нибудь отладочного дампера — вполне может быть. Тут как раз и цвет для разных типов пригодится, наверное.

    P.S.
    $ gcc -Wall demo.c                                                     ^
    In file included from demo.c:1:0:
    demo.c: In function ‘main’:
    print.h:153:40: warning: left-hand operand of comma expression has no effect [-Wunu
    sed-value]
     #define __print_push(c,size,cont) (cont, *--_p = c | (size << 5))
    
    ...
    

    • bolk
      /#22736746

      Для отладки — самое то, удобно.

    • 4p4
      /#22737080

      Не скажете данные платформы? Версия компилятора и ОС.

      • kloppspb
        /#22741264

        $ cat /etc/*-release
        DISTRIB_ID=LinuxMint
        DISTRIB_RELEASE=19.3
        DISTRIB_CODENAME=tricia
        ...
        


        $ gcc --version
        gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
        

    • AVI-crak
      /#22741220

      Чего ж в этом хорошего?

      Думать не нужно.

  2. ksergey01
    /#22736254

    Прикольный костыль, но printf все равно лучше. Хотелось бы только без спецификаторов типа в строке форматирования, но тут без c++ не обойтись

  3. BratSinot
    /#22736514

    что символ формата после "%" неверного типа.

    Просто хочу напомнить, что stdint.h с uint32_t / uint_least16_t, и inttypes.h с PRIu32 / SCNuLEAST16 и т.д. появились еще в C99 (и по наследству в C++11). Да это может не очень удобно (т.к. format строку приходится писать как "%«SCNuLEAST16), но все равно все понятно.

  4. qrdl
    /#22736568

    Интересная статья.

    Пара комментов:

    1. Почему в качестве идентификатора типа не использоваться сразу format specifier? То есть вместо 'd' для double сразу писать "%lf"? Тогда можно избавиться от многоэтажного if… if else ..., а сразу использовать нужный формат — и код проще, и работает быстрее.

    2. Действительно ли есть необходимость печатать массивы? Просто _Generic() — это C11, все нормальные (я не про MVSC сейчас) компиляторы его поддерживают, а __builtin_types_compatible_p() — расширение GCC. К тому же по крайней мере массивы char'ов _Generic() вполне нормально должен понимать, я его активно использую (с GCC), и проблем с этим не возникало.

    3. Не скажу, что это прям реально нужно для печати, но я вот использую аналогичный механизм, когда мне надо к SQL statement'у прибайндить всякие значения, и намного удобнее написать что-то типа sql_bind(stmt, foo, 5, «bar», 7), чем несколько отдельных bind_int(), bind_text() и т.д.

    4. Еще это дико полезно, когда есть свои типы, с которыми хочется уметь работать, «как с родными». Например, я использую в одном проекте свои «умные строки», и хочется не вспоминать каждый раз, «умная» это строка или нет, а просто указывать ее в качестве параметра тому же bind'у.

    • 4p4
      /#22737052

      Спасибо!


      сразу писать "%lf"?

      1. Так сначала и было, но потом возникли идеи с цветом, с печатью char и как символа и как числа, к тому же так любой пользователь может просто открыть интересующий "этаж if/else" и сделать так как ему надо (кто-то любит всё hex).

      необходимость печатать массивы?… _Generic() — это C11

      1. В библиотеке над которой я работаю, очень удобно печатать массивы. Подумаю сделать #ifdef на будущее и использовать _Generic там где не обнаружится поддержки builtin-ов.

      sql_bind(stmt, foo, 5, «bar», 7)

      1. Однозначно да, биндить к запросу очень было бы удобно. Подумаю, как-нибудь, можно ли генерализовать это в библиотеку, скажем va_arg_generic.

      свои типы, с которыми хочется уметь работать, «как с родными».

      1. Вот ещё даже не пробовал совсем ничего такого с user-defined types, алиасами, структурами, работает?

      • qrdl
        /#22737076 / +1

        Вот ещё даже не пробовал совсем ничего такого с user-defined types, работает?
        С _Generic в GCC точно работает, в clang вроде тоже пробовал, но точно не помню. По идее, с __builtin_types_compatible_p() тоже должно работать.

  5. AleksandrRd
    /#22736972

    В итоге должен получится Python c синтаксисом С?

  6. wigneddoom
    /#22739358

    Хорошая, годная магия. Но вот если бы язык развивался, и в коммитете занимались бы делом, то давно можно было бы сделать что-то подобное плюсовой libfmt или питоновскому fmt. Вынести это на уровень компилятора и радоваться.

    • locutus
      /#22741988 / -1

      Пожалуйста, не надо. Комитет Си отлично справляется со своей работой и поддерживает нормальное поступательное развитие того, что действительно нужно.

    • Gryphon88
      /#22749454

      Зря Вы так, комитет нормально работает, медленно только. Из С можно сделать плюсы, IronPython или какой другой язык, насыпать полный пакет сахара — но всё это испортит язык, как и любое предложение «давайте сделаем как в...» вместо «нужно решить такую-то проблему, вот возможные варианты».

      • wigneddoom
        /#22756000 / -1

        На мой взгляд комитет никак не работает, а начиная с С99 работает только во вред.


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


        Это просто жесть какая-то, один из самых используемых языков в мире совсем не развивается.


        Комитет — это про лебедь, рак и щуку. У них нет концепции развития языка.
        Вот _Generic, сделали сугубо для математической библиотеки.
        Нити добавили в стандартную библиотеку и тут же обгадились, что нельзя было pthreads засунуть?
        Анонимные структуры и объединения добавили, но опять полумеры. Ну если вы тырите идеи из Ken C, так и вставляйте целиком.

        • Gryphon88
          /#22759312

          Радуйтесь, сейчас комитет работает над модульностью. Вообще С он как айсберг, в большинстве своём состоит из legacy, даже я со своим небольшим опытом видел работающий код на K&R C (и переписывал его на С99 в связи с заменой оборудования). Туда очень что-то сложно добавить, чтобы олды не заплевали. Кто хочет новизны, берёт подмножество раста или плюсов.

  7. Almighty_Goose
    /#22739514 / +1

    Очень круто, прямо как паскалевский println!
    Но просто печать на экран, обычно, не так востребовано, как печать в строку, а-ля boost::format.
    Вот, если бы так просто можно было бы делать печать в std::string — вот это было бы гораздо интереснее.
    Думаю,
    auto str = sprint(x, y, z);
    будет лучше, чем
    std::string str = str( boost::format("%f %f %f") % x % y % z );

    • 4p4
      /#22739542 / +1

      Ну, в статье речь про Си, а вы про С++, что удивительно, мне казалось, что со всеми наворотами С++ там уже давно не проблема сделать такое. Думаю на С++ можно намного круче сделать, например дать возможность пользователю расширять список принимаемых типов с помощью конструкторов. Но я лет 10 на С++ не смотрел.

      • Almighty_Goose
        /#22739626

        Ну, на C++ действительно можно просто понаделать макросов вида:
        #define sprint(x, y, z) (sstream() << x << ' ' << y << ' ' << z).str()
        Что выглядит очень уродливо. И тут же возникает вопрос: А как же шаблонная магия, бро?
        Может быть, где-то сделано красивее, однако, я окуклился в std и boost и ничего больше не вижу.
        P.S.: Честно говоря, я только сейчас заметил в заголовке «Си». Эх, ребята на Си стараются, хотя то же самое уже давно сделано в std или boost. И Си самый распространенный язык… Однако, обратный переход с C++ на Си очень сложный.

        • encyclopedist
          /#22739672

          Начиная с C++20 в стандартной библиотеке будет std::format. Пока её не дрбавили в популярные реализации, можно пользоваться fmtlib, на основе которой и был сделан std::format.

          • wigneddoom
            /#22739922 / -1

            Вот это боль, я уже на радостях начал в std::format и облом. Когда они уже научатся выпускать стандарт так, чтобы хотя бы в двух компиляторах это поддерживалось?


            Иногда думаешь, что как хорошо в питонах и растах с единственной реализацией.

      • BalinTomsk
        /#22740280

        В С++ есть потоки. cout << «preved» << 12.5 << «medved» << 5 << "\n";

  8. apevzner
    /#22739914 / +1

    Большинство компиляторов Си понимает такую вещь:

    int x;
    int y = __builtin_types_compatible_p(typeof(x), int);



    Большинство — это gcc и примкнувший к нему clang?

  9. AVI-crak
    /#22740852

    Написал нечто похожее, у меня называется printo(). Тоже печатает почти всё, ну кроме перечислений — уж очень дико код раздувается.
    github.com/AVI-crak/Rtos_cortex/blob/master/sPrint.h
    #define dpr_(X) _Generic((X), \
    const char*: soft_print_con, \
    char*: soft_print, \
    uint8_t: print_uint8, \
    uint16_t: print_uint16, \
    uint32_t: print_uint32, \
    uint64_t: print_uint64, \
    int8_t: print_int8, \
    int16_t: print_int16, \
    int32_t: print_int32, \
    int64_t: print_int64, \
    float: print_float, \
    double: print_double, \
    default: print_nex \
    )(X)

    • locutus
      /#22742000

      Сделал нечто похожее в свое время, ещё натянул на это сверху вариабельные макросы. Прошло время, и я понял всю прелесть стандартного printf с форматами

    • 4p4
      /#22742664

      Посмотрите, как у меня сделано в репозитории, код не раздувается, просто, если сильно упрощёно сказать, вместо a, b, c, d надо сделать a(b(c(d)))) в итоге генерируется не больше кода чем при обычном вызове printf а, кажется, даже меньше. Я много игрался с compiler-explorer, чтобы понять, какой компилятор, чего генерирует. Не стал об этом в статье подробно писать, может зря, это, возможно, самая интересная часть. Макросы вызываются рекурсивно, и являются выражениями, и если один из них натыкается на последний аргумент (void)0 он последущие не вызывает, такой currying. В результате, сколько аргументов вы дали, столько кода и сгенерируется, а в вашем случае всегда кладется в стек все 25 аргументов (или сколько там их прописано в макросе).

  10. AVI-crak
    /#22743732

    Посмотрите, как у меня сделано в репозитории, код не раздувается

    Стоп, ваша обёртка попадает в бинарник?
    _Generic((X) и перечисление аргументов выполняются на уровне перепроцессора, это вообще не попадает в бинаркик. Обёртка из макросов получается большой, тут я согласен. Но думаю что имеет смысл один раз написать, и больше никогда не трогать.
    Кстати, макросы не раскрываются даже на нулевом уровне оптимизации, с поставленной галочкой для макросов.