Произвольное число аргументов любых типов на C11 и выше с помощью _Generic и variadic макросов

    Функция 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 хоть и выглядит не так изящно, но зато быстрый и практичный. Каждому языку свое место

    Комментарии 37

      0
      Моё ИМХО: лучший С — это С99. Остальное — это невнятные попытки зачем-то понабрать ненужных фич из С++.
        +1
        Что с форматированием? После одобрения модераторами все заголовки спойлеров попали под сам спойлер, а переносы строк в макросах пропали?
        Например было так
        #define function(x, ...) Generic((x),\
            int: function_int,\
           float: function_float,\
            char*: function_string)\
        (PP_NARG(VA_ARGS) + 1, x, VA_ARGS)
        


        А стало так
        #define function(x, ...) Generic((x),    int: function_int,  float: function_float,  char*: function_string)(PP_NARG(VA_ARGS) + 1, x, VA_ARGS)
        


        Это видимо не у меня одного так, я попытался с другого браузера открыть
          0

          Это надо Nomad_77 кастовать.

            0

            Это что значит (это просто мой первый пост)


            Еще и нижние подчеркивания вокруг __VA_OPT__ и похожих слетели. Если кто из модераторов может починить, то я буду очень благодарен

            0

            Привет, спасибо, разберёмся

            0
            Еще послетала подсветка языков. Это баг хабра?
              0

              Вы всё ещё можете редактировать свой пост.

                0

                Могу, но там очень много. И не факт что этот баг не повторится после модерации. Я надеюсь модераторы смогут починить

                  0
                  Вроде получается починить. Тогда сегодня может быть все почини (просто это будет долго)

                  Пока что прикреплю в начале статью png версию всего текста
                0
                Ура, я все починил. Если тут есть разработчики движка, то под спойлером в начале статьи я описал более-менее подробно особенности проблемы
                +1
                Ленивый принтО — мой вариант для печати левой ногой.
                printo(«текст», double, float, uint(8-32-64)_t, int(8-32-64)_t )
                github.com/AVI-crak/Rtos_cortex/blob/master/sPrint.h
                Всё тоже самое, только чуть проще и легче.
                  0

                  Хорошее альтернативное решение.


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


                  Еще заметил у вас do while(0), это как понимаю чтобы можно было использовать макрос в однострочных циклах и условиях. Учту это, а то у меня нигде это не использовалось

                    0
                    нет собственно функции, которая принимает все аргументы

                    Невозможно сделать такое на Си через одну функцию. Через макросы можно.
                    Но если используются макросы — то нет смысла в дополнительной программной обработке хитро сжатого пакета аргументов. Проще разложить по полочкам заранее.
                    Тем-более что выпрыгнуть с уровня макроса на уровень функции — очень и очень сложно. Там кода в сотни раз больше нужно чем у меня и вас.
                      0

                      Я имел ввиду что в моей функции реализации можно не просто выполнить по действию для разного аргумента, а можно сделать поведение относительно типов других аргументов.


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


                      Повторюсь, у вас хороший макрос, сделанный проще и пограмотнее моего. Но все же это не одно и то же

                  0

                  Требование хотя бы одного параметра легко решается фиктивным параметром:


                  define println(...) cool_println("", VA_ARGS)

                  Разумеется более рационально ввести dummy тип и функцию печати для него, реализацию этого оставляю для вас.

                    0
                    Спасибо, пожалуй это отличное решение

                    Про dummy не понял вас. Что это за тип, и зачем функция печати для него
                      0

                      Ну смотрите, отличное решение всё равно заботит компилятор выводом строки на консоль. Казалось бы целых 0 байт в строке, но высока вероятность что этот вызов будет передан в libc и накладные расходы получаются уже более заметными.
                      Решение из мира С++ :


                      typedef struct {}please_dont_bother_printing_me;
                      please_dont_bother_printing_me please_dont_bother_printing_me_instance;
                      #define print(...) cool_print(please_dont_bother_printing_me_instance, __VA_ARGS__)

                      Я пока смотрел на ваш код у меня вопрос появился


                      x = &va_arg(argptr, int);

                      Почему так (и вообще как оно работает в этой вашей студии)?
                      Как я понимаю va_arg уже возвращает значение нужного типа (поэтому и получает его аргументом), поэтому у меня вопросы:
                      тут что, реально берётся указатель на стек?
                      зачем всё это если далее он снова кастится в нужный тип, почему не


                      printf("%d%s", va_arg(argptr, int), sep);

                      Или я что-то в СИ не понимаю?

                        0
                        Да, берется. Если это опасно, то могу исправить :) Я так сделал для наглядности, чтобы была отдельная переменная x

                        В Си я не эксперт, так что вам виднее. Это есть такое правило, что не в коем случае не стоит брать указатель на стек? Почему? Потому что если мы будем его использовать в другом блоке, то все поломается, или тут что-то большее? Просто в данном случае он используется более как локальный синоним. Логично, что потом я на этот указатель больше не ссылаюсь
                          +1

                          О какой наглядности вообще речь, когда вы берёте int, получаете на него указатель, приводите к void*, преобразуете его макросом обратно в указатель на int, после чего в том же макросе разыменовываете указатель, превращая его снова в int?

                            0
                            Наглядность — дело субьективное. Для меня каст в воид и обратно кажется очень понятным

                            Мне больше интересно есть ли тут ошибка при взятии указателя на стек, или это просто какое-то предубеждение, что их нельзя брать ради того, чтобы нечаянно не передать в другую функцию?
                              0

                              gcc это вообще не собирает, и он кстати прав
                              Дело в том, что строго говоря СИ функция обычно возвращает int не через стек а через регистр. Даже если оно вернулось через стек, не ясно какое у него время жизни, может статься что оно разрушается сразу же по присвоению. Я думаю тут дело вот в чем — вантузный компилятор превращает va_arg в вычисленную временную переменную, но это не верно т.к. в стандарте сказано что это функция (особенная). Вопрос времени жизни этой переменной открыт так что это UB

                                0

                                Спасибо большое за пояснение. Можете еще уточнить, это касается только указателей на аргументы, или любая запись вроде
                                int a = 5;
                                int* b = &a;
                                Будет ub? Ведь часто именно такие примеры есть при объяснении работы с указателями


                                И по каким запросам можно что-нибудь нашкглить про проблему указателей на стек? Бегло я ничего не смог найти

                                  0

                                  Тут все валидно, но нужно помнить что это указатель на переменную с конкретным временем жизни, если его использовать когда a уже уничтожено — опять таки UB.


                                  ISO/IEC 9899:2011 6.2.4 §2, "The value of a pointer becomes indeterminate when the object it points to reaches the end of its lifetime."

                      0
                      Не получается, запятые все равно остаются
                      Вот так переопределил

                      #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__)
                      


                      Вот такая ошибка:

                      image

                      Это мне напоминает другую ошибку, про которую я не написал, когда я в строку
                      cool_print_##n(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11,_12)
                      Первым аргументом поместил разделитель " ". Он каким-то образом «поднялся» вверх в самый верхний вложенный макрос

                      Я правда не знаю, это стандартизированная фишка макросов, или ошибка VS
                      0

                      Если интересует способ сравнить скорость с вариадиками и узнать насколько это будет быстрее в MSVC есть встроенный механизм для этого: https://devblogs.microsoft.com/cppblog/introducing-c-build-insights/


                      Последний MSVC должен поддерживать C17: https://devblogs.microsoft.com/cppblog/c11-and-c17-standard-support-arriving-in-msvc/

                        0
                        light_and_ray
                        Макросы работают снизу вверх: если что-то есть на второй строчке — то это что-то должно иметь описание на первой строке кода, при этом первая строка самодостаточна.
                        Дык вот и описание работы макросов нужно делать снизу вверх, иначе очень легко запутаться.
                        Нужно сначала посчитать количество аргументов, и написать макросы для всех количеств аргументов. И только потом узнавать тип аргумента, с подстановкой нужной функции.
                        На нулевое количество аргументов тоже есть место для макроса, есно на склейке с нулём.
                          0
                          Нужно сначала посчитать количество аргументов, и написать макросы для всех количеств аргументов. И только потом узнавать тип аргумента, с подстановкой нужной функции.

                          В моем решение если рассматривать макросы снизу вверх, то так и происходит

                          На нулевое количество аргументов тоже есть место для макроса, есно на склейке с нулём.

                          Проблема не в том, что cool_print_0 не определен. Проблема в том, что при подстановки ничего появляется ненужная запятая и синтаксическая ошибка
                          cool_print_(, 63, 62, 62, ...)
                            0
                            появляется ненужная запятая

                            Ну так я-же сказал — набор макросов для разных количеств аргументов. Там только имя макроса используется, а его аргумент можно (нужно) оставить пустым. Запятая потому и появляется, что несуществующий аргумент требует своего определения.
                            Я у себя проверил, с пустым аргументом вызывается нужная мне функция, без ошибок.
                            #define dpr_0() soft_print(" ")
                            0
                            Почему-то при записи _Generic(('a'), char: fun_char)() компилятор выдает что-то вроде «не найдена функция для int», так что на практике если передать символ в одинарных кавычках ничего не получится и он дай бог выведется как int
                            Не почему-то, а потому, что символ в одинарных кавычках в C, в отличии от C++, имеет тип int
                              –3
                              Можете подсказать, как сделать оглавление в статье, чтобы оно еще и сбоку отображалось?
                                –1

                                За что минус, тут нет никакой солидарности к людям, которые только зарегистрировались? Нормальный вопрос, так как в редакторе нигде нельзя выбрать оглавление. Поставил перед заголовками якори и они огде не отображаются


                                Гуглить пытался, в справке по форматированию не нашел. Что сразу минусить? Нормальный вопрос

                                  +1

                                  Вы видели хоть у одного поста здесь подобным образом оформленное оглавление?


                                  в справке по форматированию не нашел

                                  Верный признак того, что такое оформление поста на Хабре невозможно.

                                    –1

                                    Видел и не раз. Может это были технические статьи и я не разобрался


                                    Конечно грубоватые тут некоторые люди в сообществое, которые так отвечают человеку, который только что присоединился. Я же это отдельным комментарием спросил, а не в самой статье, это даже не оффтоп

                                –1

                                VA_ARGS не делает вам default argument promotion на все переданные через… аргументы? См. Неправильно запрашивать у va_arg повышаемый тип — char, short или float

                                  0

                                  __VA_ARGS__ — это макрос. Он не делает никаких argument promotion по определению, это просто подстановка куска кода.

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

                                Самое читаемое