Функция print на Си, принимающая любые аргументы в любом количестве
Функция print на Си, принимающая любые аргументы в любом количестве

О себе

Я сам программист на C++, вернее я только начинающий, без коммерческого опыта. Изначально я познакомился с языком Си, а C++ открыл как мощное расширение языка C. В нем на мой взгляд добавлены те необходимые полезные вещи, которых нет в C (перегрузка функций, классы, пространства имен и др), при этом эти вещи отлично расширяют философию языка

Задумка

Я узнал что в C стандарта 2011 года добавили небольшую возможность "перегрузки" функций с помощью макроса. (Generic selection) Мне, очень интересно стало написать какую-нибудь функцию, которая максимально использовала бы эту возможность

Задумка: написать (макро) функцию print, которая выводит через пробел все переданные в нее аргументы. Звучит невероятно для Си, где обычно указывают тип принимаемого аргумента одной буквой в имени, и явно указывают число переданных аргументов. Но с джейнериками из C11 это возможно

Про бывшие проблемы с форматированием данной статьи

Этот текст под спойлером не имеет прямого отношения к теме статьи

Это моя первая статья на сайте, и с ней был технический сбой. Вот несколько моментов, которые надеюсь помогут разработчикам Хабра найти баг:

  1. Писал я пост в режиме песочницы. Все отображалось норамльно. Была пара багов, которые вряд ли имеют отношение к сбою в форматировании, но лучше укажу:

    1. Красное сообщение в шапке с просьбой подтвердить почту не проподало до перезахода в аккаунт

    2. Некоторые блоки кода схлопывались, и не отображались до обновления страницы

  2. После публикации статьи она помечалась как "на модерации", но при этом все форматирование было норамльным

  3. После прохождения модерации статья преобрела следущий ужасный вид с очень необычными багами:

    1. Заголовки спойлеров уехали под спойлеры

    2. Все __VA_ARGS__ и __VA_OPT__ превратились в VA_ARGS иVA_OPT. При этом с __LINE__ и __FILE__ ничего не произошло

    3. Самое нечитаемое: все переносы строк с символом \ пропали, и макросы развернулись в одну строку. В итоге код на 20 строк превратился в 5 строк, что очень нечитаемо и самое неприятное

    4. Символ & заменися на &amp;, а < заменился на &lt; или что-то подобное

    5. Текст из-под описания картинок продублировался отдельным блоком со строкой

  4. Честно, не ожидал что сразу наберется так много просмотров (около 700) за пару часов. И все эти люди увидели этот сбой. Но при этом как-то прочитали статью

  5. Так же мне пригодился скриншот, который я как раз сделал на всякий случай, вдруг все поломается. Я ссылку на него оставил, когда исправлял форматирование. Как в воду глядел! :) png версия с нормальным форматированием

Простой пример с одним аргументом

Введу в суть работы этой перегрузки по типу на простом примере с одним аргументом

Напишем три функции print(x) для типов int, float и char* (cstring):

void print_int(int x) {printf("%d ", x); }

void print_float(float x) {printf("%.4f ", x); }
void print_string(char* x) {printf("%s ", x); }

С помощью данного макроса соединим из под одним именем print:

#define print(x) _Generic((X),	int: print_int,  float: print_float,  char*: print_string)	(x)
  

В итоге получим, что запись print("hi") вызывает print_string("hi"), print(5.5) вызывает print_float(5.5) и так далее

После обработки препроцессором запись print("hi") превратится в _Generic(("hi"), int: print_int, float: print_float, char*: print_string)("hi"), и компилятор в зависимости от типа первого аргумента выберет имя функции, которую надо подставить вместо всего выражения _Generic(...)

Неопределенное число однородных аргументов

С помощью макросов также можно передавать неопределенное число аргументов без явного указания их числа. Покажу на примере для функции print_int

