Используйте базовую структуру для извлечения значений из void * - PullRequest
5 голосов
/ 31 октября 2019

У меня есть много типов структур в моем проекте, и другая структура, которая содержит указатель на одну из этих структур. Например,

struct one{int num = 1;};
struct two{int num = 2;};
struct three{int num = 3;};

// These structs hold many other values as well, but the first value is always `int num`.

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

struct Holder{void* any_struct};

Мой вопрос, мне нужны значения внутри этих структур, но у меня есть voidуказатель, можно ли объявить базовую структуру, что первая переменная является int, привести ее и использовать для извлечения переменной num из этих структур, таких как:

struct Base{int num};
((Base*) Holder->any_struct)->num
// Gives 1, 2 or 3

Ответы [ 4 ]

4 голосов
/ 31 октября 2019

Если вам нужно только извлечь num, вы можете использовать memcpy. Предполагая, что он всегда int и всегда первым и всегда присутствует.

int num = 0;
memcpy(&num, Holder->any_struct, sizeof(int));
// Gives 1, 2 or 3 in num.

стандартная секция C99 6.7.2.1, точка маркера 13:

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

Подробнее о стандарте в этом ответе .

3 голосов
/ 31 октября 2019

Я думаю, что это приемлемо, и я видел этот шаблон в других проектах на Си. Например, в libuv. Они определяют тип uv_handle_t и называют его «базовым дескриптором» ... вот информация на их странице (http://docs.libuv.org/en/v1.x/handle.html)

uv_handle_t - базовый тип для всех типов дескрипторов libuv.

Структуры выровнены так, что любой дескриптор libuv может быть приведен к uv_handle_t. Все функции API, определенные здесь, работают с любым типом дескриптора.

И как они реализуют шаблон, который вы могли бы принять. определить макрос для общих полей:

#define UV_HANDLE_FIELDS                                                      \
  /* public */                                                                \
  void* data;                                                                 \
  /* read-only */                                                             \
  uv_loop_t* loop;                                                            \
  uv_handle_type type;                                                        \
  /* private */                                                               \
  uv_close_cb close_cb;                                                       \
  void* handle_queue[2];                                                      \
  union {                                                                     \
    int fd;                                                                   \
    void* reserved[4];                                                        \
  } u;                                                                        \
  UV_HANDLE_PRIVATE_FIELDS                                                    \

/* The abstract base class of all handles. */
struct uv_handle_s {
  UV_HANDLE_FIELDS
};

... и затем они используют этот макрос для определения «производных» типов:

/*
 * uv_stream_t is a subclass of uv_handle_t.
 *
 * uv_stream is an abstract class.
 *
 * uv_stream_t is the parent class of uv_tcp_t, uv_pipe_t and uv_tty_t.
 */
struct uv_stream_s {
  UV_HANDLE_FIELDS
  UV_STREAM_FIELDS
};

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

2 голосов
/ 31 октября 2019

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

Как говорится, самый простой вид интерфейса базового класса похож на тот, который у вас есть:

typedef struct
{
  int num;
} base_t;

typedef struct 
{
  base_t base;
  /* struct-specific stuff here */
} one_t;

one_t one = ...;
...
base_t* ref = (base_t*)&one;
ref->num = 0; // this is well-defined

В этом коде base_t* не указывает непосредственно на num но у первого объекта в структуре, который имеет base_t. Из-за этого можно отказаться от ссылки на него.

Однако ваш исходный код с int num, распределенным по 3 структурам, не обязательно позволяет вам преобразовывать один тип структуры в другой, даже если выдоступ только к начальному члену num. Существуют различные подробности относительно строгого псевдонима и совместимых типов, которые могут вызвать проблемы.

0 голосов
/ 31 октября 2019

Конструкция, которую вы описываете для использования указателя на «базовую» структуру в качестве псевдонима для нескольких «производных» структур, хотя часто используется с такими вещами, как struct sockaddr, не гарантируется, что она будет работать по стандарту C .

Несмотря на то, что есть какой-то язык, который можно предложить, он может поддерживаться, в частности, 6.7.2.1p15:

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

Другие части предполагают, что это не так, в частности 6.3.2.3, в котором обсуждаются допустимые преобразования указателей:

1 Указатель на void может быть преобразован в или из указателя на любой тип объекта. Указатель на любой тип объекта может быть преобразован в указатель на void и обратно;результат должен сравниваться равным исходному указателю.

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

3 Целочисленное константное выражение со значением 0 или такое выражение, приведенное к типу void *, называется нулевой константой указателя,Если константа нулевого указателя преобразуется в тип указателя, результирующий указатель, называемый нулевым указателем, гарантированно сравнивается с неравным указателем на любой объект или функцию.

4 Преобразованиенулевой указатель на другой тип указателя дает нулевой указатель этого типа. Любые два нулевых указателя должны сравниваться равными.

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

6 ЛюбойТип указателя может быть преобразован в целочисленный тип. За исключением случаев, указанных ранее, результат определяется реализацией. Если результат не может быть представлен в целочисленном типе, поведение не определено. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.

7 Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен для ссылочного типа, поведение не определено.
В противном случае при обратном преобразовании результат будет сравниваться равным исходному указателю. Когда указатель на объект преобразуется в указатель на тип символа, результат указывает на младший адресуемый байт объекта. Последовательные приращения результата, вплоть до размера объекта, дают указатели на оставшиеся байты объекта.

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

Из вышесказанного не следует, что приведение из одной структуры в другую, гдедопускается использование типа первого члена.

Допускается, однако, использование union для выполнения, по сути, того же самого. В разделе 6.5.2.3p6 говорится:

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

Итак, что вы можете сделать, это определить объединение, которое содержит все возможные типы, а также базовый тип:

union various {
    struct base { int num; } b;
    struct one { int num; int a; } s1;
    struct two { int num; double b; } s2;
    struct three { int num; char *c; } s3;
};

Затем вы будете использовать этот союз в любом месте, где вам нужно, из трех подтипов. и вы можете свободно осмотреть базовый элемент, чтобы определить его тип. Например:

void foo(union various *u)
{
    switch (u->b.num) {
    case 1:
        printf("s1.a=%d\n", u->s1.a);
        break;
    case 2:
        printf("s2.b=%f\n", u->s2.b);
        break;
    case 1:
        printf("s3.c=%s\n", u->s3.c);
        break;
    }
}

...

union various u;
u.s1.num = 1;
u.s1.a = 4;
foo(&u);
u.s2.num = 2;
u.s2.b = 2.5;
foo(&u);
u.s3.num = 3;
u.s3.c = "hello";
foo(&u);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...