Почему дженерики C используют void * вместо макросов?

Насколько я понимаю, стандартным способом реализации универсальных типов данных в 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, void * и макросы решают совершенно разные задачи. Я не знаю, почему вы думаете, что «дженерики C используют void * вместо макросов». _Generic не может различить, что находится за void *.
user2357112 05.07.2024 04:53
_Generic на самом деле используется почти исключительно в макросах.
user2357112 05.07.2024 04:55

@user2357112 user2357112 Я не уверен, что понимаю ваш комментарий. Насколько я понимаю, типичным способом реализации типа Option является использование void * при вводе данных. Я спрашиваю о преимуществах этого подхода по сравнению с тем, который я описал.

Capybara 05.07.2024 05:01

Типы опций не имеют ничего общего с дженериками. Если вы имеете в виду один тип Option, который может содержать необязательное что угодно, им необходимо использовать void *, чтобы иметь возможность обрабатывать любой тип, который хочет использовать пользователь. Ваши реализации is_some и is_none могут обрабатывать только типы, которые были жестко запрограммированы в определениях макросов.

user2357112 05.07.2024 05:06

С void * у вас есть одна реализация с макросом, у вас есть реализация для каждого типа каждой функции, структуры и т. д. Макросы основаны на манипуляциях с текстом, и вам нужны всевозможные трюки и проблемы, чтобы выполнять простые вещи, такие как циклы, или даже передавать запятую как Аргумент. Если вы используете много макросов, вы получите собственный язык, который сильно отличается от C. Это означает, что вам нужно понимать все макросы, чтобы понять, что происходит. Это может затруднить поддержку такой базы кода.

Allan Wind 05.07.2024 05:13

Для некоторого возможного понимания см. мой ответ: Написание «общего» метода struct-print на C Он сравнивает/показывает версии C с void *switch), общим struct (т. е. базовым классом) и таблицей виртуальных функций.

Craig Estey 05.07.2024 05:15

Я предлагаю вам использовать термин «абстрактный тип данных», поскольку «общий» подразумевает ключевое слово_Generic, которое используется для переключения типов внутри макроса.

Allan Wind 05.07.2024 06:36

@AllanWind Эти термины шире, чем просто использование языка C. «Абстрактный тип данных», а также «типовое программирование» — это два общих термина информатики, ни один из которых не является специфическим для C как такового. «Обобщенные» обычно относятся к типовому программированию в области информатики.

Lundin 05.07.2024 08:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
8
127
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Потому что _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 06.07.2024 00:24

@user2357112 user2357112 Это было бы возможно, если бы вы определили функцию поиска/сортировки, а также обратный вызов, используя приведенную выше модель. Обратной стороной является то, что вам придется реализовать это самостоятельно, но в какой-то момент мы все это делали.

Lundin 07.07.2024 18:22

Другие вопросы по теме