void print_int(int n, ...)
{
  va_list argptr;
  va_start(argptr, n);
  int x;
  for (int i = 0; i < n; i++)
  {
    x = va_arg(argptr, int);
    printf("%d ", x);
  }
  va_end(argptr);
}

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

Макрос PP_NARG(...), возвращающий число аргументов
#ifndef PP_NARG

/* 
* 
*	The PP_NARG macro returns the number of arguments that have been
*	passed to it.
* 
*	https://groups.google.com/g/comp.std.c/c/d-6Mj5Lko_s
* 
*/

#define PP_NARG(...) 	PP_NARG_(__VA_ARGS__,PP_RSEQ_N())

#define PP_NARG_(...) 	PP_ARG_N(__VA_ARGS__)

#define PP_ARG_N( _1, _2, _3, _4, _5, _6, _7, _8, _9,_10, _11,_12,_13,_14,_15,_16,_17,_18,_19,_20, _21,_22,_23,_24,_25,_26,_27,_28,_29,_30, _31,_32,_33,_34,_35,_36,_37,_38,_39,_40, _41,_42,_43,_44,_45,_46,_47,_48,_49,_50, _51,_52,_53,_54,_55,_56,_57,_58,_59,_60, _61,_62,_63, N, ...) N

#define PP_RSEQ_N() 63,62,61,60, 59,58,57,56,55,54,53,52,51,50, 49,48,47,46,45,44,43,42,41,40, 39,38,37,36,35,34,33,32,31,30, 29,28,27,26,25,24,23,22,21,20, 19,18,17,16,15,14,13,12,11,10, 9,8,7,6,5,4,3,2,1,0

#endif
#define print(...)    print_int(PP_NARG(__VA_ARGS__), __VA_ARGS__)

Примечание: __VA_ARGS__ содержит в себе аргументы, которые попали в ...

Соответственно для джейнериков, если все аргументы одного типа, мы должны написать

#define function(x, ...) Generic((x),	int: function_int,  float: function_float,  char*: function_string)(PP_NARG(__VA_ARGS__) + 1, x, __VA_ARGS__)

В итоге получим функцию function, которая обрабатывает неопределенное число однотипных аргументов. Я написал function, а не print, так как для нашей функции это точно не подойдет. Однако если вам известно, что все аргументы одного и того же типа, то такой способ будет намного проще, чем когда любой аргумент любого типа

Неопределенное число аргументов любого типа

I. Хранение информации о типах

Мы создадим универсальную функцию вида (синтаксис вольный) hidden_print(sep, n, x1, x2, x3, ...), которая в зависимости от типа следующей переменной xi выполняет нужный printf. Для любой другой реализации можно вызывать нужную функцию с уже известным типом

Для определенности максимальное число аргументов будет 12. Для print'а этого достаточно.

Так же немного поясню по поводу названий

Все глобальные имена будут начинаться с приставки cool. Это что-то вроде пространства имен, просто я решил создать отдельную несерьезную, удобно подключаемую библиотечку, в которой хранятся такие интересные, но практически не очень полезные штучки. В этой библиотеке на c++ все функции обьявлены в пространстве имен cool, однако в Си пространств имен нет, так что пользуюсь приставками. Однако в любой момент можно сделать #define print cool_print, а затем #undef print

Для хранения информации о типах аргументов буду использовать массив cool_hidden_types[12], индекс cool_hidden_last, куда надо добавить следущий элемент cool_hidden_add_int, cool_hidden_add_float и т.д. для каждого типа, которые добавляют в массив значение о типах. Всего наша функция будет поддерживать 7 типов: int, char*, float, double, char (?), uint, long

char (?)

Почему-то при записи _Generic(('a'), char: fun_char)() компилятор выдает что-то вроде "не найдена функция для int", так что на практике если передать символ в одинарных кавычках ничего не получится и он дай бог выведется как int

Значения о типах я решил определить с помощью #define, хотя в си есть и enum. Но раз уж тут почти весь код на макросах, то гулять так гулять!

