Пример использования:
#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
выводить массивы бы не получилось.