как создать "спейсер" в структуре памяти класса c++?

<h1>проблема</h1><p>в контексте низкоуровневого встроенного <strong>низкого уровня</strong> я хотел бы создать пустое пространство в памяти, в структуре c++ и без какого-либо имени, чтобы запретить пользователю доступ к такой области памяти.</p><p>прямо сейчас я добился этого, поместив уродливое битовое поле <code>uint32\_t :96;</code>, которое будет удобно заменять три слова, но при этом будет выдано предупреждение от gcc (битовое поле слишком велико, чтобы поместиться в uint32\_t), что вполне законно.</p><p>хотя он работает нормально, он не очень чистый, если вы хотите распространять библиотеку с несколькими сотнями таких предупреждений ...</p><p><strong>как мне сделать это правильно?</strong></p><h1>почему проблема в первую очередь?</h1><p>проект, над которым я работаю, состоит в определении структуры памяти различных периферийных устройств всей линейки микроконтроллеров (stmicroelectronics stm32). для этого результатом является класс, который содержит объединение нескольких структур, которые определяют все регистры, в зависимости от целевого микроконтроллера.</p><p>один простой пример для довольно простого периферийного устройства - следующее: универсальный ввод/вывод (gpio)</p><pre><code>union { struct { gpio\_map0\_moder; gpio\_map0\_otyper; gpio\_map0\_ospeedr; gpio\_map0\_pupdr; gpio\_map0\_idr; gpio\_map0\_odr; gpio\_map0\_bsrr; gpio\_map0\_lckr; gpio\_map0\_afr; gpio\_map0\_brr; gpio\_map0\_ascr; }; struct { gpio\_map1\_crl; gpio\_map1\_crh; gpio\_map1\_idr; gpio\_map1\_odr; gpio\_map1\_bsrr; gpio\_map1\_brr; gpio\_map1\_lckr; uint32\_t :32; gpio\_map1\_afrl; gpio\_map1\_afrh; uint32\_t :64; }; struct { uint32\_t :192; gpio\_map2\_bsrrl; gpio\_map2\_bsrrh; uint32\_t :160; }; }; </code></pre><p>где все <code>gpio\_mapx\_yyy</code> - это макрос, определенный как <code>uint32\_t :32</code> или тип регистра (выделенная структура).</p><p>здесь вы видите <code>uint32\_t :192;</code>, который работает хорошо, но выдает предупреждение.</p><h1>то, что я рассмотрел до сих пор:</h1><p>я мог бы заменить его несколькими <code>uint32\_t :32;</code> (6 здесь), но у меня есть несколько крайних случаев, когда у меня есть <code>uint32\_t :1344;</code> (42) (среди прочих). поэтому я бы не стал добавлять около ста строк поверх остальных 8 тыс., хотя генерация структуры выполняется по сценарию.</p><p>точное предупреждающее сообщение выглядит примерно так: <code>width of 'sool::ll::gpio::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type</code> (мне просто нравится, насколько оно тенистое).</p><p>я предпочел бы <em>не</em> решить эту проблему, просто удалив предупреждение, но использование</p><pre><code>#pragma gcc diagnostic push #pragma gcc diagnostic ignored "-wtherightflag" /* my code */ #pragma gcc diagnostic pop </code></pre><p>может быть решение ... если я найду <code>therightflag</code>. однако, как указано в <a href="https://stackoverflow.com/a/36030403/10593381">этот поток</a> , <code>gcc/cp/class.c</code> с этой печальной частью кода:</p><pre><code>warning\_at (decl\_source\_location (field), 0, "width of %qd exceeds its type", field); </code></pre><p>что говорит нам о том, что для удаления этого предупреждения нет флага <code>-wxxx</code> ...</p>

<p>используйте несколько смежных анонимных битовых полей. так что вместо:</p><pre><code> uint32\_t :160; </code></pre><p>например, у вас будет:</p><pre><code> uint32\_t :32; uint32\_t :32; uint32\_t :32; uint32\_t :32; uint32\_t :32; </code></pre><p>один для каждого регистра вы хотите быть анонимным.</p><p>если у вас есть большие пробелы для заполнения, это может быть более понятным и менее подверженным ошибкам при использовании макросов для повторения одного 32-битного пространства. например, учитывая:</p><pre><code>#define repeat\_2(a) a a #define repeat\_4(a) repeat\_2(a) repeat\_2(a) #define repeat\_8(a) repeat\_4(a) repeat\_4(a) #define repeat\_16(a) repeat\_8(a) repeat\_8(a) #define repeat\_32(a) repeat\_16(a) repeat\_16(a) </code></pre><p>затем можно добавить пробел 1344 (42 * 32 бита):</p><pre><code>struct { ... repeat\_32(uint32\_t :32;) repeat\_8(uint32\_t :32;) repeat\_2(uint32\_t :32;) ... }; </code></pre>

