Можно ли инициализировать ссылку на структуру массивом?

Можно ли сделать что-то подобное?:

struct S {
  int length;
  char data[100];
};

class C {
  uint8_t buffer[10];
  S& s = buffer[0];
};

Если я использую указатель, он компилируется, но работать со ссылкой было бы удобнее:

class C {
  uint8_t buffer[10];
  S* s = (S*)buffer;
};

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

ОБНОВЛЯТЬ:

Кажется, я недостаточно ясно объяснил свои намерения. Возможно, фрагмент реального кода может помочь.

typedef struct BP_Error_t {
    uint8_t cause;
    uint8_t error;
} BP_Error_t;

typedef struct BP_Upload_Rq_t {
    char    file[FILE_NAME_LENGTH];
    uint8_t size;
    uint8_t chunk[MAX_FILE_CHUNK];
} BP_Upload_Rq_t;

typedef struct BusPacket_t {
    uint8_t source;
    uint8_t target;
    uint8_t packet_id;
    uint8_t length;
    union {
        BP_Error_t          error;
        BP_TPI_Poll_t       tpi_poll;
        BP_TPI_ACK_t        tpi_ack;
        BP_WiFi_Poll_t      wifi_poll;
        BP_WiFi_ACK_t       wifi_ack;
        BP_Erase_Rq_t       erase_rq;
        BP_Erase_Rsp_t      erase_rsp;
        BP_Upload_Rq_t      upload_rq;
        BP_Upload_Rsp_t     upload_rsp;
        BP_Download_Rq_t    download_rq;
        BP_Download_Rsp_t   download_rsp;
        BP_DFU_Rq_t         dfu_rq;
        BP_DFU_Rsp_t        dfu_rsp;
    };
} BusPacket_t;

Как видите, у меня есть структура, представляющая различные пакеты данных. Sizeof() будет определяться самым длинным членом объединения, который составляет 240 байт. Теперь предположим, что я хочу быть готовым отправить пакет ошибок (длиной всего 6 байт) в качестве члена класса.

Если я объявлю его как BusPacket_t, я потрачу много памяти впустую. Итак, я подумал, что могу выделить 6-байтовый массив, а затем присвоить ему псевдоним структуры для облегчения доступа.

Я бы использовал функцию-член, которая возвращает ссылку на буфер, который был преобразован в объект S POD. Обратите внимание: если типы данных не POD, я подозреваю, что вы быстро окажетесь на земле UB.

Eljay 13.07.2024 22:16

Пожалуйста, исправьте пропущенные точки с запятой. Непонятно, какую задачу вы выполняете, почему хотите приводить char, один байт к 100+ байтам.

3CxEZiVlQ 13.07.2024 22:21

Версия указателя также просто UB. Концептуально то, что вы хотите сделать, невозможно в объектной модели C++.

user17732522 13.07.2024 22:39

Правильный подход — применить std::start_lifetime_as к буферу, содержащему пакет, для создания объекта правильного типа и правильного размера, а затем использовать указатель, возвращаемый из него в точке использования. Если код должен иметь возможность обрабатывать оба типа указателей, он должен использовать соответствующие типы указателей.

user17732522 13.07.2024 22:44

Кажется, проблема в том, что в BusPacket_t не должно быть союза. Вместо этого должен быть один BusPacket_X_t для каждого из потенциальных вариантов (вероятно, в качестве шаблона), а затем (возможно) BusPacket_t должен быть их союз (или лучше std::variant). В противном случае, даже с уточнениями, я могу только повторить, что объектная модель не позволяет делать то, что вы хотите, и ваш текущий подход находится в сфере UB.

user17732522 13.07.2024 22:48

@user17732522 user17732522 да, я думал об этом. Но поля заголовка создадут неоднозначность при объединении. Я посмотрю, действительно ли мне нужен профсоюз. Это сделано исключительно для удобства анализа входящих данных (поле package_id определяет фактический формат полезной нагрузки и соответствующий член объединения, который будет использоваться)

