Союз против указателя void

Каковы были бы различия между использованием просто void *, а не объединением? Пример:

struct my_struct {
    short datatype;
    void *data;
}

struct my_struct {
    short datatype;
    union {
        char* c;
        int* i;
        long* l;
    };
};

Оба эти могут быть использованы для достижения того же самого, лучше ли использовать союз или void * хотя?

Ответ 1

У меня был именно такой случай в нашей библиотеке. У нас был общий модуль отображения строк, который мог использовать разные размеры для индекса, 8, 16 или 32 бит (по историческим причинам). Таким образом, код был заполнен таким кодом:

if(map->idxSiz == 1) 
   return ((BYTE *)map->idx)[Pos] = ...whatever
else
   if(map->idxSiz == 2) 
     return ((WORD *)map->idx)[Pos] = ...whatever
   else
     return ((LONG *)map->idx)[Pos] = ...whatever

Было 100 таких строк. На первом этапе я сменил его на объединение, и я обнаружил, что он более читабельный.

switch(map->idxSiz) {
  case 1: return map->idx.u8[Pos] = ...whatever
  case 2: return map->idx.u16[Pos] = ...whatever
  case 3: return map->idx.u32[Pos] = ...whatever
}

Это помогло мне лучше понять, что происходит, и тогда я мог бы решить полностью удалить варианты idxSiz, используя только 32-битные индексы. Но это было возможно только после того, как код стал более читаемым. PS: Это была лишь небольшая часть нашего проекта, которая составляет около 100 тысяч строк кода, написанных людьми, которых больше нет. Таким образом, изменения в коде являются постепенными, чтобы не нарушать приложения.

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

Изменить: добавлен комментарий, так как комментарии не форматируют код:

Переход к переключателю пришел раньше (теперь это настоящий код)

switch(this->IdxSiz) { 
  case 2: ((uint16_t*)this->iSort)[Pos-1] = (uint16_t)this->header.nUz; break; 
  case 4: ((uint32_t*)this->iSort)[Pos-1] = this->header.nUz; break; 
}

был изменен на

switch(this->IdxSiz) { 
  case 2: this->iSort.u16[Pos-1] = this->header.nUz; break; 
  case 4: this->iSort.u32[Pos-1] = this->header.nUz; break; 
}

Мне не следовало сочетать все благоустройство, которое я делал в коде, и только показывать этот шаг. Но я отправил свой ответ из дома, где у меня не было доступа к коду

Ответ 2

На мой взгляд, указатель void и явное литье - лучший способ, потому что для каждого опытного программиста C очевидно, что это за намерение.

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

Ответ 3

Чаще всего использовать объединение для хранения реальных объектов, а не указателей.

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

Ответ 4

Подход union требует, чтобы вы знали априори все типы, которые могут быть использованы. Подход void * позволяет хранить типы данных, которые могут даже не существовать, когда написанный код написан (хотя многое сделать с таким неизвестным типом данных может быть сложным, например, требуется передать указатель на функцию, которая будет вызываться по этим данным вместо того, чтобы обрабатывать его напрямую).

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

Ответ 5

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

Ответ 6

Мое предпочтение было бы пойти на союзный маршрут. Приведение из void * является тупым инструментом и доступ к базе данных с помощью правильно введенного указателя дает немного дополнительную безопасность.

Ответ 7

Бросьте монету. Союз чаще используется с типами без указателей, поэтому здесь выглядит немного странно. Однако явная спецификация типа, которую он предоставляет, представляет собой приличную неявную документацию. void * будет хорошо, если вы всегда знаете, что будете получать доступ только к указателям. Не начинайте вкладывать целые числа и полагаться на sizeof (void *) == sizeof (int).

Я не чувствую, что в конце концов любой из них имеет преимущество перед другим.

Ответ 8

Это немного затенено в вашем примере, потому что вы используете указатели и, следовательно, косвенные. Но union, безусловно, имеет свои преимущества.

Представьте себе:

struct my_struct {
   short datatype;
   union {
       char c;
       int i;
       long l;
   };
};

Теперь вам не нужно беспокоиться о том, откуда происходит выделение для части ценности. Никакой отдельный malloc() или ничего подобного. И вы можете обнаружить, что доступ к ->c, ->i и ->l выполняется немного быстрее. (Хотя это может иметь значение только в том случае, если их много.)

Ответ 9

Если вы создаете свой код с -fstrict-aliasing (gcc) или аналогичными параметрами в других компиляторах, вы должны быть очень осторожны с тем, как вы делаете свое кастинг. Вы можете наложить указатель столько, сколько хотите, но когда вы его разыщите, тип указателя, который вы используете для разыменования, должен соответствовать оригинальному типу (за некоторыми исключениями). Вы не можете, например, сделать что-то вроде:

void foo(void * p)
{
   short * pSubSetOfInt = (short *)p ;
   *pSubSetOfInt = 0xFFFF ;
}

void goo()
{
   int intValue = 0 ;

   foo( &intValue ) ;

   printf( "0x%X\n", intValue ) ;
}

Не удивляйтесь, если это печатает 0 (скажем) вместо 0xFFFF или 0xFFFF0000, как вы можете ожидать при построении с оптимизацией. Один из способов сделать этот код - это сделать то же самое, используя объединение, и, вероятно, код будет легче понять.

Ответ 10

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

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

Ответ 11

Объединение резервирует достаточное пространство для самого большого члена, они не должны быть одинаковыми, поскольку void * имеет фиксированный размер, тогда как объединение может использоваться для произвольного размера.

#include <stdio.h>
#include <stdlib.h>

struct m1 {
   union {
    char c[100];
   };
};

struct m2 {
    void * c;
 };


 int
 main()
 {
printf("sizeof m1 is %d ",sizeof(struct m1));
printf("sizeof m2 is %d",sizeof(struct m2));
exit(EXIT_SUCCESS);
 }

Вывод: sizeof m1 составляет 100 sizeof m2 составляет 4

РЕДАКТИРОВАТЬ: если вы используете только указатели того же размера, что и void *, я думаю, что объединение лучше, так как вы получите немного обнаружения ошибок при попытке установить .c с помощью указателя целых чисел и т.д. '. void *, если вы не создаете свой собственный распределитель, определенно быстро и грязно, к лучшему или к худшему.