Непрозрачные типы, выделяемые в стеке в C

При разработке интерфейса C обычно допускается в публичный интерфейс (.h) только то, что должно быть известно пользовательской программе.

Следовательно, например, внутренние компоненты структур должны оставаться скрытыми, если пользовательская программа не нуждается в них. Это действительно хорошая практика, так как контент и поведение структуры могут измениться в будущем, не затрагивая интерфейс.

Отличным способом достижения этой цели является использование неполных типов.

typedef struct foo opaqueType;

Теперь можно создать интерфейс, содержащий только указатели на opaqueType, без пользовательской программы, когда-либо требующей знать внутреннюю работу struct foo.

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

Обходной задачей является выделение "типа оболочки", например:

typedef struct { int faketable[8]; } opaqueType;

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

В основном это работает. Но в одном случае (GCC 4.4) компилятор жалуется, что он нарушает строгое сглаживание и генерирует багги-двоичный файл.

Теперь я прочитал массу вещей о строгом псевдониме, поэтому, я думаю, теперь я понимаю, что это значит.

Возникает вопрос: существует ли способ определить непрозрачный тип, который тем не менее может быть выделен в стеке, и не нарушая строгое правило сглаживания?

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

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

[Изменить] Информация дополняет:

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

  • Частный и открытый тип. Я делаю публичный тип приватным внутри файла .c. Не имеет значения, если они являются частью одного и того же союза. Не имеет значения, содержит ли открытый тип char.
  • Если все операции с закрытым типом просто читаются, проблем нет. Только записи вызывают проблемы.
  • Я также подозреваю, что только функции, которые автоматически встроены, попадают в неприятности.
  • Проблема возникает только при gcc 4.4 при настройке -O3. -O2 в порядке.

Наконец, моя цель - C90. Может быть, C99, если на самом деле нет выбора.

Ответ 1

Вы можете принудительно выполнить выравнивание с помощью max_align_t, и вы можете избежать строгих проблем с псевдонимом, используя массив char, поскольку char явно разрешено использовать псевдоним любого другого типа.

Что-то по строкам:

#include <stdint.h>
struct opaque
{
    union
    {
        max_align_t a;
        char b[32]; // or whatever size you need.
    } u;
};

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

ОБНОВЛЕНИЕ. Если вы нацеливаете C11, вы также можете использовать alignas():

#include <stdint.h>
#include <stdalign.h>
struct opaque
{
    alignas(max_align_t) char b[32];
};

Конечно, вы можете заменить max_align_t тем типом, который вам кажется нужным. Или даже целое число.

ОБНОВЛЕНИЕ # 2:

Тогда использование этого типа в библиотеке будет чем-то вроде строк:

void public_function(struct opaque *po)
{
    struct private *pp = (struct private *)po->b;
    //use pp->...
}

Таким образом, поскольку вы пишете указатель на char, вы не нарушаете строгие правила псевдонимов.

Ответ 2

То, что вы хотите, является своего рода эквивалентом контроля доступа С++ private в C. Как вы знаете, такой эквивалент не существует. Подход, который вы даете, примерно то, что я сделал бы. Тем не менее, я бы сделал opaqueType непрозрачным для внутренних компонентов, реализующих тип, поэтому я был бы вынужден применить его к реальному типу внутри внутренних компонентов. Принудительное приведение не должно генерировать предупреждение, о котором вы упоминаете.

Несмотря на громоздкость в использовании, вы можете определить интерфейс, обеспечивающий "стек выделенной" памяти непрозрачному типу, не подвергая размерную структуру. Идея заключается в том, что код реализации отвечает за распределение стека, а пользователь переходит в функцию обратного вызова, чтобы получить указатель на выделенный тип.

typedef struct opaqueType_raii_callback opqaueType_raii_callback;
struct opaqueType_raii_callback {
    void (*func)(opqaueType_raii_callback *, opqaueType *);
};
extern void opaqueType_raii (opaqueType_raii_callback *);
extern void opaqueType_raii_v (opaqueType_raii_callback *, size_t);


void opaqueType_raii (opaqueType_raii_callback *cb) {
    opaqueType_raii_v(cb, 1);
}

void opqaueType_raii_v (opaqueType_raii_callback *cb, size_t n) {
    opaqueType x[n];
    cb->func(cb, x);
}

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

struct foo_callback_data {
    opaqueType_raii_callback cb;
    int my_data;
    /* other data ... */
};

void foo_callback_function (opaqueType_raii_callback *cb, opaqueType *x) {
    struct foo_callback_data *data = (void *)cb;
    /* use x ... */
}

void foo () {
    struct foo_callback_data data;
    data.cb.func = foo_callback_function;
    opaqueType_raii(&data.cb);
}

Ответ 3

Для меня это похоже на то, что просто не должно быть сделано.

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

Конечно, вы не можете документировать, что одна или другая вещь была возможна, но язык C использует этот подход (строгий псевдоним), который вы можете более или менее взломать от ответа Родриго (используя max_align_t)., По правилу вы не можете узнать по интерфейсу, какие ограничения конкретный компилятор накладывает на фактическую структуру внутри реализации (для некоторых эзотерических микроконтроллеров даже тип памяти может иметь значение), поэтому я не думаю, что это может надёжно выполняться по-настоящему кросс-платформенно.