Насколько я понимаю, стандартным способом реализации универсальных типов данных в C является использование указателей void. Однако альтернативным подходом было бы использование макросов. Вот пример реализации универсального типа «Option» с использованием макросов:
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#define OPTION(T) \
struct option_##T { \
bool exists; \
T data; \
}; \
typedef struct option_##T option_##T; \
static inline bool is_some_##T(option_##T x) { return x.exists; } \
static inline bool is_none_##T(option_##T x) { return !x.exists; }
#define is_some(x) _Generic((x), option_int: is_some_int, option_char: is_some_char)(x)
#define is_none(x) _Generic((x), option_int: is_none_int, option_char: is_none_char)(x)
OPTION(int);
OPTION(char);
int main(int argc, char *argv[]) {
option_int x = {.exists = true, .data=3};
option_char y = {.exists = true, .data='a'};
printf("x is some: %d\n", is_some(x));
printf("y is some: %d\n", is_some(y));
return 0;
}
Альтернативный подход — использовать void * для ввода данных. Однако это добавит дополнительный уровень косвенности (возможно, при этом сэкономив на размере двоичного файла). Есть ли причина, по которой подход void * более распространен?
_Generic
на самом деле используется почти исключительно в макросах.
@user2357112 user2357112 Я не уверен, что понимаю ваш комментарий. Насколько я понимаю, типичным способом реализации типа Option является использование void * при вводе данных. Я спрашиваю о преимуществах этого подхода по сравнению с тем, который я описал.
Типы опций не имеют ничего общего с дженериками. Если вы имеете в виду один тип Option
, который может содержать необязательное что угодно, им необходимо использовать void *
, чтобы иметь возможность обрабатывать любой тип, который хочет использовать пользователь. Ваши реализации is_some
и is_none
могут обрабатывать только типы, которые были жестко запрограммированы в определениях макросов.
С void *
у вас есть одна реализация с макросом, у вас есть реализация для каждого типа каждой функции, структуры и т. д. Макросы основаны на манипуляциях с текстом, и вам нужны всевозможные трюки и проблемы, чтобы выполнять простые вещи, такие как циклы, или даже передавать запятую как Аргумент. Если вы используете много макросов, вы получите собственный язык, который сильно отличается от C. Это означает, что вам нужно понимать все макросы, чтобы понять, что происходит. Это может затруднить поддержку такой базы кода.
Для некоторого возможного понимания см. мой ответ: Написание «общего» метода struct-print на C Он сравнивает/показывает версии C с void *
(и switch
), общим struct
(т. е. базовым классом) и таблицей виртуальных функций.
Я предлагаю вам использовать термин «абстрактный тип данных», поскольку «общий» подразумевает ключевое слово_Generic, которое используется для переключения типов внутри макроса.
@AllanWind Эти термины шире, чем просто использование языка C. «Абстрактный тип данных», а также «типовое программирование» — это два общих термина информатики, ни один из которых не является специфическим для C как такового. «Обобщенные» обычно относятся к типовому программированию в области информатики.
Потому что _Generic
— это относительно новая функция. «Старый школьный» способ сделать это всегда заключался в использовании указателей void, либо путем создания структуры, аналогичной вашему примеру, но обычно с перечислением для обозначения представленного типа. Или иначе, с помощью обратных вызовов для конкретного типа, как это известно в bsearch
/qsort
.
Так что void*
более распространен просто по историческим причинам — в те времена другого способа сделать это не было. void*
в наши дни становятся все более неактуальными, потому что они опасно типизированы, и сейчас доступны лучшие языковые функции.
Что касается _Generic
, которые скоро будут стандартизированы typeof
, то существует множество различных способов их использования. На самом деле нет необходимости в структурах/перечислениях-оболочках. Например, вы можете использовать их как «шаблоны для бедняков» — возможно, это не рекомендуется, но довольно мощно и безопасно с точки зрения типа:
#include <stdio.h>
#define TYPES_SUPPORTED(X) \
X(int, %d) \
X(char, %c) \
#define PRINT(type, fmt) \
void type##_print (type t) \
{ \
printf(#fmt "\n", t); \
}
TYPES_SUPPORTED(PRINT)
#define GENERIC_PRINT(type, fmt) ,type: type##_print
#define print(x) \
_Generic((x), \
default:0 \
TYPES_SUPPORTED(GENERIC_PRINT) )(x)
int main()
{
int a = 123;
char c = 'A';
print(a);
print(c);
}
Этот способ использования «макросов X» означает, что вы можете отображать все поддерживаемые типы в списке, без необходимости вызывать отдельные макросы, как в вашем примере. Вместо этого вы создаете один макрос для каждого варианта использования, а затем вызываете список макросов X.
Итак, в этом примере создается одна функция int_print(int t)
и одна char_print(char t)
, печатающая параметр с использованием правильного спецификатора формата.
Затем мы можем вызывать эти функции по очереди из макроса общего типа print
, где список ассоциаций _Generic
также встроен в список макросов X, что уменьшает необходимость вводить каждое условие. Злой трюк здесь заключается в том, чтобы сначала поставить default:
, а затем начинать каждое раскрытие макроса с ,
, поскольку _Generic
в отличие от объявлений массивов/перечислений, списков инициализации и т. д. не любит завершающую запятую.
Однако _Generic
необходимо заранее указать все поддерживаемые типы. Было бы невозможно реализовать что-то вроде bsearch
или qsort
с _Generic
, поскольку они должны иметь возможность сортировать произвольные определяемые пользователем типы с произвольными компараторами, указанными пользователем.
@user2357112 user2357112 Это было бы возможно, если бы вы определили функцию поиска/сортировки, а также обратный вызов, используя приведенную выше модель. Обратной стороной является то, что вам придется реализовать это самостоятельно, но в какой-то момент мы все это делали.
_Generic
,void *
и макросы решают совершенно разные задачи. Я не знаю, почему вы думаете, что «дженерики C используют void * вместо макросов»._Generic
не может различить, что находится заvoid *
.