<p>как насчет c++ - это путь?</p><pre><code>namespace gpio { static volatile uint32\_t &map0\_moder = *reinterpret\_cast<uint32\_t*>(0x4000); static volatile uint32\_t &map0\_otyper = *reinterpret\_cast<uint32\_t*>(0x4004); } int main() { gpio::map0\_moder = 42; } </code></pre><p>вы получаете автозаполнение из-за пространства имен <code>gpio</code>, и нет необходимости в фиктивном заполнении. даже более понятно, что происходит, так как вы можете видеть адрес каждого регистра, вам совсем не нужно полагаться на поведение заполнения компилятора.</p>

<p>на арене встраиваемых систем вы можете моделировать аппаратное обеспечение, используя структуру или определяя указатели на адреса регистров. </p><p>моделирование по структуре не рекомендуется, поскольку компилятору разрешено добавлять отступы между элементами для выравнивания (хотя многие компиляторы для встроенных систем имеют прагму для упаковки структуры).</p><p>пример: </p><pre><code>uint16\_t * const uart1 = (uint16\_t *)(0x40000); const unsigned int uart\_status\_offset = 1u; const unsigned int uart\_transmit\_register = 2u; uint16\_t * const uart1\_status\_register = (uart1 + uart\_status\_offset); uint16\_t * const uart1\_transmit\_register = (uart1 + uart\_transmit\_register); </code></pre><p>вы также можете использовать обозначение массива: </p><pre><code>uint16\_t status = uart1[uart\_status\_offset]; </code></pre><p>если вы должны использовать структуру, imho, лучший способ пропустить адреса - определить члена, а не получить к нему доступ: </p><pre><code>struct uart1 { uint16\_t status; uint16\_t reserved1; // transmit register uint16\_t receive\_register; }; </code></pre><p>в одном из наших проектов у нас есть как константы, так и структуры от разных поставщиков (поставщик 1 использует константы, а поставщик 2 использует структуры).</p>

<p>геза прав, что вы действительно не хотите использовать классы для этого.</p><p>но, если вы настаиваете, лучший способ добавить неиспользуемый элемент ширины <em>n</em> байтов, это просто сделать так:</p><pre><code>char unused[n]; </code></pre><p>если вы добавите специфичную для реализации прагму, чтобы предотвратить добавление произвольных отступов к членам класса, это может сработать.</p><hr><p>для gnu c/c++ (gcc, clang и другие, которые поддерживают те же расширения), одно из допустимых мест для размещения атрибута:</p><pre><code>#include <stddef.h> #include <stdint.h> #include <assert.h> // for c11 static\_assert, so this is valid c as well as c++ struct \_\_attribute\_\_((packed)) gpio { volatile uint32\_t a; char unused[3]; volatile uint32\_t b; }; static\_assert(offsetof(struct gpio, b) == 7, "wrong gpio struct layout"); </code></pre><p>(пример <a href="https://godbolt.org/z/e0ttlg">в проводнике компилятора godbolt</a> показывает <code>offsetof(gpio, b)</code> = 7 байт.)</p>

<p>чтобы расширить ответы @ клиффорда и @ адама котвасинского:</p><pre><code>#define rep10(a) a a a a a a a a a a #define rep1034(a) rep10(rep10(rep10(a))) rep10(a a a) a a a a struct foo { int before; rep1034(unsigned int :32;) int after; }; int main(void){ struct foo bar; return 0; } </code></pre>

<p>чтобы расширить ответ клиффорда, вы всегда можете макрокомандировать анонимные битовые поля.</p><p>так что вместо </p><pre><code>uint32\_t :160; </code></pre><p>использование</p><pre><code>#define empty\_32\_1 \ uint32\_t :32 #define empty\_32\_2 \ uint32\_t :32; \ // i guess this also can be replaced with uint64\_t :64 uint32\_t :32 #define empty\_32\_3 \ uint32\_t :32; \ uint32\_t :32; \ uint32\_t :32 #define empty\_uint32(n) empty\_32\_ ## n </code></pre><p>а потом использовать как</p><pre><code>struct a { empty\_uint32(3); /* which resolves to empty\_32\_3, which then resolves to real declarations */ } </code></pre><p>к сожалению, вам понадобится столько же вариантов <code>empty\_32\_x</code>, сколько у вас будет байтов :( тем не менее, это позволяет вам иметь одиночные объявления в вашей структуре.</p>

<p>я думаю, что было бы полезно ввести еще несколько структур; что, в свою очередь, может решить проблему с проставками.</p><h3>назовите варианты</h3><p>в то время как плоские пространства имен хороши, проблема в том, что вы получаете пеструю коллекцию полей, а не простой способ передачи всех связанных полей вместе. кроме того, используя анонимные структуры в анонимном объединении, вы не можете передавать ссылки на сами структуры или использовать их в качестве параметров шаблона.</p><p>поэтому в качестве первого шага я бы рассмотрел<em> исключение <code>struct</code> </em>:</p><pre><code>// gpiomap0.h #pragma once // #includes namespace gpio { struct map0 { gpio\_map0\_moder; gpio\_map0\_otyper; gpio\_map0\_ospeedr; gpio\_map0\_pupdr; gpio\_map0\_idr; gpio\_map0\_odr; gpio\_map0\_bsrr; gpio\_map0\_lckr; gpio\_map0\_afr; gpio\_map0\_brr; gpio\_map0\_ascr; }; } // namespace gpio // gpiomap1.h #pragma once // #includes namespace gpio { struct map1 { // fields }; } // namespace gpio // ... others headers ... </code></pre><p>и, наконец, глобальный заголовок:</p><pre><code>// gpio.h #pragma once #include "gpiomap0.h" #include "gpiomap1.h" // ... other headers ... namespace gpio { union gpio { map0 map0; map1 map1; // ... others ... }; } // namespace gpio </code></pre><p>теперь я могу написать <code>void special\_map0(gpio:: map0 volatile& map);</code>, а также получить краткий обзор всех доступных архитектур.</p><h3>простые проставки</h3><p>с определением, разделенным на несколько заголовков, заголовки индивидуально намного более управляемы.</p><p>поэтому мой первоначальный подход для точного удовлетворения ваших требований - придерживаться повторного <code>std::uint32\_t:32;</code>. да, он добавляет несколько сотен строк к существующим 8-тысячным строкам, но поскольку каждый заголовок индивидуально меньше, он может быть не таким плохим.</p><p>если вы готовы рассмотреть более экзотические решения, хотя ...</p><h3>представляем $.это малоизвестный факт, что $</code> является жизнеспособным символом для идентификаторов c++; это даже жизнеспособный стартовый символ (в отличие от цифр).</p><p><code>$, появляющийся в исходном коде, вероятно, вызовет удивление, и $$$$ определенно привлечет внимание во время проверки кода. это то, что вы можете легко использовать в своих интересах:#define gpio_reserved(index_, n_) std::uint32_t $$$$##index_[n_]; struct map3 { gpio_reserved(0, 6); gpio_map2_bsrrl; gpio_map2_bsrrh; gpio_reserved(1, 5); }; вы даже можете собрать простой «пух» в качестве ловушки перед фиксацией или в своем ci, который ищет $$$$</code> в принятом коде c++ и отклоняет такие коммиты.</p>

<p>чтобы определить большой разделитель как группы из 32 бит.</p><pre><code>#define m\_32(x) m\_2(m\_16(x)) #define m\_16(x) m\_2(m\_8(x)) #define m\_8(x) m\_2(m\_4(x)) #define m\_4(x) m\_2(m\_2(x)) #define m\_2(x) x x #define spacer int : 32; struct { m\_32(spacer) m\_8(spacer) m\_4(spacer) }; </code></pre>

<p>хотя я согласен, что структуры не должны использоваться для доступа к порту ввода/вывода mcu, на оригинальный вопрос можно ответить следующим образом:</p><pre><code>struct \_\_attribute\_\_((packed)) test { char member1; char member2; volatile struct \_\_attribute\_\_((packed)) { private: volatile char spacer\_bytes[7]; } spacer; char member3; char member4; }; </code></pre><p>вам может потребоваться заменить <code>\_\_attribute\_\_((packed))</code> на <code>#pragma pack</code> или аналогичный в зависимости от синтаксиса вашего компилятора.</p><p>смешивание закрытых и открытых членов в структуре обычно приводит к тому, что компоновка памяти больше не гарантируется стандартом c++. однако, если<em> все </em>нестатические члены структуры являются частными, она все еще считается pod/стандартной компоновкой, и поэтому являются структурами, которые их внедряют.</p><p>по какой-то причине gcc выдает предупреждение, если член анонимной структуры является закрытым, поэтому мне пришлось дать ему имя. кроме того, включение его в еще одну анонимную структуру также избавляет от предупреждения (это может быть ошибкой).</p><p>обратите внимание, что член <code>spacer</code> сам по себе не является частным, поэтому доступ к данным по-прежнему возможен следующим образом: </p><pre><code>(char*)(void*)&testobj.spacer; </code></pre><p>однако такое выражение выглядит как очевидный взлом, и, надеюсь, не будет использовано без веской причины, не говоря уже об ошибке.</p>