Код работы с массивом типов
#define COOL_HIDDEN_INT 0
#define COOL_HIDDEN_STRING 1
#define COOL_HIDDEN_FLOAT 2
#define COOL_HIDDEN_DOUBLE 3
#define COOL_HIDDEN_CHAR 4
#define COOL_HIDDEN_UINT 5
#define COOL_HIDDEN_LONG 6
#define COOL_HIDDEN_VOID 7

int cool_hidden_types[12];
int cool_hidden_last = 0;

void cool_hidden_add_int()
{
    cool_hidden_types[cool_hidden_last] = COOL_HIDDEN_INT;
    cool_hidden_last += 1;
}
void cool_hidden_add_string()
{
    cool_hidden_types[cool_hidden_last] = COOL_HIDDEN_STRING;
    cool_hidden_last += 1;
}
/*аналогично для каждого типа */
void cool_hidden_add_void()
{
    cool_hidden_types[cool_hidden_last] = COOL_HIDDEN_VOID;
    cool_hidden_last += 1;
}

COOL_HIDDEN_VOID будет означать, что данный тип не поддерживается функцией. Можно было бы заморочиться и передавать информацию о размере переменной и выводить в 16-ричном виде для любой другой переменной, но я не стал это делать

Создадим теперь generic макро функцию cool_hidden_add(x), которая будет добавлять элемент в массив в зависимости от типа x

#define cool_hidden_add(x)              _Generic((x),           int: cool_hidden_add_int,           char*: cool_hidden_add_string,     float: cool_hidden_add_float,     double: cool_hidden_add_double,     char: cool_hidden_add_char,     unsigned int: cool_hidden_add_uint,     long: cool_hidden_add_long,     default: cool_hidden_add_void )()

Это было самое простое...

II. Определение числа аргументов на уровне макроса

Идея заключается в том, чтобы определить макрос вида cool_print##n(x1, x2, ..., xn) ("##" означает конкатенацию со значением n), который по очереди добавляет информацию о типе каждого xi, а затем передает в функцию реализации cool_hidden_print(sep, n, x1, x2, ...) разделитель, n, и все xi. Разделитель я определю как глобальную (если так вообще можно называть переменные с уникальной приставкой) переменную cool_print_sep = " ", которую можно изменить в любой момент

Определим это простым образом через копирование. Хотя наверное их можно было бы сгенерировать макросами, но мне было уже лень. (К тому же у меня и так статический анализатор visual studio заблудился в куче макросов и указывает ошибку там, где все нормально компилируется, но об этом позже)

В общем виде это выглядит так:
#define cool_print_n(x1, x2, x3, x4, ..., xn, ...)  cool_hidden_add(x1);  cool_hidden_add(x2);  cool_hidden_add(x3);  cool_hidden_add(x4);  ...................   cool_hidden_add(xn);cool_hidden_print(cool_print_sep, n, x1, x2, x3, x4, ..., xn)
Полный код
//hide this a big part of code
#if 1 

#define cool_print_12(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_add(x5);     cool_hidden_add(x6);     cool_hidden_add(x7);     cool_hidden_add(x8);     cool_hidden_add(x9);     cool_hidden_add(x10);     cool_hidden_add(x11);     cool_hidden_add(x12);     cool_hidden_print(cool_print_sep, 12, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12)

#define cool_print_11(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_add(x5);     cool_hidden_add(x6);     cool_hidden_add(x7);     cool_hidden_add(x8);     cool_hidden_add(x9);     cool_hidden_add(x10);     cool_hidden_add(x11);     cool_hidden_print(cool_print_sep, 11, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11)

#define cool_print_10(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_add(x5);     cool_hidden_add(x6);     cool_hidden_add(x7);     cool_hidden_add(x8);     cool_hidden_add(x9);     cool_hidden_add(x10);     cool_hidden_print(cool_print_sep, 10, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10)

#define cool_print_9(x1, x2, x3, x4, x5, x6, x7, x8, x9, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_add(x5);     cool_hidden_add(x6);     cool_hidden_add(x7);     cool_hidden_add(x8);     cool_hidden_add(x9);     cool_hidden_print(cool_print_sep, 9, x1, x2, x3, x4, x5, x6, x7, x8, x9)

#define cool_print_8(x1, x2, x3, x4, x5, x6, x7, x8, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_add(x5);     cool_hidden_add(x6);     cool_hidden_add(x7);     cool_hidden_add(x8);     cool_hidden_print(cool_print_sep, 8, x1, x2, x3, x4, x5, x6, x7, x8)

#define cool_print_7(x1, x2, x3, x4, x5, x6, x7, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_add(x5);     cool_hidden_add(x6);     cool_hidden_add(x7);     cool_hidden_print(cool_print_sep, 7, x1, x2, x3, x4, x5, x6, x7)

#define cool_print_6(x1, x2, x3, x4, x5, x6, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_add(x5);     cool_hidden_add(x6);     cool_hidden_print(cool_print_sep, 6, x1, x2, x3, x4, x5, x6)


#define cool_print_5(x1, x2, x3, x4, x5, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_add(x5);     cool_hidden_print(cool_print_sep, 5, x1, x2, x3, x4, x5)

#define cool_print_4(x1, x2, x3, x4, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_add(x4);     cool_hidden_print(cool_print_sep, 4, x1, x2, x3, x4)

#define cool_print_3(x1, x2, x3, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_add(x3);     cool_hidden_print(cool_print_sep, 3, x1, x2, x3)

#define cool_print_2(x1, x2, ...)       cool_hidden_add(x1);     cool_hidden_add(x2);     cool_hidden_print(cool_print_sep, 2, x1, x2)

#define cool_print_1(x, ...)       cool_hidden_add(x);     cool_hidden_print(cool_print_sep, 1, x)


#endif //hide this a big part of code

Немного проспойлерил, что аргументов макро функции cool_print_n всегда 12, и что после нее идет ..., но об этом далее

Значение выполнения макроса PP_NARG(__VA_ARGS__) не возможно подставить напрямую в выражение cool_print_##PP_NARG(__VA_ARGS__), так как оно развернется в что-то вроде cool_print_PP_NARG("x", 5, "i", 8,), что не имеет никакого смысла. Поэтому надо использовать код из макроса, но не возвращать число аргументов, а сразу конкатенировать

код PP_NARG(__VA_ARGS__) еще раз
#ifndef PP_NARG

/* 
* 
*	The PP_NARG macro returns the number of arguments that have been
*	passed to it.
* 
*	https://groups.google.com/g/comp.std.c/c/d-6Mj5Lko_s
* 
*/

#define PP_NARG(...) 	PP_NARG_(__VA_ARGS__,PP_RSEQ_N())

#define PP_NARG_(...) 	PP_ARG_N(__VA_ARGS__)

#define PP_ARG_N( _1, _2, _3, _4, _5, _6, _7, _8, _9,_10, _11,_12,_13,_14,_15,_16,_17,_18,_19,_20, _21,_22,_23,_24,_25,_26,_27,_28,_29,_30, _31,_32,_33,_34,_35,_36,_37,_38,_39,_40, _41,_42,_43,_44,_45,_46,_47,_48,_49,_50, _51,_52,_53,_54,_55,_56,_57,_58,_59,_60, _61,_62,_63, N, ...) N

#define PP_RSEQ_N() 63,62,61,60, 59,58,57,56,55,54,53,52,51,50, 49,48,47,46,45,44,43,42,41,40, 39,38,37,36,35,34,33,32,31,30, 29,28,27,26,25,24,23,22,21,20, 19,18,17,16,15,14,13,12,11,10, 9,8,7,6,5,4,3,2,1,0

#endif

cool_print(...) - тот же PP_NARG, но измененный

#define cool_print(...) 	cool_print_(__VA_ARGS__ , COOL_RSEQ_N())


