Как стать автором
Обновить

Умный print для C

Время на прочтение3 мин
Количество просмотров19K

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

#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

Теги:
Хабы:
Всего голосов 67: ↑64 и ↓3+84
Комментарии30

Публикации

Истории

Работа

Программист С
37 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань