Хранение и использование информации о типах в C

Я исхожу из Java и пытаюсь реализовать двусвязный список в C в качестве упражнения. Я хотел сделать что-то вроде дженериков Java, где я бы передал тип указателя в инициализацию списка, и этот тип указателя будет использоваться для приведения указателя списка void, но я не уверен, возможно ли это?

Я ищу что-то, что можно сохранить в структуре списка и использовать для приведения *data к правильному типу из узла. Я думал об использовании двойного указателя, но тогда мне нужно было бы объявить его пустым указателем, и у меня была бы та же проблема.

typedef struct node {
    void *data;
    struct node *next;
    struct node *previous;
} node;

typedef struct list {
    node *head;
    node *tail;
   //??? is there any way to store the data type of *data? 
} list;

person LiamRyan    schedule 21.06.2017    source источник
comment
Я не понимаю всех этих вещей о нулевых указателях... 0 оценивает нулевой указатель любого типа, как и (void *)0... но что касается более четкого вопроса в комментарии к вашему коду: Не совсем, вам нужно придумать собственное решение (например, enum для типов, которые вы используете). Информация о типе в C больше не существует после компиляции, поэтому нет объекта типа типа.   -  person    schedule 21.06.2017
comment
Можно использовать void list_put_int(list *L, int *p) { .... xx.data = p; ...} и int *list_get_int(list *L) { .... return xx.data;} еще как @Felix Palmen сказал, что нет объекта типа типа и нет приведения, которое изменяется во время выполнения (кроме, возможно, VLA, и это все равно здесь не поможет).   -  person chux - Reinstate Monica    schedule 21.06.2017
comment
Код может использовать _Generic с макросом для достижения большей части цели. Это немного запутанно.   -  person chux - Reinstate Monica    schedule 21.06.2017
comment
@FelixPalmen Вы правы, извините, мой мозг упал на ноль вместо пустоты, обновил вопрос   -  person LiamRyan    schedule 21.06.2017


Ответы (5)


Как правило, используются специальные функции, подобные приведенным ниже.

void    List_Put_int(list *L, int *i);
void    List_Put_double(list *L, double *d);
int *   List_Get_int(list *L);
double *List_Get_double(list *L);

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

Ниже приведен базовый код для сохранения/выборки до 3 типов указателей. Макросы потребуют расширения для каждого нового типа. _Generic не допускает перечисления 2 типов, которые могут быть одинаковыми, например unsigned * и size_t *. Так что есть ограничения.

Макрос type_id(X) создает перечисление для 3 типов, которые можно использовать для проверки наличия проблем во время выполнения, как с LIST_POP(L, &d); ниже.

typedef struct node {
  void *data;
  int type;
} node;

typedef struct list {
  node *head;
  node *tail;
} list;

node node_var;
void List_Push(list *l, void *p, int type) {
  // tbd code - simplistic use of global for illustration only
  node_var.data = p;
  node_var.type = type;
}

void *List_Pop(list *l, int type) {
  // tbd code
  assert(node_var.type == type);
  return node_var.data;
}

#define cast(X,ptr) _Generic((X), \
  double *: (double *) (ptr), \
  unsigned *: (unsigned *) (ptr), \
  int *: (int *) (ptr) \
  )

#define type_id(X) _Generic((X), \
  double *: 1, \
  unsigned *: 2, \
  int *: 3 \
  )

#define LIST_PUSH(L, data)  { List_Push((L),(data), type_id(data)); }
#define LIST_POP(L, dataptr) (*(dataptr)=cast(*dataptr, List_Pop((L), type_id(*dataptr))) )

Пример использования и вывод

int main() {
  list *L = 0; // tbd initialization
  int i = 42;
  printf("%p %d\n", (void*) &i, i);
  LIST_PUSH(L, &i);
  int *j;
  LIST_POP(L, &j);
  printf("%p %d\n", (void*) j, *j);
  double *d;
  LIST_POP(L, &d);
}

42
42
assertion error
person chux - Reinstate Monica    schedule 21.06.2017

В C нет способа сделать то, что вы хотите. Нет способа сохранить тип в переменной, и C не имеет системы шаблонов, такой как C++, которая позволила бы вам подделать его в препроцессоре.

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

person Max    schedule 21.06.2017

C не имеет никакой информации о типе времени выполнения и не имеет типа «Тип». Типы не имеют смысла после того, как код был скомпилирован. Таким образом, нет решения того, о чем вы спрашиваете, предоставленного языком.

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

enum ElementType
{
    ET_INT; // int
    ET_DOUBLE; // double
    ET_CAR; // struct Car
    // ...
 };

и перечислите здесь любой тип, который когда-либо должен попасть в ваш контейнер. Другая причина заключается в том, что ваш контейнер должен стать владельцем хранящихся в нем объектов и, следовательно, должен знать, как их уничтожить (а иногда и как их клонировать). Для таких случаев я рекомендую использовать указатели на функции:

typedef void (*ElementDeleter)(void *element);
typedef void *(*ElementCloner)(const void *element);

Затем расширьте свою структуру, чтобы она содержала следующее:

typedef struct list {
    node *head;
    node *tail;
    ElementDeleter deleter;
    ElementCloner cloner;
} list;

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

myList->deleter(myNode->data);
// delete the contained element without knowing its type
person Community    schedule 21.06.2017

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

person OVelychko    schedule 21.06.2017

В отличие от Java или C++, C не обеспечивает никакой безопасности типов. Чтобы кратко ответить на ваш вопрос, изменив тип узла следующим образом:

struct node {
   node* prev;  /* put these at front */
   node* next;
   /* no data here */
};

Затем вы можете отдельно объявить узлы, несущие любые данные

struct data_node {.
    data_node *prev;            // keep these two data members at the front 
    data_node *next;            // and in the same order as in struct list.

    // you can add more data members here.
};
/* OR... */
enter code here
struct data_node2 {
  node node_data;    /* WANING: this may look a bit safer, but is _only_ if placed at the front.
  /*  more data ... */
};

Затем вы можете создать библиотеку, которая работает со списками node без данных.

void list_add(list* l, node* n);
void list_remove(list* l, node* n);
/* etc... */

И путем кастинга используйте этот API-интерфейс «общие списки», чтобы выполнить операцию в вашем списке.

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

struct data_list
{
  data_node* head;    /* this makes intent clear. */
  data_node* tail;
};

struct data2_list
{
  data_node2* head;
  data_node2* tail;
};

/* ... */

data_node* my_data_node = malloc(sizeof(data_node));
data_node2* my_data_node2 = malloc(sizeof(data_node2));

/* ... */

list_add((list*)&my_list, (node*)my_data_node); 
list_add((list*)&my_list2, &(my_data_node2->node_data)); 

/* warning above is because one could write this */
list_add((list*)&my_list2, (node*)my_data_node2); 


/* etc... */

Эти два метода генерируют один и тот же объектный код, поэтому, какой из них вы выберете, на самом деле зависит от вас.

Кроме того, избегайте обозначения структуры typedef, если это позволяет ваш компилятор, что в наши дни делает большинство компиляторов. Это увеличивает читабельность в долгосрочной перспективе, ИМХО. Вы можете быть уверены, что некоторые этого не сделают, а некоторые согласятся со мной по этому вопросу.

person Michaël Roy    schedule 21.06.2017
comment
C не обеспечивает никакой безопасности типов - вы действительно не это имеете в виду? Вероятно, вы имеете в виду, что вам нужно пожертвовать некоторой безопасностью типов для универсального кода или чего-то в этом роде. - person ; 21.06.2017
comment
@felixpalmen некоторые компиляторы C обрабатывают все указатели данных как void*. - person Michaël Roy; 21.06.2017
comment
Я бы не назвал такую ​​вещь компилятором C. У вас есть пример такого неправильного поведения, который позволил бы неявно преобразовывать любой тип указателя данных в любой другой? - person ; 21.06.2017
comment
@FelixPalmen A a; B* b; void* p = &a; b=p; - person Michaël Roy; 21.06.2017
comment
void * — это общий тип указателя. Смотрите мой первый комментарий. И на самом деле то, что вы здесь описываете, скорее всего, является неопределенным поведением. Чего я не вижу, так это того, что компилятор обрабатывает все указатели данных как void*. Все указатели данных неявно преобразуются из/в void * в соответствии со стандартом. - person ; 21.06.2017
comment
Если бы это был UB, каждый вызов malloc() и free() без приведения привел бы к неопределенному поведению. Это может привести к UB, если A и B бинарно несовместимы. В любом случае это демонстрирует, что C не обеспечивает безопасность типов. Это то, о чем должен знать кто-то из мира Java, как утверждает ОП. - person Michaël Roy; 21.06.2017
comment
Ты не прав. Это приводит к UB, как только A и B не являются совместимыми типами, а правила для совместимых типов довольно узки. - person ; 21.06.2017
comment
Ты не прав. Код, подобный тому, что я описал в своем комментарии, широко используется в мире C. Если вы сомневаетесь, внимательно изучите любую реализацию сокета posix. - person Michaël Roy; 22.06.2017
comment
API сокетов POSIX — лучший пример широко распространенного плохо определенного C. То, что люди используют его, не делает его правильным. Конечно, назначение ваших указателей, как показано выше, еще не является UB, доступ к объекту наверняка является: C11 §6.5.16.1 -- 3 Если значение хранится в объекте, считывается из другого объекта, который каким-либо образом перекрывает хранилище первого объекта, тогда перекрытие должно быть точным, и два объекта должны иметь уточненные или неопределенные версии совместимого типа; в противном случае поведение не определено. -- по-прежнему никак не связано с вашим ложным утверждением о безопасности типов в C. - person ; 22.06.2017
comment
Имейте в виду, что стандарт c89 был создан для того, чтобы сокеты все равно компилировались, и вы поймете c. - person Michaël Roy; 22.06.2017
comment
Нет такой вещи, как с98, вы, наверное, имеете в виду с89. Код сокетов, конечно, компилируется, но вы должны быть очень осторожны с псевдонимами указателей, вы можете легко вызвать UB с этим беспорядком. И, конечно же, код, вызывающий UB, также компилируется. Вы должны попытаться понять C, и ваше утверждение C не обеспечивает никакой безопасности типов просто неверно. - person ; 22.06.2017