#define cool_print_(...) 	COOL_ARG_N(__VA_ARGS__)

#define COOL_ARG_N(     _1, _2, _3, _4, _5, _6, _7, _8, _9,_10, _11,_12,_13,_14,_15,_16,_17,_18,_19,_20,     _21,_22,_23,_24,_25,_26,_27,_28,_29,_30, _31,_32,_33,_34,_35,_36,_37,_38,_39,_40,     _41,_42,_43,_44,_45,_46,_47,_48,_49,_50, _51,_52,_53,_54,_55,_56,_57,_58,_59,_60,     _61,_62,_63, n, ...)                             cool_print_##n(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11,_12)

#define COOL_RSEQ_N()     63,62,61,60,59,58,57,56,55,54,53,52,51,50,     49,48,47,46,45,44,43,42,41,40,39,38,37,36,35,34,33,32,31,30,     29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,     9,8,7,6,5,4,3,2,1,0

Как это работает:

1. 	Условно происходит вызов cool_print("a", 4, "b")

2. 	Это превращается в cool_print_("a", 4, "b", 
		63,62,61,60,59,58,57,56,55,54,
		53,52,51,50,49,48,47,46,45,44,43,
		42,41,40,39,38,37,36,35,34,33,32,
		31,30,29,28,27,26,25,24,23,22,21,
		20,19,18,17,16,15,14,13,12,11,10,
		9,8,7,6,5,4,3,2,1,0

3. 	Затем из cool_print_ это все передается в макрос COOL_ARG_N,
    который способен принять 64 аргумента. В 64-й аргумент, названный n
    попадает как раз количество исходных аргументов из-за того, что при
    добавление аргументов VA_ARGS перед последовательностью COOL_RSEQ_N 
    (63..0) часть чисел из этой последовательности вытесняется

4.	В конце концов в макросе COOL_ARG_N просиходит конкатенация
		cool_print_##n и вызов этого макроса. 
		В данном примере это cool_print_3

	cool_print_3("a", 4, "b", 
	63,62,61,60,59,58,57,56,55,54,53,52,51,50,
	49,48,47,46,45,44,43,42,41,40,39,38,37,36,
	35,34,33,32,31,30,29,28,27,26,25,24,23,22,
	21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3)

Всего передается 12 аргументов, так как невозможно обрезать лишние. 
И это и не надо, так как благодаря "..." в списке аргументов каждого
макроса cool_print_##n они способны проглотить ненужный хвост

Так же можно было бы сократить число аргументов с 64 до 12, 
но я посчитал это не очень важным

III. Собственно реализация функции

Отлично, у нас при вызове cool_print("наши", "аргументы", 10) происходит заполнение массива cool_hidden_types информацией о типе каждого аргумента, а затем вызывается функция реализации cool_hidden_print(int sep, int n, ...)! Давайте напишем эту функцию

Небольшое пояснение как в C работать с неопределенным числом аргументов вообще

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

  1. В обьявлении функции последним параметром надо указать ...

  2. va_list argptr- это определение указателя argptr, который будет в использоваться в дальнейшем

  3. va_start(argptr, n)- установка указателя на последний определенный аргумент

  4. va_arg(argptr, float)- возвращает значение слудующего аргумента

  5. va_end(argptr)- завершает работу с аргументами

Итого просто в Си без всяких этих макросов нам необходимо вычислять какими-нибудь образом тип следующего аргумента, и условии прекращения перебора аргументов

Суть такова: в цикле перебираются все элементы все элементы. В swich'e каждый элемент кастится в void'ый указатель x, а затем вызывается в виде printf("...%s", *((type) x), sep), где type - это тип аргумента, а "..." - это специфичный для данного типа формат вывода. Например для int это printf("%d%s", *((type) x), sep). Для упрощенной записи приведения типов я использую вспомогательный макрос #define COOL_CAST(T, x) ((T) (x))

код cool_hidden_print(sep, n, ...)
#define COOL_CAST(T, x) ((T) (x))


void cool_hidden_print(char* sep, int n, ...)
{
    va_list argptr;
    va_start(argptr, n);
void* x;

for (int i = 0; i &lt; n; i++)
{
    switch (cool_hidden_types[i])
    {
    case COOL_HIDDEN_INT:
        x = &va_arg(argptr, int);
        printf("%d%s", COOL_CAST(int, x), sep);
        break;

    case COOL_HIDDEN_STRING:
        x = &va_arg(argptr, char*);
        printf("%s%s", COOL_CAST(char*, x) , sep);
        break;

    case COOL_HIDDEN_FLOAT:
        x = &va_arg(argptr, float);
        printf("%.4f%s", COOL_CAST(float, x), sep);
        break;

    case COOL_HIDDEN_DOUBLE:
        x = &va_arg(argptr, double);
        printf("%.4f%s", COOL_CAST(double, x), sep);
        break;

    case COOL_HIDDEN_CHAR:
        x = &va_arg(argptr, char);
        printf("%c%s", COOL_CAST(char, x), sep);
        break;

    case COOL_HIDDEN_UINT:
        x = &va_arg(argptr, unsigned int);
        printf("%.4u%s", COOL_CAST(unsigned int, x), sep);
        break;

    case COOL_HIDDEN_VOID:
        printf("unsupported type%s", sep);
        break;

    default:
        printf("Internal COOL/C/PRINT error line: %d in %s", __LINE__, __FILE__);
        break;
    } 
}

va_end(argptr);
cool_hidden_last = 0;

}

В комментариях подметили, что брать указатель на стек не в коем случае нельзя, так как эта переменная может перестать существовать в любое время. Поэтому тут нужно сразу va_arg подставлять в printf

Дополнительные мелочи

После подключения библиотеки можно избавиться от приставки cool_ с помощью

#define print cool_print

Идеально, теперь наша функция print полностью работает! В качестве вишенки на торте определим функцию println, которая после вывода переводит нас на новую строку

#define cool_println(...) 		cool_print(__VA_ARGS__);    printf("\n")

#define cool_printlnn() printf("\n")

К моему большому сожалению я не смог решить проблему, что при вызове макроса без аргументов происходит синтаксическая ошибка из-за лишней запятой в начале, поэтому без аргументов нужно вызывать printlnn()... Я пытался решить это, при определении cool_print

ни так
#define cool_print(...) cool_print_(__VA_ARGS__ , ## COOL_RSEQ_N()
ни так
#define cool_print(...) cool_print_(__VA_ARGS__ ## , COOL_RSEQ_N()
все равно не работает

Как советуют в интернете делать при таком случае ничего не происходит. Видимо эта запись работает только для ,## __VA_ARGS__, когда __VA_ARGS__ идет в конце, а не в начале

Возможно можно как-нибудь еще одним вложенным макросом определить, является ли __VA_ARGS__ пустым. Я нашел в гугле решение только тогда когда максимум 2 аргумента

Еще есть еще некий __VAR_OPT__, который делает как раз то, что нужно, но его добавят, как я понял, в следующем стандарте

У меня почему-то сработало один раз (,), но после перезапуска visual stidio стало __VAR_OPT__ не определено. К тому же в том месте __VAR_OPT__ ставить нельза, так как макрос будет считать что у нас 63 а не 64 аргумента (что приводила к ошибке вызова не того cool_print##n (на единицу меньше). Нужно что-то вроде

#define cool_print(...)	__VAR_OPT__(  cool_print_(__VA_ARGS__ , COOL_RSEQ_N())  )
вместо текущего
#define cool_print(...) 	cool_print_(__VA_ARGS__ , COOL_RSEQ_N())

Так же в комментариях предложили логичное и изящное решение этой проблемы - добавление произвольного первого аргумента, который мы будем игнорировать в функции реализации

#define cool_print(...)    cool_print_("", __VA_ARGS__)

#define cool_print_(...) 	cool_print__(__VA_ARGS__ , COOL_RSEQ_N())


#define cool_print__(...) 	COOL_ARG_N(__VA_ARGS__)

Но это не помогает, и лишние запятые остаются


Проблемы этого метода

  • Первое - это конечно же невозможность вызвать функцию без аргументов. К тому же огромным минусом является то, что в случае ошибки генерируется очень невнятное сообщение об ошибке, и я не вижу куда можно вставить его

  • Второе - сложность реализации. На C++ аналогичная функция выглядит намного проще. Хотя, имея в качестве шаблона мою функцию print будет не так сложно реализовать любую другую

  • Третье - статический анализатор. В visual studio у меня подчеркнут красным каждый print и println со словами "требуется выражение", и висит по одной ошибки (прям красным цветом) на каждый вызов этой функции. Не смотря на это все нормально компилируется. И даже не думаю что на это должно тратиться сильно больше времени, чем на раскрытие variadic templates в c++, хотя я тесты не проводил (а как вообще замерить время компиляции - это отдельный вопрос)

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

При этом компилируется прекрасно
При этом компилируется прекрасно

При этом компилируется прекрасно


Полный код данной библиотечки можете найти по ссылке на моем гитхабе: print.h

А вот пример использования c_example.c

Заключение

Удивительно сколько всего можно сделать на чистом Си с очень слабыми шаблонами. Но все же Си предназначен не совсем для этого. Данная статья нужна в большей мере для интереса, хотя может кому-нибудь и поможет в работе. Для удобств написания кода на Си как раз и был создан C++, который позволяет работать с обьектами через класс, а не писать id обьекта первым параметром в методах; он позваляет делать перегрузку функций для тех случаев, когда это необходимо (чтобы не городить функции типа pow, powi, powf); упрощает работу с указателями, добавляя ссылки, добавляет пространства имен, чтобы не городить приставок, добавляет контейнеры. Но как итог всего этого - медленная компиляция

Вот пример реализации этой же функции print на C++:

print на C++
#ifndef COOL_PRINT_HPP
#define COOL_PRINT_HPP

#include <string>
#include <iostream>
#include <iomanip>

namespace
{
	std::ostream* out = &std::cout;
}

namespace cool
{
	inline void setCyrillic()
	{
		setlocale(LC_ALL, "Russian");
	}

	void setPrintOut(std::ostream& os)
	{
		::out = &os;
	}

	std::ostream* getPrintOutPtr()
	{
		return ::out;
	}
	
	inline void printFlush()
	{
		*::out << std::flush;
	}

	inline void print() 
	{ 
		*::out << ' ';
	}

	template <typename Arg>
	inline void print(const Arg& arg)
	{
		*::out <<  std::fixed << std::setprecision(4) << arg << ' ';
	}

	template <typename Arg, typename... Args>
	void print(const Arg& arg, const Args&... args)
	{
		print(arg);
		print(args...);
	}
	////

	inline void println()
	{
		*::out << '\n';
	}
	template <typename Arg>
	inline void println(const Arg& arg)
	{
		*::out << std::fixed << std::setprecision(4) << arg << '\n';
	}

	template <typename... Args>
	void println(const Args&... args)
	{
		print(args...);
		println();
	}
	///
	void print0() { }

	template <typename Arg>
	inline void print0(const Arg& arg)
	{
		*::out << std::fixed << std::setprecision(4) << arg;
	}

	template <typename Arg, typename... Args>
	void print0(const Arg& arg, const Args&... args)
	{
		print0(arg);
		print0(args...);
	}

#define COOL_INFO(x) (std::string(#x) + " = " + std::to_string(x))
}


#endif

Более понятно, примерно в 3 раза меньше кода, и реализация более полноценная. Но в то же время стандартный printf хоть и выглядит не так изящно, но зато быстрый и практичный. Каждому языку свое место