Maple 13.07.2024 22:57

@Maple С заголовками проблем нет. Если вы создаете объединение отдельных типов, вам все равно разрешено проверять заголовок любого из них через любого из членов объединения, поскольку заголовок представляет собой общую начальную последовательность стандартных типов макета (en.cppreference.com/ w/cpp/language/data_members#Standard-la‌​yout).

user17732522 13.07.2024 23:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
7
125
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Обратите внимание: если вы просто пишете на C++, то typedef бесполезен.

В противном случае я бы перевернул ситуацию и сначала объявил BusPacket_t, а затем использовал его как «заголовок» в других структурах, например:

struct BusPacket_t {
    uint8_t source;
    uint8_t target;
    uint8_t packet_id;
    uint8_t length;
};

struct BP_Error_t {
    BusPacket_t packet;
    uint8_t     cause;
    uint8_t     error;
};

struct BP_Upload_Rq_t {
    BusPacket_t packet;
    char        file[FILE_NAME_LENGTH];
    uint8_t     size;
    uint8_t     chunk[MAX_FILE_CHUNK];
};

...

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

Я предполагаю, что вам нужно будет читать и записывать эти данные в такие места, как сетевой поток, поэтому я бы избегал использования классов, которые могут в конечном итоге добавить нежелательные элементы в вашу структуру. В противном случае я бы получил выгоду от класса BusPacket_t и воспользовался бы его функциями (и, возможно, некоторыми виртуальными функциями, чтобы получить общий размер этого пакета).

class BusPacket_t {
public:
    virtual size_t size() const = 0;
};

class BP_Error_t : public BusPacket_t {
public:
    virtual size_t size() const { return sizeof(BP_Error_t); }
};

Но я думаю, что sizeof() будет включать размер виртуального стола (например). Так что, возможно, это не то, что вы хотите...


Чтобы ответить на ваш вопрос о ссылке, вот как ее написать, чтобы она скомпилировалась:

struct S {
  int length;
  char data[100];
};

struct C {
  uint8_t buffer[10];
  S& s = *(S *)buffer;
};

В некоторой степени это будет работать так, как ожидалось, за исключением того, что это UB, поскольку uint8_t buffer[10] может представлять или не представлять объект S:

  • в вашем примере это не так, поскольку buffer составляет 10 байт, а sizeof(S), скорее всего, 104 байта.
  • насколько я знаю, если размер совпадает, он считается UB, поскольку вы используете reinterpret_cast<>() ((S *), также называемый старым приведением, представляет собой reinterpret_cast<>()).
  • buffer может быть правильно выровнено для объекта S, а может и нет (хотя, скорее всего, будет хорошо, если в начале структуры вы выделяете память, поскольку выделенная память правильно выровнена для всех основных типов). [см. комментарии]
    • Как показано в комментариях, принудительное выравнивание возможно с помощью alignof(<type>), однако, если у вас много требований к выравниванию (т. е. необходимо сопоставить структуру A, B и C), это становится довольно сложным в обработке, т. е. вам нужно найти наиболее ограничивающее выравнивание.
  • для того, чтобы buffer представлял S, требуется std::launder(), поэтому мы должны как минимум использовать S& s = *std::launder((S *)buffer); — однако, как упоминалось выше, одно из правил функции std::launder() заключается в том, что buffer должно представлять объект x и поскольку buffer — 10 байт, а S — 104, это явно неверно. [см. комментарии]

Это уже UB, потому что ни один объект S не может существовать. Чтобы он существовал в массиве, размер массива должен быть не меньше S. Даже если бы объект существовал, простое использование приведенного указателя и его разыменование было бы UB. Сначала нужно будет пройти std::launder.

user17732522 14.07.2024 00:00

Поскольку buffer не выровнен должным образом для S, на практике также существует большая вероятность неожиданного поведения в результате этого UB, даже если не происходит доступа за пределы исходных массивов.

user17732522 14.07.2024 00:01

Вместо виртуальных функций можно использовать std::variant, а для получения размера можно использовать std::visit([](auto& typedPacket){ return sizeof(typedPacked); }, packet). Раздражает только то, что std::variant будет содержать собственный дискриминатор типов, в то время как OP уже встроен в пакетные данные. Может быть полезно написать специальный std::variant/std::visit для типа пакета.

user17732522 14.07.2024 00:10

Принят ответ за предоставление компилируемого синтаксиса для исходного вопроса. Остальная часть ответа и комментарии заставляют меня переоценить весь подход. Я, вероятно, воспользуюсь предлагаемым включением заголовка с атрибутом «packed». Кроме того, есть ли какая-либо разница между включением и наследованием заголовка, о которой мне следует знать?

Maple 14.07.2024 23:14

@user17732522 user17732522 «Потому что буфер не выровнен должным образом для S». Мне приходится использовать упакованный атрибут для всех этих структур, чтобы убедиться, что никакой мусор не попадает в шину. AFAIK, он принудительно выравнивает их по байтам. Не сделает ли это их совместимыми с байтовыми массивами, которые вообще не должны иметь никакого выравнивания?

Maple 14.07.2024 23:33

@Maple Проблема в том, что массив байтов должен быть выровнен в соответствии с требованием выравнивания S. В стандартном C++ никогда не может быть допустимого указателя на какой-либо тип T, который не соответствует как минимум alignof(T). Упакованные атрибуты — это расширения, специфичные для компилятора, которые работают для определенного подмножества архитектур. Не все архитектуры поддерживают несогласованный доступ, а если и поддерживают, то обычно (намного) медленнее. Упакованные атрибуты также накладывают ограничения на использование структуры в зависимости от компилятора.

user17732522 15.07.2024 02:35

@Maple Например, как правило, никакие указатели или ссылки на члены структуры не могут передаваться (потому что они будут смещены). Правильный вариант — написать alignof(S) uint8_t buffer[10]; вместо uint8_t buffer[10];, чтобы принудительно выполнить требования к выравниванию, независимо от того, использует ли S упакованный атрибут. (Конечно, это не устраняет другие причины UB.)

user17732522 15.07.2024 02:36

@user17732522 user17732522 как насчет присвоения адреса члена структуры указателю локальной структуры, если все три (указатель, структура и элемент) определены с помощью struct __attribute__((packed, Align(1)))? Сохраняются ли эти атрибуты, когда структура используется в качестве члена класса?

Maple 18.07.2024 23:01

@Maple «указатель на локальную структуру»: что именно вы имеете в виду? Конечно, все это будет полностью зависеть от компилятора. Предполагая GCC: если вообще важно, так это атрибут aligned для типа, на который указывает тип переменной-указателя. Однако мне даже неясно, можно ли использовать атрибут aligned в typedef для эффективного уменьшения выравнивания. Даже если это должно быть возможно, насколько я понимаю, существуют давние проблемы, из-за которых указатели всегда считаются выровненными для нормального выравнивания их типа.

user17732522 19.07.2024 00:11

@Maple Поэтому я думаю, что формирование (или, по крайней мере, разыменование) любого указателя на член члена структуры packed потенциально будет небезопасно, особенно на архитектурах со строгими требованиями к выравниванию, но потенциально также, например. на x86, когда компилятор решает использовать некоторые инструкции SSE, требующие выравнивания. Я не уверен, выполняет ли GCC какие-либо реальные оптимизации, предполагающие выравнивание указателей, помимо соответствующего выбора машинных инструкций. См., например. gcc.gnu.org/bugzilla/show_bug.cgi?id=51628 для обсуждения всего вышеперечисленного.

user17732522 19.07.2024 00:14

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