Можно ли сделать что-то подобное?:
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-байтовый массив, а затем присвоить ему псевдоним структуры для облегчения доступа.
Пожалуйста, исправьте пропущенные точки с запятой. Непонятно, какую задачу вы выполняете, почему хотите приводить char, один байт к 100+ байтам.
Версия указателя также просто UB. Концептуально то, что вы хотите сделать, невозможно в объектной модели C++.
Правильный подход — применить std::start_lifetime_as к буферу, содержащему пакет, для создания объекта правильного типа и правильного размера, а затем использовать указатель, возвращаемый из него в точке использования. Если код должен иметь возможность обрабатывать оба типа указателей, он должен использовать соответствующие типы указателей.
Кажется, проблема в том, что в BusPacket_t не должно быть союза. Вместо этого должен быть один BusPacket_X_t для каждого из потенциальных вариантов (вероятно, в качестве шаблона), а затем (возможно) BusPacket_t должен быть их союз (или лучше std::variant). В противном случае, даже с уточнениями, я могу только повторить, что объектная модель не позволяет делать то, что вы хотите, и ваш текущий подход находится в сфере UB.
@user17732522 user17732522 да, я думал об этом. Но поля заголовка создадут неоднозначность при объединении. Я посмотрю, действительно ли мне нужен профсоюз. Это сделано исключительно для удобства анализа входящих данных (поле package_id определяет фактический формат полезной нагрузки и соответствующий член объединения, который будет использоваться)
@Maple С заголовками проблем нет. Если вы создаете объединение отдельных типов, вам все равно разрешено проверять заголовок любого из них через любого из членов объединения, поскольку заголовок представляет собой общую начальную последовательность стандартных типов макета (en.cppreference.com/ w/cpp/language/data_members#Standard-layout).





Обратите внимание: если вы просто пишете на 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 байта.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.
Поскольку buffer не выровнен должным образом для S, на практике также существует большая вероятность неожиданного поведения в результате этого UB, даже если не происходит доступа за пределы исходных массивов.
Вместо виртуальных функций можно использовать std::variant, а для получения размера можно использовать std::visit([](auto& typedPacket){ return sizeof(typedPacked); }, packet). Раздражает только то, что std::variant будет содержать собственный дискриминатор типов, в то время как OP уже встроен в пакетные данные. Может быть полезно написать специальный std::variant/std::visit для типа пакета.
Принят ответ за предоставление компилируемого синтаксиса для исходного вопроса. Остальная часть ответа и комментарии заставляют меня переоценить весь подход. Я, вероятно, воспользуюсь предлагаемым включением заголовка с атрибутом «packed». Кроме того, есть ли какая-либо разница между включением и наследованием заголовка, о которой мне следует знать?
@user17732522 user17732522 «Потому что буфер не выровнен должным образом для S». Мне приходится использовать упакованный атрибут для всех этих структур, чтобы убедиться, что никакой мусор не попадает в шину. AFAIK, он принудительно выравнивает их по байтам. Не сделает ли это их совместимыми с байтовыми массивами, которые вообще не должны иметь никакого выравнивания?
@Maple Проблема в том, что массив байтов должен быть выровнен в соответствии с требованием выравнивания S. В стандартном C++ никогда не может быть допустимого указателя на какой-либо тип T, который не соответствует как минимум alignof(T). Упакованные атрибуты — это расширения, специфичные для компилятора, которые работают для определенного подмножества архитектур. Не все архитектуры поддерживают несогласованный доступ, а если и поддерживают, то обычно (намного) медленнее. Упакованные атрибуты также накладывают ограничения на использование структуры в зависимости от компилятора.
@Maple Например, как правило, никакие указатели или ссылки на члены структуры не могут передаваться (потому что они будут смещены). Правильный вариант — написать alignof(S) uint8_t buffer[10]; вместо uint8_t buffer[10];, чтобы принудительно выполнить требования к выравниванию, независимо от того, использует ли S упакованный атрибут. (Конечно, это не устраняет другие причины UB.)
@user17732522 user17732522 как насчет присвоения адреса члена структуры указателю локальной структуры, если все три (указатель, структура и элемент) определены с помощью struct __attribute__((packed, Align(1)))? Сохраняются ли эти атрибуты, когда структура используется в качестве члена класса?
@Maple «указатель на локальную структуру»: что именно вы имеете в виду? Конечно, все это будет полностью зависеть от компилятора. Предполагая GCC: если вообще важно, так это атрибут aligned для типа, на который указывает тип переменной-указателя. Однако мне даже неясно, можно ли использовать атрибут aligned в typedef для эффективного уменьшения выравнивания. Даже если это должно быть возможно, насколько я понимаю, существуют давние проблемы, из-за которых указатели всегда считаются выровненными для нормального выравнивания их типа.
@Maple Поэтому я думаю, что формирование (или, по крайней мере, разыменование) любого указателя на член члена структуры packed потенциально будет небезопасно, особенно на архитектурах со строгими требованиями к выравниванию, но потенциально также, например. на x86, когда компилятор решает использовать некоторые инструкции SSE, требующие выравнивания. Я не уверен, выполняет ли GCC какие-либо реальные оптимизации, предполагающие выравнивание указателей, помимо соответствующего выбора машинных инструкций. См., например. gcc.gnu.org/bugzilla/show_bug.cgi?id=51628 для обсуждения всего вышеперечисленного.
Я бы использовал функцию-член, которая возвращает ссылку на буфер, который был преобразован в объект
SPOD. Обратите внимание: если типы данных не POD, я подозреваю, что вы быстро окажетесь на земле UB.